diff --git a/__tests__/integration/snapshots/interaction/change-size-polar-crosshairs-x-y/step0.svg b/__tests__/integration/snapshots/interaction/change-size-polar-crosshairs-x-y/step0.svg
new file mode 100644
index 0000000000..7a24d8b77e
--- /dev/null
+++ b/__tests__/integration/snapshots/interaction/change-size-polar-crosshairs-x-y/step0.svg
@@ -0,0 +1,428 @@
+
\ No newline at end of file
diff --git a/__tests__/integration/snapshots/interaction/change-size-polar-crosshairs-x-y/step1.svg b/__tests__/integration/snapshots/interaction/change-size-polar-crosshairs-x-y/step1.svg
new file mode 100644
index 0000000000..d2279f929c
--- /dev/null
+++ b/__tests__/integration/snapshots/interaction/change-size-polar-crosshairs-x-y/step1.svg
@@ -0,0 +1,428 @@
+
\ No newline at end of file
diff --git a/__tests__/integration/snapshots/interaction/indices-line-crosshairs-x-y/step0.svg b/__tests__/integration/snapshots/interaction/indices-line-crosshairs-x-y/step0.svg
new file mode 100644
index 0000000000..1688ae702f
--- /dev/null
+++ b/__tests__/integration/snapshots/interaction/indices-line-crosshairs-x-y/step0.svg
@@ -0,0 +1,212 @@
+
\ No newline at end of file
diff --git a/__tests__/integration/snapshots/interaction/indices-line-crosshairs-x-y/step1.svg b/__tests__/integration/snapshots/interaction/indices-line-crosshairs-x-y/step1.svg
new file mode 100644
index 0000000000..de2542e6ea
--- /dev/null
+++ b/__tests__/integration/snapshots/interaction/indices-line-crosshairs-x-y/step1.svg
@@ -0,0 +1,212 @@
+
\ No newline at end of file
diff --git a/__tests__/plots/interaction/change-size-polar-crosshairs-xy.ts b/__tests__/plots/interaction/change-size-polar-crosshairs-xy.ts
new file mode 100644
index 0000000000..0fccdeb5b2
--- /dev/null
+++ b/__tests__/plots/interaction/change-size-polar-crosshairs-xy.ts
@@ -0,0 +1,107 @@
+import { G2Spec, PLOT_CLASS_NAME } from '../../../src';
+import { step } from './utils';
+
+export async function changeSizePolarCrosshairsXY(): Promise {
+ const data = [
+ { item: 'Design', type: 'a', score: 70 },
+ { item: 'Design', type: 'b', score: 30 },
+ { item: 'Development', type: 'a', score: 60 },
+ { item: 'Development', type: 'b', score: 70 },
+ { item: 'Marketing', type: 'a', score: 50 },
+ { item: 'Marketing', type: 'b', score: 60 },
+ { item: 'Users', type: 'a', score: 40 },
+ { item: 'Users', type: 'b', score: 50 },
+ { item: 'Test', type: 'a', score: 60 },
+ { item: 'Test', type: 'b', score: 70 },
+ { item: 'Language', type: 'a', score: 70 },
+ { item: 'Language', type: 'b', score: 50 },
+ { item: 'Technology', type: 'a', score: 50 },
+ { item: 'Technology', type: 'b', score: 40 },
+ { item: 'Support', type: 'a', score: 30 },
+ { item: 'Support', type: 'b', score: 40 },
+ { item: 'Sales', type: 'a', score: 60 },
+ { item: 'Sales', type: 'b', score: 40 },
+ { item: 'UX', type: 'a', score: 50 },
+ { item: 'UX', type: 'b', score: 60 },
+ ];
+
+ return {
+ type: 'view',
+ coordinate: {
+ type: 'polar',
+ },
+ scale: {
+ x: { padding: 0.5, align: 0 },
+ y: { tickCount: 5, domainMax: 80 },
+ },
+ autoFit: true,
+ data,
+ interaction: {
+ legendFilter: false,
+ elementPointMove: true,
+ tooltip: {
+ crosshairs: true,
+ crosshairsStroke: 'red',
+ crosshairsLineDash: [4, 4],
+ },
+ },
+ axis: {
+ x: {
+ grid: true,
+ gridStrokeWidth: 1,
+ tick: false,
+ gridLineDash: [0, 0],
+ },
+ y: {
+ zIndex: 1,
+ title: false,
+ gridConnect: 'line',
+ gridStrokeWidth: 1,
+ gridLineDash: [0, 0],
+ },
+ },
+ children: [
+ {
+ type: 'area',
+ encode: {
+ x: 'item',
+ y: 'score',
+ color: 'type',
+ key: 'type',
+ },
+ style: {
+ fillOpacity: 0.5,
+ },
+ },
+ {
+ type: 'line',
+ encode: {
+ x: 'item',
+ y: 'score',
+ color: 'type',
+ key: 'type',
+ },
+ style: {
+ lineWidth: 2,
+ },
+ },
+ ],
+ };
+}
+
+changeSizePolarCrosshairsXY.tooltip = true;
+
+changeSizePolarCrosshairsXY.steps = ({ canvas }) => {
+ const { document } = canvas;
+ const [plot] = document.getElementsByClassName(PLOT_CLASS_NAME);
+ return [
+ step(plot, 'pointermove', {
+ offsetX: 100,
+ offsetY: 350,
+ }),
+ step(plot, 'pointermove', {
+ offsetX: 176,
+ offsetY: 350,
+ }),
+ ];
+};
diff --git a/__tests__/plots/interaction/index.ts b/__tests__/plots/interaction/index.ts
index 3199918c5a..20159216ab 100644
--- a/__tests__/plots/interaction/index.ts
+++ b/__tests__/plots/interaction/index.ts
@@ -78,3 +78,5 @@ export { profitIntervalLegendFilterElementHighlight } from './profit-interval-le
export { stocksLineSlider } from './stocks-line-slider';
export { alphabetIntervalCustom } from './alphabet-interval-custom';
export { mockIntervalTooltipBackground } from './mock-interval-tooltip-background';
+export { indicesLineCrosshairsXY } from './indices-line-crosshairs-xy';
+export { changeSizePolarCrosshairsXY } from './change-size-polar-crosshairs-xy';
diff --git a/__tests__/plots/interaction/indices-line-chart-facet.ts b/__tests__/plots/interaction/indices-line-chart-facet.ts
index f307c71f1f..e28591c2db 100644
--- a/__tests__/plots/interaction/indices-line-chart-facet.ts
+++ b/__tests__/plots/interaction/indices-line-chart-facet.ts
@@ -38,8 +38,8 @@ export async function indicesLineChartFacet(): Promise {
series: true,
facet: true,
body: false,
- crosshairs: true,
- crosshairsLineWidth: 30,
+ crosshairsY: true,
+ crosshairsYLineWidth: 30,
marker: false,
},
},
diff --git a/__tests__/plots/interaction/indices-line-crosshairs-xy.ts b/__tests__/plots/interaction/indices-line-crosshairs-xy.ts
new file mode 100644
index 0000000000..9ecb12f84e
--- /dev/null
+++ b/__tests__/plots/interaction/indices-line-crosshairs-xy.ts
@@ -0,0 +1,55 @@
+import { csv } from 'd3-fetch';
+import { autoType } from 'd3-dsv';
+import { G2Spec, PLOT_CLASS_NAME } from '../../../src';
+import { step } from './utils';
+
+export async function indicesLineCrosshairsXY(): Promise {
+ const data = await csv('data/indices.csv', autoType);
+ return {
+ type: 'view',
+ children: [
+ {
+ type: 'line',
+ data,
+ axis: {
+ y: { labelAutoRotate: false },
+ },
+ transform: [{ type: 'normalizeY', basis: 'first', groupBy: 'color' }],
+ legend: false,
+ encode: {
+ x: 'Date',
+ y: 'Close',
+ color: 'Symbol',
+ key: 'Symbol',
+ },
+ state: {
+ active: { stroke: 'red' },
+ },
+ },
+ ],
+ interaction: {
+ tooltip: {
+ crosshairs: true,
+ crosshairsXStroke: 'red',
+ crosshairsYStroke: 'blue',
+ },
+ },
+ };
+}
+
+indicesLineCrosshairsXY.tooltip = true;
+
+indicesLineCrosshairsXY.steps = ({ canvas }) => {
+ const { document } = canvas;
+ const [plot] = document.getElementsByClassName(PLOT_CLASS_NAME);
+ return [
+ step(plot, 'pointermove', {
+ offsetX: 100,
+ offsetY: 350,
+ }),
+ step(plot, 'pointermove', {
+ offsetX: 176,
+ offsetY: 350,
+ }),
+ ];
+};
diff --git a/__tests__/plots/interaction/points-point-regression-quad-inset.ts b/__tests__/plots/interaction/points-point-regression-quad-inset.ts
index 4b929e260a..d6a9c4466d 100644
--- a/__tests__/plots/interaction/points-point-regression-quad-inset.ts
+++ b/__tests__/plots/interaction/points-point-regression-quad-inset.ts
@@ -17,7 +17,11 @@ export function pointsPointRegressionQuadInset(): G2Spec {
scale: { x: { domain: [-4, 4] }, y: { domain: [-2, 14] } },
axis: { x: { title: false }, y: { title: false } },
interaction: {
- tooltip: { body: false, crosshairsLineWidth: 30, marker: false },
+ tooltip: {
+ body: false,
+ crosshairsLineWidth: 30,
+ marker: false,
+ },
},
children: [
{
diff --git a/__tests__/plots/interaction/points-point-regression-quad-transpose.ts b/__tests__/plots/interaction/points-point-regression-quad-transpose.ts
index 4524fa94c2..407ca0919f 100644
--- a/__tests__/plots/interaction/points-point-regression-quad-transpose.ts
+++ b/__tests__/plots/interaction/points-point-regression-quad-transpose.ts
@@ -16,7 +16,11 @@ export function pointsPointRegressionQuadTranspose(): G2Spec {
scale: { x: { domain: [-4, 4] }, y: { domain: [-2, 14] } },
axis: { x: { title: false }, y: { title: false } },
interaction: {
- tooltip: { body: false, crosshairsLineWidth: 30, marker: false },
+ tooltip: {
+ body: false,
+ crosshairsLineWidth: 30,
+ marker: false,
+ },
},
coordinate: { transform: [{ type: 'transpose' }] },
children: [
diff --git a/__tests__/plots/interaction/points-point-regression-quad.ts b/__tests__/plots/interaction/points-point-regression-quad.ts
index adffbd8138..e313162e5a 100644
--- a/__tests__/plots/interaction/points-point-regression-quad.ts
+++ b/__tests__/plots/interaction/points-point-regression-quad.ts
@@ -16,7 +16,11 @@ export function pointsPointRegressionQuad(): G2Spec {
scale: { x: { domain: [-4, 4] }, y: { domain: [-2, 14] } },
axis: { x: { title: false }, y: { title: false } },
interaction: {
- tooltip: { body: false, crosshairsLineWidth: 30, marker: false },
+ tooltip: {
+ body: false,
+ crosshairsLineWidth: 30,
+ marker: false,
+ },
},
children: [
{
diff --git a/__tests__/plots/interaction/stocks-line-slider.ts b/__tests__/plots/interaction/stocks-line-slider.ts
index 8795b1054b..29798a614e 100644
--- a/__tests__/plots/interaction/stocks-line-slider.ts
+++ b/__tests__/plots/interaction/stocks-line-slider.ts
@@ -26,7 +26,12 @@ export function stocksLineSlider(): G2Spec {
tooltip: false,
},
],
- interaction: { tooltip: { crosshairsLineWidth: 10, body: false } },
+ interaction: {
+ tooltip: {
+ crosshairsLineWidth: 10,
+ body: false,
+ },
+ },
};
}
diff --git a/site/docs/spec/interaction/tooltip.zh.md b/site/docs/spec/interaction/tooltip.zh.md
index d3f1c7f1cb..02f70f81e5 100644
--- a/site/docs/spec/interaction/tooltip.zh.md
+++ b/site/docs/spec/interaction/tooltip.zh.md
@@ -46,8 +46,12 @@ chart.render();
| position | tooltip 位置 | `TooltipPosition` | - |
| mount | tooltip 渲染的 dom 节点 | `string` \| `HTMLElement` | 图表容器 |
| bounding | tooltip 渲染的限制区域,超出会自动调整位置 | `BBox` | 图表区域大小 |
-| crosshairs | 是否展示指示线 | `boolean` | - |
-| `crosshairs${StyleAttrs}` | 指示线的样式 | `number \| string` | - |
+| crosshairs | 是否展示指示线 | `boolean` | - |
+| crosshairsX | 是否展示X方向指示线 | `boolean` | - |
+| crosshairsY | 是否展示Y方向指示线 | `boolean` | - |
+| `crosshairs${StyleAttrs}` | 指示线的样式 | `number \| string` | - |
+| `crosshairsX${StyleAttrs}` | X方向指示线的样式(优先级更高) | `number \| string` | - |
+| `crosshairsY${StyleAttrs}` | Y方向指示线的样式 (优先级很高) | `number \| string` | - |
| `marker${StyleAttrs}` | marker 的样式 | `number \| string` | - |
| render | 自定义 tooltip 渲染函数 | `(event, options) => HTMLElement \| string` | - |
| sort | item 排序器 | `(d: TooltipItemValue) => any` | - |
diff --git a/src/interaction/tooltip.ts b/src/interaction/tooltip.ts
index 4a0b94fb7e..c290c04e24 100644
--- a/src/interaction/tooltip.ts
+++ b/src/interaction/tooltip.ts
@@ -5,7 +5,7 @@ import { Tooltip as TooltipComponent } from '@antv/component';
import { Constant, Band } from '@antv/scale';
import { defined, subObject } from '../utils/helper';
import { isTranspose, isPolar } from '../utils/coordinate';
-import { angle, sub } from '../utils/vector';
+import { angle, sub, dist } from '../utils/vector';
import { invert } from '../utils/scale';
import { BBox } from '../runtime';
import {
@@ -288,6 +288,106 @@ function groupItems(
};
}
+function updateRuleX(
+ root,
+ points,
+ mouse,
+ {
+ plotWidth,
+ plotHeight,
+ mainWidth,
+ mainHeight,
+ startX,
+ startY,
+ transposed,
+ polar,
+ insetLeft,
+ insetTop,
+ ...rest
+ },
+) {
+ const defaults = {
+ lineWidth: 1,
+ stroke: '#1b1e23',
+ strokeOpacity: 0.5,
+ ...rest,
+ };
+
+ const createCircle = (cx, cy, r) => {
+ const circle = new Circle({
+ style: {
+ cx,
+ cy,
+ r,
+ ...defaults,
+ },
+ });
+ root.appendChild(circle);
+ return circle;
+ };
+
+ const createLine = (x1, x2, y1, y2) => {
+ const line = new Line({
+ style: {
+ x1,
+ x2,
+ y1,
+ y2,
+ ...defaults,
+ },
+ });
+ root.appendChild(line);
+ return line;
+ };
+
+ const minDistPoint = (mouse, points) => {
+ // only one point do not need compute
+ if (points.length === 1) {
+ return points[0];
+ }
+ const dists = points.map((p) => dist(p, mouse));
+ const minDistIndex = minIndex(dists, (d) => d);
+ return points[minDistIndex];
+ };
+
+ const target = minDistPoint(mouse, points);
+
+ const pointsOf = () => {
+ if (transposed)
+ return [
+ startX + target[0],
+ startX + target[0],
+ startY,
+ startY + plotHeight,
+ ];
+ return [startX, startX + plotWidth, target[1] + startY, target[1] + startY];
+ };
+
+ const pointsOfPolar = () => {
+ const cx = startX + insetLeft + mainWidth / 2;
+ const cy = startY + insetTop + mainHeight / 2;
+ const cdist = dist([cx, cy], target);
+ return [cx, cy, cdist];
+ };
+
+ if (polar) {
+ const [cx, cy, r] = pointsOfPolar();
+ const ruleX = root.ruleX || createCircle(cx, cy, r);
+ ruleX.style.cx = cx;
+ ruleX.style.cy = cy;
+ ruleX.style.r = r;
+ root.ruleX = ruleX;
+ } else {
+ const [x1, x2, y1, y2] = pointsOf();
+ const ruleX = root.ruleX || createLine(x1, x2, y1, y2);
+ ruleX.style.x1 = x1;
+ ruleX.style.x2 = x2;
+ ruleX.style.y1 = y1;
+ ruleX.style.y2 = y2;
+ root.ruleX = ruleX;
+ }
+}
+
function updateRuleY(
root,
points,
@@ -311,10 +411,12 @@ function updateRuleY(
strokeOpacity: 0.5,
...rest,
};
+
const Y = points.map((p) => p[1]);
const X = points.map((p) => p[0]);
const y = mean(Y);
const x = mean(X);
+
const pointsOf = () => {
if (polar) {
const r = Math.min(mainWidth, mainHeight) / 2;
@@ -328,6 +430,7 @@ function updateRuleY(
if (transposed) return [startX, startX + plotWidth, y + startY, y + startY];
return [x + startX, x + startX, startY, startY + plotHeight];
};
+
const [x1, x2, y1, y2] = pointsOf();
const createLine = () => {
const line = new Line({
@@ -360,6 +463,13 @@ function hideRuleY(root) {
}
}
+function hideRuleX(root) {
+ if (root.ruleX) {
+ root.ruleX.remove();
+ root.ruleX = undefined;
+ }
+}
+
function updateMarker(root, { data, style, theme }) {
if (root.markers) root.markers.forEach((d) => d.remove());
const markers = data
@@ -442,6 +552,8 @@ export function seriesTooltip(
scale,
coordinate,
crosshairs,
+ crosshairsX,
+ crosshairsY,
render,
groupName,
emitter,
@@ -469,6 +581,7 @@ export function seriesTooltip(
const transposed = isTranspose(coordinate);
const polar = isPolar(coordinate);
const style = deepMix(_style, rest);
+
const {
innerWidth: plotWidth,
innerHeight: plotHeight,
@@ -662,22 +775,50 @@ export function seriesTooltip(
});
}
- if (crosshairs) {
- const points = filteredSeriesData.map((d) => d[1]);
+ if (crosshairs || crosshairsX || crosshairsY) {
const ruleStyle = subObject(style, 'crosshairs');
- updateRuleY(root, points, {
+
+ const ruleStyleX = {
...ruleStyle,
- plotWidth,
- plotHeight,
- mainWidth,
- mainHeight,
- insetLeft,
- insetTop,
- startX,
- startY,
- transposed,
- polar,
- });
+ ...subObject(style, 'crosshairsX'),
+ };
+ const ruleStyleY = {
+ ...ruleStyle,
+ ...subObject(style, 'crosshairsY'),
+ };
+
+ const points = filteredSeriesData.map((d) => d[1]);
+ if (crosshairsX) {
+ updateRuleX(root, points, mouse, {
+ ...ruleStyleX,
+ plotWidth,
+ plotHeight,
+ mainWidth,
+ mainHeight,
+ insetLeft,
+ insetTop,
+ startX,
+ startY,
+ transposed,
+ polar,
+ });
+ }
+
+ if (crosshairsY) {
+ updateRuleY(root, points, {
+ ...ruleStyleY,
+ plotWidth,
+ plotHeight,
+ mainWidth,
+ mainHeight,
+ insetLeft,
+ insetTop,
+ startX,
+ startY,
+ transposed,
+ polar,
+ });
+ }
}
if (marker) {
@@ -701,13 +842,19 @@ export function seriesTooltip(
const hide = (event: MouseEvent) => {
hideTooltip({ root, single, emitter, event });
- if (crosshairs) hideRuleY(root);
+ if (crosshairs) {
+ hideRuleY(root);
+ hideRuleX(root);
+ }
if (marker) hideMarker(root);
};
const destroy = () => {
destroyTooltip({ root, single });
- if (crosshairs) hideRuleY(root);
+ if (crosshairs) {
+ hideRuleY(root);
+ hideRuleX(root);
+ }
if (marker) hideMarker(root);
};
@@ -939,12 +1086,15 @@ export function Tooltip(options) {
const {
shared,
crosshairs,
+ crosshairsX,
+ crosshairsY,
series,
name,
item = () => ({}),
facet = false,
...rest
} = options;
+
return (target, viewInstances, emitter) => {
const { container, view } = target;
const { scale, markState, coordinate, theme } = view;
@@ -953,6 +1103,7 @@ export function Tooltip(options) {
const defaultShowCrosshairs = interactionKeyof(markState, 'crosshairs');
const plotArea = selectPlotArea(container);
const isSeries = maybeValue(series, defaultSeries);
+ const crosshairsSetting = maybeValue(crosshairs, defaultShowCrosshairs);
// For non-facet and series tooltip.
if (isSeries && hasSeries(markState) && !facet) {
@@ -962,7 +1113,12 @@ export function Tooltip(options) {
elements: selectG2Elements,
scale,
coordinate,
- crosshairs: maybeValue(crosshairs, defaultShowCrosshairs),
+ crosshairs: crosshairsSetting,
+ // the crosshairsX settings level: crosshairsX > crosshairs > false
+ // it means crosshairsX default is false
+ crosshairsX: maybeValue(maybeValue(crosshairsX, crosshairs), false),
+ // crosshairsY default depend on the crossharisSettings
+ crosshairsY: maybeValue(crosshairsY, crosshairsSetting),
item,
emitter,
});
@@ -992,6 +1148,10 @@ export function Tooltip(options) {
scale,
coordinate,
crosshairs: maybeValue(crosshairs, defaultShowCrosshairs),
+ // the crosshairsX settings level: crosshairsX > crosshairs > false
+ // it means crosshairsX default is false
+ crosshairsX: maybeValue(maybeValue(crosshairsX, crosshairs), false),
+ crosshairsY: maybeValue(crosshairsY, crosshairsSetting),
item,
startX,
startY,