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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + a + + + + + + + + + + + + + + + + + + + + b + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + a + + + + + + + + + + + + + + + + + + + + b + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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,