VChart
VChart copied to clipboard
[Feature] linear/time x-axis chart can select only one tooltip datum
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;
- 希望hover 的时候,只高亮左侧的一个点
What does the proposed API look like?
{
tooltipFilter: ( tooltipValue: number, datumValue, datum, field) => boolean
}
最后给到用户的解决方案:
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;