VChart icon indicating copy to clipboard operation
VChart copied to clipboard

[Feature] linear/time x-axis chart can select only one tooltip datum

Open xile611 opened this issue 3 months ago • 1 comments

What problem does this feature solve?

  const spec = {
    theme: {
      name: 'green',
      type: 'light',
      fontFamily: 'TikTok Text,TikTokFont',
      colorScheme: {
        default: [
          'rgb(0, 153, 149)',
          'rgb(254, 194, 76)',
          'rgb(0, 99, 190)',
          'rgb(0, 202, 177)',
          'rgb(153, 136, 235)',
          'rgb(0, 116, 73)',
          'rgb(0, 164, 243)'
        ],
        funnel: ['#028380', '#009995', '#30ACA9', '#5DBEBB', '#88CFCD', '#B1E0DD', '#D9F0EE']
      },
      background: '#FFFFFF',
      component: {
        title: {
          textStyle: {
            fill: 'rgba(0, 0, 0, 0.92)'
          }
        },
        tooltip: {
          panel: {
            backgroundColor: 'rgba(255,255,255,0.95)'
          },
          shape: {
            shapeType: 'circle'
          },
          titleLabel: {
            fontSize: 12,
            fontWeight: 500,
            fill: 'rgba(0, 0, 0, 0.92)',
            fontFamily: 'TikTok Text,TikTokFont'
          },
          keyLabel: {
            fontSize: 12,
            fontWeight: 'normal',
            fill: 'rgba(0, 0, 0, 0.92)',
            fontFamily: 'TikTok Text,TikTokFont'
          },
          valueLabel: {
            fontSize: 12,
            fontWeight: 500,
            fill: 'rgba(0, 0, 0, 0.92)',
            fontFamily: 'TikTok Text,TikTokFont'
          }
        },
        dataZoom: {
          background: {
            fill: 'rgba(0, 0, 0, 0.35)'
          },
          backgroundChart: {
            area: {
              fill: 'rgba(67,142,255,0.3)'
            }
          },
          selectedBackground: {
            fill: 'rgba(67,142,255, 0.1)'
          },
          startHandler: {
            fill: 'rgb(0, 153, 149)'
          },
          endHandler: {
            fill: 'rgb(0, 153, 149)'
          }
        },
        brush: {
          style: {
            fill: 'rgba(67,142,255,0.3)'
          }
        },
        axisBand: {
          tick: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.14)'
            }
          },
          domainLine: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.04)'
            }
          },
          label: {
            style: {
              fill: 'rgba(0, 0, 0, 0.35)'
            }
          },
          title: {
            style: {
              fill: 'rgba(0, 0, 0, 0.92)'
            }
          },
          grid: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.1)'
            }
          }
        },
        axisLinear: {
          tick: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.14)'
            }
          },
          domainLine: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.04)'
            }
          },
          label: {
            style: {
              fill: 'rgba(0, 0, 0, 0.35)'
            }
          },
          title: {
            style: {
              fill: 'rgba(0, 0, 0, 0.92)'
            }
          },
          grid: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.1)'
            }
          }
        },
        discreteLegend: {
          padding: [16, 0],
          item: {
            label: {
              style: {
                fill: 'rgba(0, 0, 0, 0.55)'
              },
              state: {
                unSelected: {
                  fill: 'rgba(0, 0, 0, 0.35)'
                }
              }
            },
            shape: {
              style: {
                symbolType: 'circle',
                size: 8
              }
            }
          },
          pager: {
            handler: {
              style: {
                fill: 'rgb(0, 153, 149)'
              },
              state: {
                disable: {
                  fill: 'rgb(240, 240, 240)'
                }
              }
            }
          }
        },
        markLine: {
          line: {
            style: {
              lineDash: [0],
              stroke: 'rgba(0, 0, 0, 0.14)'
            }
          }
        },
        markPoint: {
          itemContent: {
            text: {
              style: {
                fill: 'rgba(0, 0, 0, 0.55)',
                fontFamily: 'TikTok Text,TikTokFont'
              },
              labelBackground: {
                style: {
                  fill: 'rgba(240, 240, 240, 1)'
                }
              }
            }
          }
        }
      },
      series: {
        gauge: {
          segment: {
            style: {
              fill: 'rgba(0, 0, 0, 0.1)'
            }
          }
        },
        map: {
          area: {
            state: {
              hover: {
                fill: 'rgb(0, 153, 149)'
              }
            }
          }
        },
        scatter: {
          label: {
            visible: false,
            offset: 7,
            position: 'bottom',
            style: {
              fontSize: 12,
              lineWidth: 2,
              fill: 'rgba(0, 0, 0, 0.55)'
            }
          }
        },
        funnel: {
          outerLabel: {
            line: {
              style: {
                fontSize: 12,
                stroke: 'rgba(0, 0, 0, 0.1)'
              }
            }
          }
        },
        bar: {
          label: {
            style: {
              fontSize: 12,
              fill: 'rgba(0, 0, 0, 0.35)'
            }
          }
        }
      }
    },
    type: 'common',
    seriesField: 'color',
    legends: {
      orient: 'top',
      position: 'start',
      style: {
        dy: -12
      },
      item: {
        shape: {
          space: 4,
          style: {
            size: 10,
            symbolType: 'circle'
          }
        }
      }
    },
    data: [
      {
        id: 'data-0',
        values: [
          {
            'Final Price': 78,
            tag: 'Final Price',
            time: '09/12/2025 12:00 AM',
            'Final Price-amount': '78',
            'Final Price-amountFormatted': '$78.00'
          },
          {
            'Final Price': 78,
            tag: 'Final Price',
            time: '09/12/2025 7:06 PM',
            'Final Price-amount': '78',
            'Final Price-amountFormatted': '$78.00'
          },
          {
            'Final Price': 88,
            tag: 'Final Price',
            time: '09/15/2025 7:18 PM',
            'Final Price-amount': '88',
            'Final Price-amountFormatted': '$88.00'
          },
          {
            'Final Price': 32.11,
            tag: 'Final Price',
            time: '09/21/2025 12:00 AM',
            'Final Price-amount': '32.11',
            'Final Price-amountFormatted': '$32.11'
          },
          {
            'Final Price': 88,
            tag: 'Final Price',
            time: '09/22/2025 12:00 AM',
            'Final Price-amount': '88',
            'Final Price-amountFormatted': '$88.00'
          },
          {
            'Final Price': 88,
            tag: 'Final Price',
            time: '09/26/2025 12:00 AM',
            'Final Price-amount': '88',
            'Final Price-amountFormatted': '$88.00'
          }
        ].map(entry => {
          return {
            ...entry,
            time: new Date(entry.time).getTime()
          };
        })
      }
    ],
    padding: [0, 0, 2, 8],
    tooltip: {
      mark: {
        position: 'left',
        title: {
          value: 'Final Price'
        }
      },
      dimension: {
        position: 'left',
        title: {
          value: 'Final Price'
        },
        content: {
          key: datum => {
            return formatTimestampToYMDHM(datum?.time);
          },
          value: datum => {
            return datum?.['Final Price-amountFormatted'];
          }
        }
      }
    },
    crosshair: {
      xField: {
        visible: true,
        line: {
          visible: true,
          type: 'line'
        }
      }
    },
    axes: [
      {
        id: 'leftYAxis',
        orient: 'left',
        tickCount: 5,
        exactTickCount: true,
        zero: false,
        label: {},
        seriesId: ['line-0-Final Price']
      },
      {
        orient: 'bottom',
        label: {
          visible: true
        },
        // type: 'linear',
        type: 'time',
        layers: [
          {
            tickStep: 86400,
            timeFormat: '%m/%d/%Y'
          }
        ],
        // -86400000
        tooltipFilterRange: [-1 * 60 * 60 * 1000, 0]
      }
    ],
    series: [
      {
        type: 'line',
        id: 'line-0-Final Price',
        dataId: 'data-0',
        name: 'Final Price',
        seriesField: 'tag',
        xField: 'time',
        yField: 'Final Price',
        stack: false,
        point: {
          visible: false,
          state: {
            dimension_hover: {
              stroke: '#fff',
              shadowColor: 'rgba(0, 0, 0, 0.08)',
              shadowOffsetY: 2,
              lineWidth: 2
            }
          }
        },
        activePoint: true,
        line: {
          style: {
            curveType: 'stepAfter'
          }
        }
      }
    ],
    color: {
      type: 'ordinal',
      domain: ['Final Price'],
      range: [
        'rgb(0, 153, 149)',
        'rgb(254, 194, 76)',
        'rgb(0, 99, 190)',
        'rgb(0, 202, 177)',
        'rgb(153, 136, 235)',
        'rgb(0, 116, 73)',
        'rgb(0, 164, 243)'
      ]
    },
    markLine: [
      {
        y: 100,
        label: {
          position: 'insideStartTop',
          text: 'Original Price',
          labelBackground: {
            visible: false,
            style: {
              fill: 'rgba(0, 0, 0, 0.92)',
              fontSize: 12,
              lineHeight: 18,
              fontWeight: 400
            }
          }
        },
        line: {
          style: {
            stroke: 'rgba(0, 0, 0, 0.55)',
            lineDash: [2, 3]
          }
        },
        startSymbol: {
          visible: true,
          symbolType: 'triangleDown'
        },
        endSymbol: {
          visible: false
        },
        autoRange: true
      }
    ]
  };

