diff --git a/__tests__/integration/snapshots/tooltip/alphabet-interval-full/step0.html b/__tests__/integration/snapshots/tooltip/alphabet-interval-full/step0.html
new file mode 100644
index 0000000000..1c9e7e687b
--- /dev/null
+++ b/__tests__/integration/snapshots/tooltip/alphabet-interval-full/step0.html
@@ -0,0 +1,75 @@
+
;
diff --git a/__tests__/integration/snapshots/tooltip/alphabet-interval-multi-field/step0.html b/__tests__/integration/snapshots/tooltip/alphabet-interval-multi-field/step0.html
new file mode 100644
index 0000000000..c1f0ec9ac2
--- /dev/null
+++ b/__tests__/integration/snapshots/tooltip/alphabet-interval-multi-field/step0.html
@@ -0,0 +1,75 @@
+;
diff --git a/__tests__/integration/snapshots/tooltip/morley-box-channel/step0.html b/__tests__/integration/snapshots/tooltip/morley-box-channel/step0.html
new file mode 100644
index 0000000000..530fcba36f
--- /dev/null
+++ b/__tests__/integration/snapshots/tooltip/morley-box-channel/step0.html
@@ -0,0 +1,162 @@
+;
diff --git a/__tests__/plots/tooltip/aapl-line.ts b/__tests__/plots/tooltip/aapl-line.ts
index 8c51323869..d22378eb6d 100644
--- a/__tests__/plots/tooltip/aapl-line.ts
+++ b/__tests__/plots/tooltip/aapl-line.ts
@@ -14,6 +14,8 @@ export function aaplLine(): G2Spec {
encode: {
x: 'date',
y: 'close',
+ },
+ tooltip: {
title: (d) => new Date(d.date).toUTCString(),
},
},
diff --git a/__tests__/plots/tooltip/alphabet-interval-full.ts b/__tests__/plots/tooltip/alphabet-interval-full.ts
new file mode 100644
index 0000000000..3639ce25bb
--- /dev/null
+++ b/__tests__/plots/tooltip/alphabet-interval-full.ts
@@ -0,0 +1,27 @@
+import { G2Spec } from '../../../src';
+import { tooltipSteps } from './utils';
+
+export function alphabetIntervalFull(): G2Spec {
+ return {
+ type: 'interval',
+ padding: 0,
+ data: {
+ type: 'fetch',
+ value: 'data/alphabet.csv',
+ },
+ axis: false,
+ legend: false,
+ encode: {
+ x: 'letter',
+ y: 'frequency',
+ color: 'steelblue',
+ },
+ tooltip: {
+ title: { channel: 'color' },
+ items: ['letter', 'frequency'],
+ },
+ interaction: { tooltip: true },
+ };
+}
+
+alphabetIntervalFull.steps = tooltipSteps(0);
diff --git a/__tests__/plots/tooltip/alphabet-interval-multi-field.ts b/__tests__/plots/tooltip/alphabet-interval-multi-field.ts
new file mode 100644
index 0000000000..a448b8e44b
--- /dev/null
+++ b/__tests__/plots/tooltip/alphabet-interval-multi-field.ts
@@ -0,0 +1,31 @@
+import { G2Spec } from '../../../src';
+import { tooltipSteps } from './utils';
+
+export function alphabetIntervalMultiField(): G2Spec {
+ return {
+ type: 'view',
+ children: [
+ {
+ type: 'interval',
+ padding: 0,
+ data: {
+ type: 'fetch',
+ value: 'data/alphabet.csv',
+ },
+ axis: false,
+ legend: false,
+ encode: {
+ x: 'letter',
+ y: 'frequency',
+ color: 'steelblue',
+ },
+ tooltip: [{ field: 'letter' }, { field: 'frequency' }],
+ },
+ ],
+ interaction: {
+ tooltip: true,
+ },
+ };
+}
+
+alphabetIntervalMultiField.steps = tooltipSteps(0);
diff --git a/__tests__/plots/tooltip/alphabet-interval-multi.ts b/__tests__/plots/tooltip/alphabet-interval-multi.ts
index 4322c7b025..bbeb297ae8 100644
--- a/__tests__/plots/tooltip/alphabet-interval-multi.ts
+++ b/__tests__/plots/tooltip/alphabet-interval-multi.ts
@@ -18,8 +18,8 @@ export function alphabetIntervalMulti(): G2Spec {
x: 'letter',
y: 'frequency',
color: 'steelblue',
- tooltip: ['letter', 'frequency'],
},
+ tooltip: ['letter', 'frequency'],
},
],
interaction: {
diff --git a/__tests__/plots/tooltip/alphabet-interval-object.ts b/__tests__/plots/tooltip/alphabet-interval-object.ts
index afc6a21901..be769d0e47 100644
--- a/__tests__/plots/tooltip/alphabet-interval-object.ts
+++ b/__tests__/plots/tooltip/alphabet-interval-object.ts
@@ -18,17 +18,19 @@ export function alphabetIntervalObject(): G2Spec {
x: 'letter',
y: 'frequency',
color: 'steelblue',
- tooltip: (d) => ({
+ },
+ tooltip: [
+ (d) => ({
color: 'red',
value: d.frequency,
name: 'F',
}),
- tooltip1: (d) => ({
+ (d) => ({
color: 'yellow',
value: d.letter,
name: 'L',
}),
- },
+ ],
},
],
interaction: {
diff --git a/__tests__/plots/tooltip/alphabet-interval-title.ts b/__tests__/plots/tooltip/alphabet-interval-title.ts
index d80b21cfd0..e2e69de11b 100644
--- a/__tests__/plots/tooltip/alphabet-interval-title.ts
+++ b/__tests__/plots/tooltip/alphabet-interval-title.ts
@@ -18,6 +18,8 @@ export function alphabetIntervalTitle(): G2Spec {
x: 'letter',
y: 'frequency',
color: 'steelblue',
+ },
+ tooltip: {
title: 'frequency',
},
},
diff --git a/__tests__/plots/tooltip/index.ts b/__tests__/plots/tooltip/index.ts
index 0569ea1cfd..a96173190b 100644
--- a/__tests__/plots/tooltip/index.ts
+++ b/__tests__/plots/tooltip/index.ts
@@ -18,3 +18,6 @@ export { alphabetInterval1dMounted } from './alphabet-interval-1d-mounted';
export { indicesLineItems } from './indices-line-items';
export { flareTreemapPoptip } from './flare-treemap-poptip';
export { flareTreemapPoptipCustom } from './flare-treemap-poptip-custom';
+export { morleyBoxChannel } from './morley-box-channel';
+export { alphabetIntervalMultiField } from './alphabet-interval-multi-field';
+export { alphabetIntervalFull } from './alphabet-interval-full';
diff --git a/__tests__/plots/tooltip/indices-line-chart-facet.ts b/__tests__/plots/tooltip/indices-line-chart-facet.ts
index 19298a5ac6..26eeeb21f4 100644
--- a/__tests__/plots/tooltip/indices-line-chart-facet.ts
+++ b/__tests__/plots/tooltip/indices-line-chart-facet.ts
@@ -26,6 +26,8 @@ export async function indicesLineChartFacet(): Promise {
y: 'Close',
color: 'Symbol',
key: 'Symbol',
+ },
+ tooltip: {
title: (d) => new Date(d.Date).toUTCString(),
},
},
diff --git a/__tests__/plots/tooltip/indices-line-items.ts b/__tests__/plots/tooltip/indices-line-items.ts
index fcfc2bfa2a..6bb10a6cb3 100644
--- a/__tests__/plots/tooltip/indices-line-items.ts
+++ b/__tests__/plots/tooltip/indices-line-items.ts
@@ -23,14 +23,17 @@ export async function indicesLineItems(): Promise {
y: 'Close',
color: 'Symbol',
key: 'Symbol',
+ },
+ tooltip: {
title: (d) => new Date(d.Date).toUTCString(),
+ items: [
+ (d, i, D, V) => ({ name: 'Close', value: V.y.value[i].toFixed(1) }),
+ ],
},
},
],
interaction: {
- tooltip: {
- item: ({ value }) => ({ value: value.toFixed(1) }),
- },
+ tooltip: true,
},
};
}
diff --git a/__tests__/plots/tooltip/indices-line-point-reverse.ts b/__tests__/plots/tooltip/indices-line-point-reverse.ts
index a6b1eb1e0f..9ef97a9ebd 100644
--- a/__tests__/plots/tooltip/indices-line-point-reverse.ts
+++ b/__tests__/plots/tooltip/indices-line-point-reverse.ts
@@ -29,9 +29,8 @@ export async function indicesLinePointReverse(): Promise {
y: 'Close',
color: 'Symbol',
key: 'Symbol',
- title: null,
- tooltip: null,
},
+ tooltip: null,
},
{
type: 'point',
@@ -40,8 +39,8 @@ export async function indicesLinePointReverse(): Promise {
y: 'Close',
color: 'Symbol',
key: 'Symbol',
- tooltip: (d) => new Date(d.Date).toUTCString(),
},
+ tooltip: (d) => new Date(d.Date).toUTCString(),
},
],
interaction: {
diff --git a/__tests__/plots/tooltip/indices-line-reverse.ts b/__tests__/plots/tooltip/indices-line-reverse.ts
index 4e79c4ff2e..f816fbf088 100644
--- a/__tests__/plots/tooltip/indices-line-reverse.ts
+++ b/__tests__/plots/tooltip/indices-line-reverse.ts
@@ -22,8 +22,8 @@ export async function indicesLineReverse(): Promise {
y: 'Close',
color: 'Symbol',
key: 'Symbol',
- title: (d) => new Date(d.Date).toUTCString(),
},
+ tooltip: { title: (d) => new Date(d.Date).toUTCString() },
},
],
interaction: {
diff --git a/__tests__/plots/tooltip/morley-box-channel.ts b/__tests__/plots/tooltip/morley-box-channel.ts
new file mode 100644
index 0000000000..ec9802c0f0
--- /dev/null
+++ b/__tests__/plots/tooltip/morley-box-channel.ts
@@ -0,0 +1,38 @@
+import { G2Spec } from '../../../src';
+import { tooltipSteps } from './utils';
+
+export function morleyBoxChannel(): G2Spec {
+ return {
+ type: 'view',
+ children: [
+ {
+ type: 'boxplot',
+ inset: 6,
+ data: {
+ type: 'fetch',
+ value: 'data/morley.csv',
+ },
+ encode: {
+ x: 'Expt',
+ y: 'Speed',
+ },
+ style: {
+ boxFill: '#aaa',
+ pointStroke: '#000',
+ },
+ tooltip: [
+ { name: 'min', channel: 'y' },
+ { name: 'q1', channel: 'y1' },
+ { name: 'q2', channel: 'y2' },
+ { name: 'q3', channel: 'y3' },
+ { name: 'max', color: 'red', channel: 'y4' },
+ ],
+ },
+ ],
+ interaction: {
+ tooltip: true,
+ },
+ };
+}
+
+morleyBoxChannel.steps = tooltipSteps(0);
diff --git a/__tests__/plots/tooltip/morley-box.ts b/__tests__/plots/tooltip/morley-box.ts
index ffd4fa07e8..7daf8a0ec0 100644
--- a/__tests__/plots/tooltip/morley-box.ts
+++ b/__tests__/plots/tooltip/morley-box.ts
@@ -2,13 +2,7 @@ import { G2Spec } from '../../../src';
import { tooltipSteps } from './utils';
export function morleyBox(): G2Spec {
- const names = {
- tooltip: 'min',
- tooltip1: 'q1',
- tooltip2: 'q2',
- tooltip3: 'q3',
- tooltip4: 'max',
- };
+ const format = (d) => `${d / 1000}k`;
return {
type: 'view',
children: [
@@ -27,16 +21,33 @@ export function morleyBox(): G2Spec {
boxFill: '#aaa',
pointStroke: '#000',
},
+ tooltip: [
+ (d, i, D, V) => ({
+ name: 'min',
+ value: format(V.y.value[i]),
+ }),
+ (d, i, D, V) => ({
+ name: 'q1',
+ value: format(V.y1.value[i]),
+ }),
+ (d, i, D, V) => ({
+ name: 'q2',
+ value: format(V.y2.value[i]),
+ }),
+ (d, i, D, V) => ({
+ name: 'q3',
+ value: format(V.y3.value[i]),
+ }),
+ (d, i, D, V) => ({
+ name: 'max',
+ color: 'red',
+ value: format(V.y4.value[i]),
+ }),
+ ],
},
],
interaction: {
- tooltip: {
- item: ({ channel, value }) => ({
- name: names[channel],
- color: channel === 'tooltip4' ? 'red' : undefined,
- value: `${value / 1000}k`,
- }),
- },
+ tooltip: true,
},
};
}
diff --git a/__tests__/plots/tooltip/stateages-interval-shared.ts b/__tests__/plots/tooltip/stateages-interval-shared.ts
index e0e3097b38..cf9076c836 100644
--- a/__tests__/plots/tooltip/stateages-interval-shared.ts
+++ b/__tests__/plots/tooltip/stateages-interval-shared.ts
@@ -16,7 +16,6 @@ export function stateAgesIntervalShared(): G2Spec {
type: 'fetch',
value: 'data/stateages.csv',
},
- axis: false,
legend: false,
encode: {
x: 'state',
diff --git a/__tests__/plots/tooltip/temperatures-line-point-discrete.ts b/__tests__/plots/tooltip/temperatures-line-point-discrete.ts
index 5f74e98250..3d8da1f807 100644
--- a/__tests__/plots/tooltip/temperatures-line-point-discrete.ts
+++ b/__tests__/plots/tooltip/temperatures-line-point-discrete.ts
@@ -21,12 +21,9 @@ export function temperaturesLinePointDiscrete(): G2Spec {
x: 'month',
y: 'temperature',
color: 'city',
- tooltip: null,
- title: null,
- },
- style: {
- fill: 'white',
},
+ tooltip: null,
+ style: { fill: 'white' },
},
],
interaction: {
diff --git a/__tests__/unit/api/mark.spec.ts b/__tests__/unit/api/mark.spec.ts
index e574cbb68f..b24a33f876 100644
--- a/__tests__/unit/api/mark.spec.ts
+++ b/__tests__/unit/api/mark.spec.ts
@@ -54,6 +54,8 @@ function setOptions(node) {
.attr('insetRight', 40)
.axis('x', { tickCount: 10 })
.legend('y', { title: 'hello' })
+ .tooltip('a')
+ .tooltip('b')
.slider('x', {})
.scrollbar('x', {})
.label({ text: 'hello' })
@@ -107,6 +109,7 @@ function getOptions() {
active: { fill: 'red' },
inactive: { fill: 'blue' },
},
+ tooltip: { items: ['a', 'b'] },
};
}
diff --git a/__tests__/unit/api/props.spec.ts b/__tests__/unit/api/props.spec.ts
index f1e7271117..991faca017 100644
--- a/__tests__/unit/api/props.spec.ts
+++ b/__tests__/unit/api/props.spec.ts
@@ -96,4 +96,27 @@ describe('defineProps', () => {
expect(n1).toBeInstanceOf(Node);
expect(n.type).toBeNull();
});
+
+ it('definedProps([...]) should define mix prop', () => {
+ const N = defineProps([{ type: 'mix', name: 'mix' }])(Node);
+ const n = new N();
+ n.mix('a');
+ n.mix('b');
+ expect(n.mix()).toEqual({ items: ['a', 'b'] });
+ n.mix(['c', 'd']);
+ expect(n.mix()).toEqual({ items: ['c', 'd'] });
+ n.mix({ items: ['f', 'g'] });
+ expect(n.mix()).toEqual({
+ items: ['f', 'g'],
+ });
+ n.mix({ title: 'hello' });
+ expect(n.mix()).toEqual({
+ title: 'hello',
+ });
+ n.mix({ name: 'min', channel: 'y' });
+ expect(n.mix()).toEqual({
+ title: 'hello',
+ items: [{ name: 'min', channel: 'y' }],
+ });
+ });
});
diff --git a/scripts/bench/package.json b/scripts/bench/package.json
index 447531abf9..c6f9e0c32f 100644
--- a/scripts/bench/package.json
+++ b/scripts/bench/package.json
@@ -8,7 +8,6 @@
"@antv/g-canvas": "^1.9.8",
"@antv/g-old": "npm:@antv/g@3.4.10",
"@antv/g-svg": "^1.8.16",
- "@antv/g": "^5.11.0",
"@antv/g2": "^4.2.8",
"@antv/util": "^3.3.2",
"@observablehq/plot": "^0.6.0",
diff --git a/site/docs/api/mark/polygon.zh.md b/site/docs/api/mark/polygon.zh.md
index ff6c773551..749c9562ec 100644
--- a/site/docs/api/mark/polygon.zh.md
+++ b/site/docs/api/mark/polygon.zh.md
@@ -58,7 +58,6 @@ chart
.scale('x', { domain: [0, 800] })
.scale('y', { domain: [0, 600] })
.axis(false)
- .scale('tooltip', { field: 'value' })
.style('stroke', '#fff')
.style('fillOpacity', 0.65);
diff --git a/site/examples/general/.polygon/demo/treemap.ts b/site/examples/general/.polygon/demo/treemap.ts
index 58f3bb4c91..bd418c4172 100644
--- a/site/examples/general/.polygon/demo/treemap.ts
+++ b/site/examples/general/.polygon/demo/treemap.ts
@@ -41,8 +41,10 @@ chart
.encode('y', 'y')
.encode('size', 'r')
.encode('color', (d) => d.parent.data.name)
- .encode('tooltip', (d) => d.parent.data.name)
- .encode('title', '')
+ .tooltip({
+ title: '',
+ items: [(d) => d.parent.data.name],
+ })
.scale('x', { domain: [0, 1] })
.scale('y', { domain: [0, 1], range: [0, 1] })
.scale('size', { type: 'identity' })
diff --git a/site/examples/general/.polygon/demo/voronoi.ts b/site/examples/general/.polygon/demo/voronoi.ts
index 74182b2565..f9c42e0fa7 100644
--- a/site/examples/general/.polygon/demo/voronoi.ts
+++ b/site/examples/general/.polygon/demo/voronoi.ts
@@ -45,7 +45,6 @@ chart
.encode('color', (d) => d.data.value)
.scale('x', { domain: [0, 800] })
.scale('y', { domain: [0, 600] })
- .scale('tooltip', { field: 'value' })
.axis(false)
.style('stroke', '#fff')
.style('fillOpacity', 0.65);
diff --git a/site/examples/interaction/interaction/demo/tooltip-custom.ts b/site/examples/interaction/interaction/demo/tooltip-custom.ts
index 2d9d2b957e..4f25488b10 100644
--- a/site/examples/interaction/interaction/demo/tooltip-custom.ts
+++ b/site/examples/interaction/interaction/demo/tooltip-custom.ts
@@ -6,14 +6,6 @@ const chart = new Chart({
inset: 6,
});
-const names = {
- tooltip: 'min',
- tooltip1: 'q1',
- tooltip2: 'q2',
- tooltip3: 'q3',
- tooltip4: 'max',
-};
-
chart
.boxplot()
.data({
@@ -21,14 +13,13 @@ chart
value: 'https://assets.antv.antgroup.com/g2/morley.json',
})
.encode('x', 'Expt')
- .encode('y', 'Speed');
+ .encode('y', 'Speed')
+ .tooltip({ name: 'min', channel: 'y' })
+ .tooltip({ name: 'q1', channel: 'y1' })
+ .tooltip({ name: 'q2', channel: 'y2' })
+ .tooltip({ name: 'q3', channel: 'y3' })
+ .tooltip({ name: 'max', color: 'red', channel: 'y4' });
-chart.interaction('tooltip', {
- item: ({ channel, value }) => ({
- name: names[channel],
- color: channel === 'tooltip4' ? 'red' : undefined,
- value: `${value / 1000}k`,
- }),
-});
+chart.interaction('tooltip', true);
chart.render();
diff --git a/site/examples/interaction/interaction/demo/tooltip-series.ts b/site/examples/interaction/interaction/demo/tooltip-series.ts
index 12cd151eec..d7df2fda0e 100644
--- a/site/examples/interaction/interaction/demo/tooltip-series.ts
+++ b/site/examples/interaction/interaction/demo/tooltip-series.ts
@@ -15,8 +15,16 @@ chart
.encode('x', (d) => new Date(d.Date))
.encode('y', 'Close')
.encode('color', 'Symbol')
- .encode('title', (d) => d.Date.toLocaleString())
.axis('y', { title: '↑ Change in price (%)' })
+ .tooltip({
+ title: (d) => new Date(d.Date).toUTCString(),
+ items: [
+ (d, i, data, column) => ({
+ name: 'Close',
+ value: column.y.value[i].toFixed(1),
+ }),
+ ],
+ })
.label({
text: 'Symbol',
selector: 'last',
@@ -25,8 +33,6 @@ chart
},
});
-chart.interaction('tooltip', {
- item: ({ value }) => ({ value: value.toFixed(1) }),
-});
+chart.interaction('tooltip', true);
chart.render();
diff --git a/src/api/composition/base.ts b/src/api/composition/base.ts
index a226dd5567..412351470a 100644
--- a/src/api/composition/base.ts
+++ b/src/api/composition/base.ts
@@ -1,10 +1,8 @@
import { DisplayObject } from '@antv/g';
import { Coordinate } from '@antv/coord';
-import { defineProps } from '../props';
import { Node } from '../node';
import { G2Theme, G2ViewDescriptor } from '../../runtime';
-@defineProps([{ name: 'on', type: 'event' }])
export class CompositionNode<
Value extends Record = Record,
ParentValue extends Record = Record,
diff --git a/src/api/mark/mark.ts b/src/api/mark/mark.ts
index 83f8dad939..416f2e51bc 100644
--- a/src/api/mark/mark.ts
+++ b/src/api/mark/mark.ts
@@ -149,6 +149,7 @@ export const props: NodePropertyDescriptor[] = [
{ name: 'slider', type: 'object' },
{ name: 'scrollbar', type: 'object' },
{ name: 'state', type: 'object' },
+ { name: 'tooltip', type: 'mix' },
];
@defineProps(props)
diff --git a/src/api/mark/types.ts b/src/api/mark/types.ts
index 1dbf78bd3e..8998e58cb7 100644
--- a/src/api/mark/types.ts
+++ b/src/api/mark/types.ts
@@ -17,4 +17,5 @@ export type API = {
scrollbar: ObjectAttribute;
legend: ObjectAttribute;
layout: ValueAttribute;
+ tooltip: ValueAttribute;
};
diff --git a/src/api/props.ts b/src/api/props.ts
index 834fcbe5a8..caf90754b9 100644
--- a/src/api/props.ts
+++ b/src/api/props.ts
@@ -1,5 +1,7 @@
+import { isStrictObject } from '../utils/helper';
+
export type NodePropertyDescriptor = {
- type: 'object' | 'value' | 'array' | 'node' | 'container' | 'event';
+ type: 'object' | 'value' | 'array' | 'node' | 'container' | 'mix';
name: string;
key?: string;
ctor?: new (...args: any[]) => any;
@@ -36,6 +38,24 @@ function defineObjectProp(
};
}
+function defineMixProp(Node, { name }: NodePropertyDescriptor) {
+ Node.prototype[name] = function (key) {
+ if (arguments.length === 0) return this.attr(name);
+ if (Array.isArray(key)) return this.attr(name, { items: key });
+ if (
+ isStrictObject(key) &&
+ (key.title !== undefined || key.items !== undefined)
+ ) {
+ return this.attr(name, key);
+ }
+ const obj = this.attr(name) || {};
+ const { items = [] } = obj;
+ items.push(key);
+ obj.items = items;
+ return this.attr(name, obj);
+ };
+}
+
function defineNodeProp(Node, { name, ctor }: NodePropertyDescriptor) {
Node.prototype[name] = function () {
return this.append(ctor);
@@ -62,6 +82,7 @@ export function defineProps(descriptors: NodePropertyDescriptor[]) {
else if (type === 'object') defineObjectProp(Node, descriptor);
else if (type === 'node') defineNodeProp(Node, descriptor);
else if (type === 'container') defineContainerProp(Node, descriptor);
+ else if (type === 'mix') defineMixProp(Node, descriptor);
}
return Node;
};
diff --git a/src/interaction/native/tooltip.ts b/src/interaction/native/tooltip.ts
index 36e14e3d6a..d23af2b233 100644
--- a/src/interaction/native/tooltip.ts
+++ b/src/interaction/native/tooltip.ts
@@ -86,33 +86,16 @@ function filterDefined(obj) {
);
}
-function singleItem(element, item, scale) {
+function singleItem(element) {
const { __data__: datum } = element;
- const { title, ...rest } = datum;
- const defaultColor = itemColorOf(element);
- const items = Object.entries(rest)
- .filter(([key]) => key.startsWith('tooltip'))
- .map(([key, d]: any) => {
- const { field: f, title = f } = scale[key].getOptions();
- const {
- field = undefined,
- color = defaultColor,
- name = field || title,
- value,
- ...rest
- } = normalizeTooltip(d);
- return {
- ...rest,
- color,
- name,
- value,
- ...filterDefined(item({ channel: key, value })),
- };
- })
- .filter(({ value }) => value !== undefined);
+ const { title, items = [] } = datum;
+ const newItems = items.map(({ color = itemColorOf(element), ...item }) => ({
+ ...item,
+ color,
+ }));
return {
...(title && { title }),
- items,
+ items: newItems,
};
}
@@ -157,39 +140,22 @@ function uniqueTitles(titles) {
function groupItems(
elements,
- item,
scale,
data = elements.map((d) => d['__data__']),
) {
const T = uniqueTitles(data.map((d) => d.title)).filter(defined);
- const items = data.flatMap((datum, i) => {
+ const newItems = data.flatMap((datum, i) => {
const element = elements[i];
- const { title, ...rest } = datum;
- const defaultColor = itemColorOf(element);
- return Object.entries(rest)
- .filter(([key]) => key.startsWith('tooltip'))
- .map(([key, d]: any) => {
- const { field: f, title = f } = scale[key].getOptions();
- const {
- field = undefined,
- color = defaultColor,
- name = groupNameOf(scale, datum) || field || title,
- value,
- ...rest
- } = normalizeTooltip(d);
- return {
- ...rest,
- name,
- color,
- value,
- ...filterDefined(item({ channel: key, value })),
- };
- })
- .filter(({ value }) => defined(value));
+ const { items = [], title } = datum;
+ return items.map(({ color = itemColorOf(element), name, ...item }) => ({
+ ...item,
+ color,
+ name: groupNameOf(scale, datum) || name || title,
+ }));
});
return {
...(T.length > 0 && { title: T.join(',') }),
- items,
+ items: newItems,
};
}
@@ -335,7 +301,7 @@ export function seriesTooltip(
const { __data__: data } = element;
return Object.fromEntries(
Object.entries(data)
- .filter(([key]) => key.startsWith('series'))
+ .filter(([key]) => key.startsWith('series') && key !== 'series')
.map(([key, V]) => {
const d = V[index];
return [lowerFirst(key.replace('series', '')), d];
@@ -371,12 +337,7 @@ export function seriesTooltip(
// Get the displayed tooltip data.
const selectedElements = [...seriesElements, ...selectedItems];
- const tooltipData = groupItems(
- selectedElements,
- item,
- scale,
- selectedData,
- );
+ const tooltipData = groupItems(selectedElements, scale, selectedData);
showTooltip(root, tooltipData, mouse[0] + x, mouse[1] + y);
if (crosshairs) {
@@ -418,7 +379,6 @@ export function tooltip(
leading = true,
trailing = false,
groupKey = (d) => d, // group elements by specified key
- item,
}: Record,
) {
const elements = elementsof(root);
@@ -432,9 +392,7 @@ export function tooltip(
const k = groupKey(element);
const group = keyGroup.get(k);
const data =
- group.length === 1
- ? singleItem(group[0], item, scale)
- : groupItems(group, item, scale);
+ group.length === 1 ? singleItem(group[0]) : groupItems(group, scale);
const { offsetX, offsetY } = event;
showTooltip(root, data, offsetX, offsetY);
},
diff --git a/src/mark/utils.ts b/src/mark/utils.ts
index e938eb4b35..40a59eb605 100644
--- a/src/mark/utils.ts
+++ b/src/mark/utils.ts
@@ -26,11 +26,7 @@ export function baseChannels(options: ChannelOptions = {}): Channel[] {
}
export function baseGeometryChannels(options: ChannelOptions = {}): Channel[] {
- return [
- ...baseChannels(options),
- { name: 'title', scale: 'identity' },
- { name: 'tooltip', scale: 'identity', independent: true },
- ];
+ return [...baseChannels(options), { name: 'title', scale: 'identity' }];
}
export function tooltip2d() {
diff --git a/src/runtime/mark.ts b/src/runtime/mark.ts
index d2fa5ee611..cd50c3c834 100644
--- a/src/runtime/mark.ts
+++ b/src/runtime/mark.ts
@@ -26,6 +26,8 @@ import {
maybeVisualChannel,
addGuideToScale,
maybeNonAnimate,
+ normalizeTooltip,
+ extractTooltip,
} from './transform';
export async function initializeMark(
@@ -41,7 +43,7 @@ export async function initializeMark(
context,
);
- const { encode, scale, data } = transformedMark;
+ const { encode, scale, data, tooltip } = transformedMark;
// Skip mark with non-tabular data. Do not skip empty
// data, they are useful for facet to display axes.
@@ -109,7 +111,7 @@ export async function initializeMark(
});
});
- return [transformedMark, { ...partialProps, index: I, channels }];
+ return [transformedMark, { ...partialProps, index: I, channels, tooltip }];
}
export function createColumnOf(library: G2Library): ColumnOf {
@@ -152,9 +154,11 @@ async function applyMarkTransform(
maybeArrayField,
maybeNonAnimate,
addGuideToScale,
+ normalizeTooltip,
...preInference.map(useTransform),
...transform.map(useTransform),
...postInference.map(useTransform),
+ extractTooltip,
];
let index = [];
let transformedMark = mark;
diff --git a/src/runtime/plot.ts b/src/runtime/plot.ts
index 14aa416346..1fd7036489 100644
--- a/src/runtime/plot.ts
+++ b/src/runtime/plot.ts
@@ -520,7 +520,7 @@ function initializeState(
modifier,
key: markKey,
} = mark;
- const { index, channels } = state;
+ const { index, channels, tooltip } = state;
const scale = Object.fromEntries(
channels.map(({ name, scale }) => [name, scale]),
);
@@ -541,6 +541,8 @@ function initializeState(
);
const count = dataDomain || I.length;
const T = modifier ? modifier(P, count, layout) : [];
+ const titleOf = (i) => tooltip.title?.[i]?.value;
+ const itemsOf = (i) => tooltip.items.map((V) => V[i]);
const visualData: Record[] = I.map((d, i) => {
const datum = {
points: P[i],
@@ -548,12 +550,20 @@ function initializeState(
index: d,
markKey,
viewKey: key,
+ ...(tooltip && {
+ title: titleOf(d),
+ items: itemsOf(d),
+ }),
};
for (const [k, V] of Object.entries(value)) {
datum[k] = V[d];
if (S) datum[`series${upperFirst(k)}`] = S[i].map((i) => V[i]);
}
if (S) datum['seriesIndex'] = S[i];
+ if (S && tooltip) {
+ datum['seriesItems'] = S[i].map((si) => itemsOf(si));
+ datum['seriesTitle'] = S[i].map((si) => titleOf(si));
+ }
return datum;
});
state.data = visualData;
diff --git a/src/runtime/transform.ts b/src/runtime/transform.ts
index 4df1c0d068..835543cf25 100644
--- a/src/runtime/transform.ts
+++ b/src/runtime/transform.ts
@@ -1,7 +1,7 @@
import { Primitive } from 'd3-array';
import { deepMix } from '@antv/util';
import { indexOf, mapObject } from '../utils/array';
-import { composeAsync, defined } from '../utils/helper';
+import { composeAsync, defined, isStrictObject } from '../utils/helper';
import { useLibrary } from './library';
import { createColumnOf } from './mark';
import { Data, DataComponent } from './types/data';
@@ -105,6 +105,65 @@ export function extractColumns(
return [I, { ...mark, encode: valuedEncode }];
}
+/**
+ * Normalize mark.tooltip to {title, items}.
+ */
+export function normalizeTooltip(
+ I: number[],
+ mark: G2Mark,
+ context: TransformContext,
+): [number[], G2Mark] {
+ const { tooltip = {} } = mark;
+ if (tooltip === null) return [I, mark];
+ if (Array.isArray(tooltip)) {
+ return [I, { ...mark, tooltip: { items: tooltip } }];
+ }
+ if (isStrictObject(tooltip)) return [I, { ...mark, tooltip }];
+ return [I, { ...mark, tooltip: { items: [tooltip] } }];
+}
+
+export function extractTooltip(
+ I: number[],
+ mark: G2Mark,
+ context: TransformContext,
+): [number[], G2Mark] {
+ const { data, encode, tooltip = {} } = mark;
+ if (tooltip === null) return [I, mark];
+ const valueOf = (item) => {
+ if (!item) return item;
+ if (typeof item === 'string') {
+ return I.map((i) => ({ name: item, value: data[i][item] }));
+ }
+ if (isStrictObject(item)) {
+ const { field, channel, color, name = field } = item;
+ return I.map((i) => ({
+ name,
+ color,
+ value: field
+ ? data[i][field]
+ : channel
+ ? encode[channel].value[i]
+ : null,
+ }));
+ }
+ if (typeof item === 'function') {
+ return I.map((i) => {
+ const v = item(data[i], i, data, encode);
+ if (isStrictObject(v)) return v;
+ return { value: v };
+ });
+ }
+ return item;
+ };
+ const { title, items = [], ...rest } = tooltip;
+ const newTooltip = {
+ title: valueOf(title),
+ items: items.map(valueOf),
+ ...rest,
+ };
+ return [I, { ...mark, tooltip: newTooltip }];
+}
+
export function maybeArrayField(
I: number[],
mark: G2Mark,
diff --git a/src/runtime/types/common.ts b/src/runtime/types/common.ts
index bf28086a4f..ccc72aa16e 100644
--- a/src/runtime/types/common.ts
+++ b/src/runtime/types/common.ts
@@ -68,6 +68,7 @@ export type G2MarkState = {
index?: number[];
data?: Record[];
channels?: ChannelGroups[];
+ tooltip?: any; // @todo
} & Omit;
export type MaybeArray = T | T[];
diff --git a/src/runtime/types/options.ts b/src/runtime/types/options.ts
index 7b7079d213..6098a80acb 100644
--- a/src/runtime/types/options.ts
+++ b/src/runtime/types/options.ts
@@ -116,6 +116,7 @@ export type G2Mark = {
legend?: boolean | Record;
slider?: Record;
scrollbar?: Record;
+ tooltip?: any; // @todo
filter?: (i: number) => boolean;
children?: G2MarkChildrenCallback;
dataDomain?: number;
diff --git a/src/spec/geometry.ts b/src/spec/geometry.ts
index 46d08e5bc6..328e5ec8a3 100644
--- a/src/spec/geometry.ts
+++ b/src/spec/geometry.ts
@@ -1,4 +1,4 @@
-import { MarkComponent } from '../runtime';
+import { MarkComponent, Primitive } from '../runtime';
import { Encode } from './encode';
import { Transform } from './transform';
import { Scale } from './scale';
@@ -93,8 +93,7 @@ export type ChannelTypes =
| 'groupKey'
| 'label'
| 'position'
- | 'series'
- | `tooltip${number}`;
+ | 'series';
export type BaseGeometry<
T extends GeometryTypes,
@@ -149,6 +148,7 @@ export type BaseGeometry<
frame?: boolean;
labels?: Record[];
stack?: boolean;
+ tooltip?: Tooltip;
animate?:
| boolean
| {
@@ -309,3 +309,25 @@ export type WordCloudMark = BaseGeometry<
};
export type CustomComponent = BaseGeometry;
+
+export type Tooltip =
+ | TooltipItem
+ | TooltipItem[]
+ | { title?: Encodeable; items?: TooltipItem[] }
+ | null;
+
+export type TooltipTitle = string | { field?: string; channel?: string };
+
+export type TooltipItem =
+ | string
+ | { name?: string; color?: string; channel?: string; field?: string }
+ | Encodeable
+ | Encodeable<{
+ name?: string;
+ color?: string;
+ value?: Primitive;
+ }>;
+
+export type Encodeable =
+ | T
+ | ((d: any, index: number, data: any[], column: any) => T);
diff --git a/src/transform/maybeTitle.ts b/src/transform/maybeTitle.ts
index 6fc142f460..065bc8eaf2 100644
--- a/src/transform/maybeTitle.ts
+++ b/src/transform/maybeTitle.ts
@@ -1,6 +1,6 @@
import { deepMix } from '@antv/util';
import { TransformComponent as TC } from '../runtime';
-import { column, columnOf } from './utils/helper';
+import { columnOf } from './utils/helper';
export type MaybeTitleOptions = {
channel?: string;
@@ -13,10 +13,18 @@ export const MaybeTitle: TC = (options = {}) => {
const { channel = 'x' } = options;
return (I, mark) => {
const { encode } = mark;
- const { title } = encode;
+ const { tooltip } = mark;
+ if (tooltip === null) return [I, mark];
+ const { title } = tooltip;
if (title !== undefined) return [I, mark];
const [T, ft] = columnOf(encode, channel);
- return [I, deepMix({}, mark, { encode: { title: column(T, ft) } })];
+ if (!T) return [I, mark];
+ return [
+ I,
+ deepMix({}, mark, {
+ tooltip: { title: T.map((d) => ({ value: d, name: ft })) },
+ }),
+ ];
};
};
diff --git a/src/transform/maybeTooltip.ts b/src/transform/maybeTooltip.ts
index f1180bce5e..a71090f485 100644
--- a/src/transform/maybeTooltip.ts
+++ b/src/transform/maybeTooltip.ts
@@ -12,28 +12,23 @@ export type MaybeTooltipOptions = {
export const MaybeTooltip: TC = (options) => {
const { channel } = options;
return (I, mark) => {
- const { encode } = mark;
- const { tooltip } = encode;
- if (tooltip !== undefined) return [I, mark];
+ const { encode, tooltip } = mark;
+ if (tooltip === null) return [I, mark];
+ const { items = [] } = tooltip;
+ if (items.length > 0) return [I, mark];
const channels = Array.isArray(channel) ? channel : [channel];
- let index = 0;
- const entries = channels.flatMap((channel) =>
- Object.entries(encode)
- .filter(([key]) => key.startsWith(channel))
- .flatMap(([key]) => {
+ const newItems = channels.flatMap((channel) =>
+ Object.keys(encode)
+ .filter((key) => key.startsWith(channel))
+ .map((key) => {
const [V, fv] = columnOf(encode, key);
- const E = [[key, column(V)]];
// Only show channel with field.
- if (V && fv !== null) {
- const T = V.map((v) => ({ value: v, field: fv }));
- //@ts-ignore
- E.push([`tooltip${index === 0 ? '' : index}`, column(T, fv)]);
- index++;
- }
- return E;
- }),
+ if (V && fv !== null) return V.map((v) => ({ value: v, name: fv }));
+ return null;
+ })
+ .filter((d) => d !== null),
);
- return [I, deepMix({}, mark, { encode: Object.fromEntries(entries) })];
+ return [I, deepMix({}, mark, { tooltip: { items: newItems } })];
};
};
diff --git a/src/utils/helper.ts b/src/utils/helper.ts
index 50c52d47de..aed5a27801 100644
--- a/src/utils/helper.ts
+++ b/src/utils/helper.ts
@@ -112,5 +112,10 @@ export function maybePercentage(x: number | string, size: number) {
}
export function isStrictObject(d: any): boolean {
- return typeof d === 'object' && !(d instanceof Date) && d !== null;
+ return (
+ typeof d === 'object' &&
+ !(d instanceof Date) &&
+ d !== null &&
+ !Array.isArray(d)
+ );
}