const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();

// Just for the convenience of console debugging, DO NOT COPY!
window['vchart'] = vchart;
  1. 希望hover 的时候,只高亮左侧的一个点

What does the proposed API look like?

{
   tooltipFilter: ( tooltipValue: number, datumValue, datum,  field) => boolean
}

xile611 avatar Sep 11 '25 07:09 xile611

最后给到用户的解决方案:

    function formatTimestampToYMDHM(timestamp) {
    const date = new Date(timestamp);
    const pad = (n) => (n < 10 ? '0' + n : n);
    const year = date.getFullYear();
    const month = pad(date.getMonth() + 1);
    const day = pad(date.getDate());
    const hour = pad(date.getHours());
    const minute = pad(date.getMinutes());
    return `${year}-${month}-${day} ${hour}:${minute}`;
  }

  const spec = {
    theme: {
      name: 'green',
      type: 'light',
      fontFamily: 'TikTok Text,TikTokFont',
      colorScheme: {
        default: [
          'rgb(0, 153, 149)',
          'rgb(254, 194, 76)',
          'rgb(0, 99, 190)',
          'rgb(0, 202, 177)',
          'rgb(153, 136, 235)',
          'rgb(0, 116, 73)',
          'rgb(0, 164, 243)'
        ],
        funnel: ['#028380', '#009995', '#30ACA9', '#5DBEBB', '#88CFCD', '#B1E0DD', '#D9F0EE']
      },
      background: '#FFFFFF',
      component: {
        title: {
          textStyle: {
            fill: 'rgba(0, 0, 0, 0.92)'
          }
        },
        tooltip: {
          panel: {
            backgroundColor: 'rgba(255,255,255,0.95)'
          },
          shape: {
            shapeType: 'circle'
          },
          titleLabel: {
            fontSize: 12,
            fontWeight: 500,
            fill: 'rgba(0, 0, 0, 0.92)',
            fontFamily: 'TikTok Text,TikTokFont'
          },
          keyLabel: {
            fontSize: 12,
            fontWeight: 'normal',
            fill: 'rgba(0, 0, 0, 0.92)',
            fontFamily: 'TikTok Text,TikTokFont'
          },
          valueLabel: {
            fontSize: 12,
            fontWeight: 500,
            fill: 'rgba(0, 0, 0, 0.92)',
            fontFamily: 'TikTok Text,TikTokFont'
          }
        },
        dataZoom: {
          background: {
            fill: 'rgba(0, 0, 0, 0.35)'
          },
          backgroundChart: {
            area: {
              fill: 'rgba(67,142,255,0.3)'
            }
          },
          selectedBackground: {
            fill: 'rgba(67,142,255, 0.1)'
          },
          startHandler: {
            fill: 'rgb(0, 153, 149)'
          },
          endHandler: {
            fill: 'rgb(0, 153, 149)'
          }
        },
        brush: {
          style: {
            fill: 'rgba(67,142,255,0.3)'
          }
        },
        axisBand: {
          tick: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.14)'
            }
          },
          domainLine: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.04)'
            }
          },
          label: {
            style: {
              fill: 'rgba(0, 0, 0, 0.35)'
            }
          },
          title: {
            style: {
              fill: 'rgba(0, 0, 0, 0.92)'
            }
          },
          grid: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.1)'
            }
          }
        },
        axisLinear: {
          tick: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.14)'
            }
          },
          domainLine: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.04)'
            }
          },
          label: {
            style: {
              fill: 'rgba(0, 0, 0, 0.35)'
            }
          },
          title: {
            style: {
              fill: 'rgba(0, 0, 0, 0.92)'
            }
          },
          grid: {
            style: {
              stroke: 'rgba(0, 0, 0, 0.1)'
            }
          }
        },
        discreteLegend: {
          padding: [16, 0],
          item: {
            label: {
              style: {
                fill: 'rgba(0, 0, 0, 0.55)'
              },
              state: {
                unSelected: {
                  fill: 'rgba(0, 0, 0, 0.35)'
                }
              }
            },
            shape: {
              style: {
                symbolType: 'circle',
                size: 8
              }
            }
          },
          pager: {
            handler: {
              style: {
                fill: 'rgb(0, 153, 149)'
              },
              state: {
                disable: {
                  fill: 'rgb(240, 240, 240)'
                }
              }
            }
          }
        },
        markLine: {
          line: {
            style: {
              lineDash: [0],
              stroke: 'rgba(0, 0, 0, 0.14)'
            }
          }
        },
        markPoint: {
          itemContent: {
            text: {
              style: {
                fill: 'rgba(0, 0, 0, 0.55)',
                fontFamily: 'TikTok Text,TikTokFont'
              },
              labelBackground: {
                style: {
                  fill: 'rgba(240, 240, 240, 1)'
                }
              }
            }
          }
        }
      },
      series: {
        gauge: {
          segment: {
            style: {
              fill: 'rgba(0, 0, 0, 0.1)'
            }
          }
        },
        map: {
          area: {
            state: {
              hover: {
                fill: 'rgb(0, 153, 149)'
              }
            }
          }
        },
        scatter: {
          label: {
            visible: false,
            offset: 7,
            position: 'bottom',
            style: {
              fontSize: 12,
              lineWidth: 2,
              fill: 'rgba(0, 0, 0, 0.55)'
            }
          }
        },
        funnel: {
          outerLabel: {
            line: {
              style: {
                fontSize: 12,
                stroke: 'rgba(0, 0, 0, 0.1)'
              }
            }
          }
        },
        bar: {
          label: {
            style: {
              fontSize: 12,
              fill: 'rgba(0, 0, 0, 0.35)'
            }
          }
        }
      }
    },
    type: 'common',
    seriesField: 'color',
    legends: {
      orient: 'top',
      position: 'start',
      style: {
        dy: -12
      },
      item: {
        shape: {
          space: 4,
          style: {
            size: 10,
            symbolType: 'circle'
          }
        }
      }
    },
    data: [
      {
        id: 'data-0',
        values: [
          {
            'Final Price': 78,
            tag: 'Final Price',
            time: '09/12/2025 12:00 AM',
            'Final Price-amount': '78',
            'Final Price-amountFormatted': '$78.00'
          },
          {
            'Final Price': 78,
            tag: 'Final Price',
            time: '09/12/2025 7:06 PM',
            'Final Price-amount': '78',
            'Final Price-amountFormatted': '$78.00'
          },
          {
            'Final Price': 88,
            tag: 'Final Price',
            time: '09/15/2025 7:18 PM',
            'Final Price-amount': '88',
            'Final Price-amountFormatted': '$88.00'
          },
          {
            'Final Price': 32.11,
            tag: 'Final Price',
            time: '09/21/2025 12:00 AM',
            'Final Price-amount': '32.11',
            'Final Price-amountFormatted': '$32.11'
          },
          {
            'Final Price': 88,
            tag: 'Final Price',
            time: '09/22/2025 12:00 AM',
            'Final Price-amount': '88',
            'Final Price-amountFormatted': '$88.00'
          },
          {
            'Final Price': 88,
            tag: 'Final Price',
            time: '09/26/2025 12:00 AM',
            'Final Price-amount': '88',
            'Final Price-amountFormatted': '$88.00'
          }
        ].map(entry => {
          return {
            ...entry,
            time: new Date(entry.time).getTime()
          };
        })
      }
    ],
    padding: [0, 0, 2, 8],
    tooltip: {
      mark: {
        position: 'left',
        title: {
          value: 'Final Price'
        }
      },
      dimension: {
        position: 'left',
        title: {
          value: 'Final Price'
        },
        content: {
          key: datum => {
            return formatTimestampToYMDHM(datum?.time);
          },
          value: datum => {
            return datum?.['Final Price-amountFormatted'];
          }
        }
      }
    },
    crosshair: {
      followTooltip: true,
      xField: {
        visible: true,
        line: {
          visible: true,
          type: 'line'
        }
      }
    },
    axes: [
      {
        id: 'leftYAxis',
        orient: 'left',
        tickCount: 5,
        exactTickCount: true,
        zero: false,
        label: {},
        seriesId: ['line-0-Final Price']
      },
      {
        orient: 'bottom',
        label: {
          visible: true
        },
        // type: 'linear',
        type: 'time',
        layers: [
          {
            tickStep: 86400,
            timeFormat: '%m/%d/%Y'
          }
        ],
        // -86400000
        // tooltipFilterRange: [-1 * 60 * 60 * 1000, 0]
      }
    ],
    series: [
      {
        type: 'line',
        id: 'line-0-Final Price',
        dataId: 'data-0',
        name: 'Final Price',
        seriesField: 'tag',
        xField: 'time',
        yField: 'Final Price',
        stack: false,
        point: {
          visible: false,
          state: {
            dimension_hover: {
              stroke: '#fff',
              shadowColor: 'rgba(0, 0, 0, 0.08)',
              shadowOffsetY: 2,
              lineWidth: 2
            }
          }
        },
        activePoint: true,
        line: {
          style: {
            curveType: 'stepAfter'
          }
        }
      }
    ],
    color: {
      type: 'ordinal',
      domain: ['Final Price'],
      range: [
        'rgb(0, 153, 149)',
        'rgb(254, 194, 76)',
        'rgb(0, 99, 190)',
        'rgb(0, 202, 177)',
        'rgb(153, 136, 235)',
        'rgb(0, 116, 73)',
        'rgb(0, 164, 243)'
      ]
    },
    markLine: [
      {
        y: 100,
        label: {
          position: 'insideStartTop',
          text: 'Original Price',
          labelBackground: {
            visible: false,
            style: {
              fill: 'rgba(0, 0, 0, 0.92)',
              fontSize: 12,
              lineHeight: 18,
              fontWeight: 400
            }
          }
        },
        line: {
          style: {
            stroke: 'rgba(0, 0, 0, 0.55)',
            lineDash: [2, 3]
          }
        },
        startSymbol: {
          visible: true,
          symbolType: 'triangleDown'
        },
        endSymbol: {
          visible: false
        },
        autoRange: true
      }
    ]
  };

const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();

// Just for the convenience of console debugging, DO NOT COPY!
window['vchart'] = vchart;

xile611 avatar Nov 13 '25 12:11 xile611