From af2ac7c6ec3e104ac060b342a9f8ebb88e3e646c Mon Sep 17 00:00:00 2001 From: Irina Kuzmina Date: Thu, 25 Jan 2024 15:30:45 +0200 Subject: [PATCH] feat(D3 plugin): add halo to scatter hover series (#394) * feat(D3 plugin): add halo to scatter hover series * fix types * fix review(1) --- src/plugins/d3/examples/scatter/Basic.tsx | 26 +-- .../components/Tooltip/TooltipTriggerArea.tsx | 11 +- .../__tests__/prepare-line-series.test.ts | 2 +- .../d3/renderer/hooks/useSeries/constants.ts | 10 +- .../renderer/hooks/useSeries/prepare-area.ts | 9 +- ...prepare-line-series.ts => prepare-line.ts} | 6 +- .../hooks/useSeries/prepare-scatter.ts | 73 +++++++++ .../renderer/hooks/useSeries/prepareSeries.ts | 46 +----- .../d3/renderer/hooks/useSeries/types.ts | 25 ++- .../d3/renderer/hooks/useSeries/utils.ts | 2 +- .../d3/renderer/hooks/useShapes/index.tsx | 4 +- .../d3/renderer/hooks/useShapes/marker.ts | 42 ++--- .../hooks/useShapes/scatter/index.tsx | 152 +++++++----------- .../hooks/useShapes/scatter/prepare-data.ts | 29 +--- .../renderer/hooks/useShapes/scatter/types.ts | 17 ++ .../d3/renderer/hooks/useShapes/utils.ts | 3 +- src/plugins/d3/renderer/utils/symbol.ts | 2 +- src/types/widget-data/line.ts | 8 +- src/types/widget-data/marker.ts | 3 + src/types/widget-data/series.ts | 13 +- src/types/widget-data/tooltip.ts | 6 +- 21 files changed, 253 insertions(+), 236 deletions(-) rename src/plugins/d3/renderer/hooks/useSeries/{prepare-line-series.ts => prepare-line.ts} (98%) create mode 100644 src/plugins/d3/renderer/hooks/useSeries/prepare-scatter.ts create mode 100644 src/plugins/d3/renderer/hooks/useShapes/scatter/types.ts diff --git a/src/plugins/d3/examples/scatter/Basic.tsx b/src/plugins/d3/examples/scatter/Basic.tsx index 1a3b79c6..a30daefb 100644 --- a/src/plugins/d3/examples/scatter/Basic.tsx +++ b/src/plugins/d3/examples/scatter/Basic.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import {dateTime} from '@gravity-ui/date-utils'; import {ChartKit} from '../../../../components/ChartKit'; -import type {ChartKitWidgetData, ScatterSeries, ScatterSeriesData} from '../../../../types'; +import type {ChartKitWidgetData, ScatterSeries} from '../../../../types'; import {ExampleWrapper} from '../ExampleWrapper'; import nintendoGames from '../nintendoGames'; @@ -47,29 +46,6 @@ export const Basic = () => { text: 'Release dates', }, }, - tooltip: { - renderer: (d) => { - const point = d.hovered[0]?.data as ScatterSeriesData; - - if (!point) { - return null; - } - - const title = point.custom.title; - const score = point.custom.user_score; - const date = dateTime({input: point.custom.date}).format('DD MMM YYYY'); - - return ( - - {title} -
- Release date: {date} -
- User score: {score} -
- ); - }, - }, }; return ( diff --git a/src/plugins/d3/renderer/components/Tooltip/TooltipTriggerArea.tsx b/src/plugins/d3/renderer/components/Tooltip/TooltipTriggerArea.tsx index 2366de42..8dfdb3bc 100644 --- a/src/plugins/d3/renderer/components/Tooltip/TooltipTriggerArea.tsx +++ b/src/plugins/d3/renderer/components/Tooltip/TooltipTriggerArea.tsx @@ -9,6 +9,7 @@ import type {NodeWithD3Data} from '../../utils'; import {PreparedLineData} from '../../hooks/useShapes/line/types'; import {BarYSeriesData, LineSeriesData} from '../../../../../types'; import {PreparedBarYData} from '../../hooks/useShapes/bar-y/types'; +import get from 'lodash/get'; const THROTTLE_DELAY = 50; @@ -119,7 +120,7 @@ export const TooltipTriggerArea = (args: Args) => { const rectRef = React.useRef(null); const xBarData = React.useMemo(() => { const result = shapesData - .filter((sd) => sd.series.type === 'bar-x') + .filter((sd) => get(sd, 'series.type') === 'bar-x') .map((sd) => ({x: (sd as PreparedBarXData).x, data: sd})); return sort(result, (item) => item.x); @@ -127,13 +128,13 @@ export const TooltipTriggerArea = (args: Args) => { const xLineData = React.useMemo(() => { const result = shapesData - .filter((sd) => ['line', 'area'].includes(sd.series.type)) + .filter((sd) => ['line', 'area'].includes((sd as PreparedLineData).series.type)) .reduce((acc, sd) => { return acc.concat( (sd as PreparedLineData).points.map((d) => ({ x: d.x, data: d.data, - series: sd.series, + series: d.series, })), ); }, [] as XLineData[]); @@ -142,7 +143,7 @@ export const TooltipTriggerArea = (args: Args) => { }, [shapesData]); const barYData = React.useMemo(() => { - const barYShapeData = shapesData.filter((sd) => sd.series.type === 'bar-y'); + const barYShapeData = shapesData.filter((sd) => get(sd, 'series.type') === 'bar-y'); const result = Array.from(group(barYShapeData, (sd) => (sd as PreparedBarYData).y)).map( ([y, shapes]) => { const yValue = y + (shapes[0] as PreparedBarYData).height / 2; @@ -156,7 +157,7 @@ export const TooltipTriggerArea = (args: Args) => { return { x: preparedData.x + preparedData.width, data: preparedData.data, - series: shape.series, + series: preparedData.series, }; }), (item) => item.x, diff --git a/src/plugins/d3/renderer/hooks/useSeries/__tests__/prepare-line-series.test.ts b/src/plugins/d3/renderer/hooks/useSeries/__tests__/prepare-line-series.test.ts index 7d3e7009..633c3bb0 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/__tests__/prepare-line-series.test.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/__tests__/prepare-line-series.test.ts @@ -1,4 +1,4 @@ -import {DEFAULT_MARKER, prepareLineSeries} from '../prepare-line-series'; +import {DEFAULT_MARKER, prepareLineSeries} from '../prepare-line'; import {scaleOrdinal} from 'd3'; import type {LineSeries} from '../../../../../../types'; import type {PreparedLegend} from '../types'; diff --git a/src/plugins/d3/renderer/hooks/useSeries/constants.ts b/src/plugins/d3/renderer/hooks/useSeries/constants.ts index 792f566f..8996bb37 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/constants.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/constants.ts @@ -1,4 +1,5 @@ import type {BaseTextStyle, Halo} from '../../../../../types'; +import {PointMarkerOptions} from '../../../../../types/widget-data/marker'; export const DEFAULT_LEGEND_SYMBOL_SIZE = 8; @@ -15,5 +16,12 @@ export const DEFAULT_DATALABELS_STYLE: BaseTextStyle = { export const DEFAULT_HALO_OPTIONS: Required = { enabled: true, opacity: 0.25, - size: 10, + size: 6, +}; + +export const DEFAULT_POINT_MARKER_OPTIONS: Omit, 'enabled'> = { + radius: 4, + borderColor: '', + borderWidth: 0, + symbol: 'circle', }; diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts index 51bb660b..6db51cf9 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts @@ -9,18 +9,17 @@ import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE, DEFAULT_HALO_OPTIONS, + DEFAULT_POINT_MARKER_OPTIONS, } from './constants'; import {getRandomCKId} from '../../../../../utils'; import {getSeriesStackId, prepareLegendSymbol} from './utils'; +import {PointMarkerOptions} from '../../../../../types/widget-data/marker'; export const DEFAULT_LINE_WIDTH = 1; export const DEFAULT_MARKER = { + ...DEFAULT_POINT_MARKER_OPTIONS, enabled: false, - symbol: 'circle', - radius: 4, - borderWidth: 0, - borderColor: '', }; type PrepareAreaSeriesArgs = { @@ -32,7 +31,7 @@ type PrepareAreaSeriesArgs = { function prepareMarker(series: AreaSeries, seriesOptions?: ChartKitWidgetSeriesOptions) { const seriesHoverState = get(seriesOptions, 'area.states.hover'); - const markerNormalState = Object.assign( + const markerNormalState: Required = Object.assign( {}, DEFAULT_MARKER, seriesOptions?.area?.marker, diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-line-series.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts similarity index 98% rename from src/plugins/d3/renderer/hooks/useSeries/prepare-line-series.ts rename to src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts index 6211cbf2..34cc1fc7 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-line-series.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts @@ -17,6 +17,7 @@ import { DEFAULT_DATALABELS_STYLE, DEFAULT_HALO_OPTIONS, DEFAULT_LEGEND_SYMBOL_PADDING, + DEFAULT_POINT_MARKER_OPTIONS, } from './constants'; import {getRandomCKId} from '../../../../../utils'; @@ -25,11 +26,8 @@ export const DEFAULT_LINE_WIDTH = 1; export const DEFAULT_DASH_STYLE = DashStyle.Solid; export const DEFAULT_MARKER = { + ...DEFAULT_POINT_MARKER_OPTIONS, enabled: false, - symbol: 'circle', - radius: 4, - borderWidth: 0, - borderColor: '', }; type PrepareLineSeriesArgs = { diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-scatter.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-scatter.ts new file mode 100644 index 00000000..ee809a85 --- /dev/null +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-scatter.ts @@ -0,0 +1,73 @@ +import {ScaleOrdinal} from 'd3'; +import get from 'lodash/get'; +import merge from 'lodash/merge'; +import type {PreparedLegend, PreparedScatterSeries} from './types'; +import type {ChartKitWidgetSeriesOptions, ScatterSeries} from '../../../../../types'; +import {getSymbolType} from '../../utils'; + +import {prepareLegendSymbol} from './utils'; +import {DEFAULT_HALO_OPTIONS, DEFAULT_POINT_MARKER_OPTIONS} from './constants'; + +import {PointMarkerOptions} from '../../../../../types/widget-data/marker'; +import {getRandomCKId} from '../../../../../utils'; + +function prepareMarker( + series: ScatterSeries, + seriesOptions: ChartKitWidgetSeriesOptions | undefined, + index: number, +) { + const seriesHoverState = get(seriesOptions, 'scatter.states.hover'); + const markerNormalState: Required = { + ...DEFAULT_POINT_MARKER_OPTIONS, + enabled: true, + symbol: (series as ScatterSeries).symbolType || getSymbolType(index), + }; + + const hoveredMarkerDefaultOptions = { + enabled: true, + radius: markerNormalState.radius, + borderWidth: 1, + borderColor: '#ffffff', + halo: DEFAULT_HALO_OPTIONS, + }; + + return { + states: { + normal: markerNormalState, + hover: merge(hoveredMarkerDefaultOptions, seriesHoverState?.marker), + }, + }; +} + +interface PrepareScatterSeriesArgs { + colorScale: ScaleOrdinal; + series: ScatterSeries[]; + legend: PreparedLegend; + seriesOptions?: ChartKitWidgetSeriesOptions; +} + +export function prepareScatterSeries(args: PrepareScatterSeriesArgs): PreparedScatterSeries[] { + const {colorScale, series, seriesOptions, legend} = args; + + return series.map((s, index) => { + const id = getRandomCKId(); + const name = 'name' in s && s.name ? s.name : ''; + const symbolType = (s as ScatterSeries).symbolType || getSymbolType(index); + + const prepared: PreparedScatterSeries = { + id, + type: s.type, + name, + color: get(s, 'color', colorScale(name)), + visible: get(s, 'visible', true), + legend: { + enabled: get(s, 'legend.enabled', legend.enabled), + symbol: prepareLegendSymbol(s, symbolType), + }, + data: s.data, + marker: prepareMarker(s, seriesOptions, index), + }; + + return prepared; + }, []); +} diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts b/src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts index 781b613f..87b26ce4 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts @@ -1,5 +1,3 @@ -import cloneDeep from 'lodash/cloneDeep'; -import get from 'lodash/get'; import type {ScaleOrdinal} from 'd3'; import type { @@ -10,46 +8,17 @@ import type { ChartKitWidgetSeriesOptions, LineSeries, PieSeries, + ScatterSeries, } from '../../../../../types'; -import {SymbolType} from '../../../../../constants'; -import {getSymbolType} from '../../utils'; -import {ScatterSeries} from '../../../../../types/widget-data'; - -import type {PreparedLegend, PreparedSeries, PreparedScatterSeries} from './types'; -import {prepareLineSeries} from './prepare-line-series'; +import type {PreparedLegend, PreparedSeries} from './types'; +import {prepareLineSeries} from './prepare-line'; import {prepareBarXSeries} from './prepare-bar-x'; import {prepareBarYSeries} from './prepare-bar-y'; -import {prepareLegendSymbol} from './utils'; import {ChartKitError} from '../../../../../libs'; import {preparePieSeries} from './prepare-pie'; import {prepareArea} from './prepare-area'; - -type PrepareAxisRelatedSeriesArgs = { - colorScale: ScaleOrdinal; - series: ChartKitWidgetSeries; - legend: PreparedLegend; - index: number; -}; - -function prepareAxisRelatedSeries(args: PrepareAxisRelatedSeriesArgs): PreparedScatterSeries[] { - const {colorScale, series, legend, index} = args; - const preparedSeries = cloneDeep(series) as PreparedScatterSeries; - const name = 'name' in series && series.name ? series.name : ''; - - const symbolType = ((series as ScatterSeries).symbolType || getSymbolType(index)) as SymbolType; - - preparedSeries.symbolType = symbolType; - preparedSeries.color = 'color' in series && series.color ? series.color : colorScale(name); - preparedSeries.name = name; - preparedSeries.visible = get(preparedSeries, 'visible', true); - preparedSeries.legend = { - enabled: get(preparedSeries, 'legend.enabled', legend.enabled), - symbol: prepareLegendSymbol(series, symbolType), - }; - - return [preparedSeries]; -} +import {prepareScatterSeries} from './prepare-scatter'; export function prepareSeries(args: { type: ChartKitWidgetSeries['type']; @@ -76,12 +45,7 @@ export function prepareSeries(args: { return prepareBarYSeries({series: series as BarYSeries[], legend, colorScale}); } case 'scatter': { - return series.reduce((acc, singleSeries, index) => { - acc.push( - ...prepareAxisRelatedSeries({series: singleSeries, legend, colorScale, index}), - ); - return acc; - }, []); + return prepareScatterSeries({series: series as ScatterSeries[], legend, colorScale}); } case 'line': { return prepareLineSeries({ diff --git a/src/plugins/d3/renderer/hooks/useSeries/types.ts b/src/plugins/d3/renderer/hooks/useSeries/types.ts index 94a35d8e..3500f467 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/types.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/types.ts @@ -33,7 +33,7 @@ export type PathLegendSymbol = { export type SymbolLegendSymbol = { shape: 'symbol'; - symbolType: SymbolType; + symbolType: `${SymbolType}`; } & Required; export type PreparedLegendSymbol = RectLegendSymbol | PathLegendSymbol | SymbolLegendSymbol; @@ -85,7 +85,24 @@ type BasePreparedSeries = { export type PreparedScatterSeries = { type: ScatterSeries['type']; data: ScatterSeriesData[]; - symbolType: SymbolType; + marker: { + states: { + normal: { + symbol: `${SymbolType}`; + enabled: boolean; + radius: number; + borderWidth: number; + borderColor: string; + }; + hover: { + enabled: boolean; + radius: number; + borderWidth: number; + borderColor: string; + halo: PreparedHaloOptions; + }; + }; + }; } & BasePreparedSeries; export type PreparedBarXSeries = { @@ -159,7 +176,7 @@ export type PreparedLineSeries = { marker: { states: { normal: { - symbol: string; + symbol: `${SymbolType}`; enabled: boolean; radius: number; borderWidth: number; @@ -194,7 +211,7 @@ export type PreparedAreaSeries = { marker: { states: { normal: { - symbol: string; + symbol: `${SymbolType}`; enabled: boolean; radius: number; borderWidth: number; diff --git a/src/plugins/d3/renderer/hooks/useSeries/utils.ts b/src/plugins/d3/renderer/hooks/useSeries/utils.ts index d962176b..7360ff0f 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/utils.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/utils.ts @@ -21,7 +21,7 @@ export const getAllLegendItems = (series: PreparedSeries[]) => { export function prepareLegendSymbol( series: ChartKitWidgetSeries, - symbolType?: SymbolType, + symbolType?: `${SymbolType}`, ): PreparedLegendSymbol { const symbolOptions = series.legend?.symbol || {}; diff --git a/src/plugins/d3/renderer/hooks/useShapes/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/index.tsx index 77f2914c..ee9204b0 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/index.tsx @@ -17,7 +17,7 @@ import type { import {BarXSeriesShapes, prepareBarXData} from './bar-x'; import type {PreparedBarXData} from './bar-x'; import {ScatterSeriesShape, prepareScatterData} from './scatter'; -import type {PreparedScatterData} from './scatter'; +import type {PreparedScatterData} from './scatter/types'; import {PieSeriesShapes} from './pie'; import {preparePieData} from './pie/prepare-data'; import type {PreparedPieData} from './pie/types'; @@ -27,7 +27,7 @@ import type {PreparedLineData} from './line/types'; import {BarYSeriesShapes, prepareBarYData} from './bar-y'; import type {PreparedBarYData} from './bar-y/types'; export type {PreparedBarXData} from './bar-x'; -export type {PreparedScatterData} from './scatter'; +export type {PreparedScatterData} from './scatter/types'; import {AreaSeriesShapes} from './area'; import {prepareAreaData} from './area/prepare-data'; import type {PreparedAreaData} from './area/types'; diff --git a/src/plugins/d3/renderer/hooks/useShapes/marker.ts b/src/plugins/d3/renderer/hooks/useShapes/marker.ts index 15c6ae8f..b09c06cc 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/marker.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/marker.ts @@ -1,13 +1,17 @@ -import {BaseType, Selection, symbol, symbolCircle, symbolSquare} from 'd3'; +import {BaseType, Selection, symbol} from 'd3'; import {MarkerData as LineMarkerData} from './line/types'; import {MarkerData as AreaMarkerData} from './area/types'; +import {MarkerData as ScatterMarkerData} from './scatter/types'; import {block} from '../../../../../utils/cn'; +import {SymbolType} from '../../../../../constants'; +import {getSymbol} from '../../utils'; +import get from 'lodash/get'; const b = block('d3-marker'); const haloClassName = b('halo'); const symbolClassName = b('symbol'); -type MarkerData = LineMarkerData | AreaMarkerData; +type MarkerData = LineMarkerData | AreaMarkerData | ScatterMarkerData; export function renderMarker( selection: Selection, @@ -22,9 +26,11 @@ export function renderMarker( .append('path') .attr('class', haloClassName) .attr('d', (d) => { - const type = d.point.series.marker.states.normal.symbol; - const radius = d.point.series.marker.states.hover.halo.size; - return getMarkerSymbol(type, radius); + const series = d.point.series; + const type = series.marker.states.normal.symbol; + const radius = get(d.point.data, 'radius', series.marker.states.hover.radius); + const haloSize = series.marker.states.hover.halo.size; + return getMarkerSymbol(type, radius + haloSize); }) .attr('fill', (d) => d.point.series.color) .attr('opacity', (d) => d.point.series.marker.states.hover.halo.opacity) @@ -57,27 +63,21 @@ export function setMarker( ) { selection .attr('d', (d) => { - const radius = - d.point.series.marker.states[state].radius + - d.point.series.marker.states[state].borderWidth; - return getMarkerSymbol(d.point.series.marker.states.normal.symbol, radius); + const series = d.point.series; + const type = series.marker.states.normal.symbol; + const radius = get(d.point.data, 'radius', series.marker.states[state].radius); + const size = radius + series.marker.states[state].borderWidth; + return getMarkerSymbol(type, size); }) .attr('stroke-width', (d) => d.point.series.marker.states[state].borderWidth) .attr('stroke', (d) => d.point.series.marker.states[state].borderColor); } -export function getMarkerSymbol(type: string, radius: number) { - switch (type) { - case 'square': { - const size = Math.pow(radius, 2) * Math.PI; - return symbol(symbolSquare, size)(); - } - case 'circle': - default: { - const size = Math.pow(radius, 2) * Math.PI; - return symbol(symbolCircle, size)(); - } - } +export function getMarkerSymbol(type: `${SymbolType}` = SymbolType.Circle, radius: number) { + const symbolFn = getSymbol(type); + const size = Math.pow(radius, 2) * Math.PI; + + return symbol(symbolFn, size)(); } export function selectMarkerHalo(parentSelection: Selection) { diff --git a/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx index 9ff7b563..6f03ad93 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx @@ -1,19 +1,21 @@ import React from 'react'; import get from 'lodash/get'; -import {symbol, color, pointer, select} from 'd3'; -import type {BaseType, Dispatch, Selection} from 'd3'; +import {pointer, select} from 'd3'; +import type {BaseType, Dispatch} from 'd3'; import {block} from '../../../../../../utils/cn'; - -import {extractD3DataFromNode, isNodeContainsD3Data, getSymbol} from '../../../utils'; -import type {NodeWithD3Data} from '../../../utils'; +import {TooltipDataChunkScatter} from '../../../../../../types'; import {PreparedSeriesOptions} from '../../useSeries/types'; -import type {PreparedScatterData} from './prepare-data'; -import {shapeKey} from '../utils'; -import {SymbolType} from '../../../../../../constants'; - +import {setActiveState, shapeKey} from '../utils'; +import type {PreparedScatterData, MarkerData} from './types'; +import { + getMarkerHaloVisibility, + renderMarker, + selectMarkerHalo, + selectMarkerSymbol, + setMarker, +} from '../marker'; export {prepareScatterData} from './prepare-data'; -export type {PreparedScatterData} from './prepare-data'; type ScatterSeriesShapeProps = { dispatcher: Dispatch; @@ -24,17 +26,6 @@ type ScatterSeriesShapeProps = { const b = block('d3-scatter'); -const EMPTY_SELECTION = null as unknown as Selection< - BaseType, - PreparedScatterData, - SVGGElement, - unknown ->; - -const isNodeContainsScatterData = (node?: Element): node is NodeWithD3Data => { - return isNodeContainsD3Data(node); -}; - export function ScatterSeriesShape(props: ScatterSeriesShapeProps) { const {dispatcher, preparedData, seriesOptions, svgContainer} = props; const ref = React.useRef(null); @@ -48,38 +39,33 @@ export function ScatterSeriesShape(props: ScatterSeriesShapeProps) { const hoverOptions = get(seriesOptions, 'scatter.states.hover'); const inactiveOptions = get(seriesOptions, 'scatter.states.inactive'); + svgElement.selectAll('*').remove(); + const selection = svgElement .selectAll('path') .data(preparedData, shapeKey) - .join( - (enter) => enter.append('path').attr('class', b('point')), - (update) => update, - (exit) => exit.remove(), - ) - .attr('d', (d) => { - const symbolType = d.series.symbolType || SymbolType.Circle; - const scatterSymbol = getSymbol(symbolType); - - // D3 takes size as square pixels, so we need to make square pixels size by multiplying - // https://d3js.org/d3-shape/symbol#symbol - return symbol(scatterSymbol, d.size * d.size)(); - }) - .attr('transform', (d) => { - return 'translate(' + d.cx + ',' + d.cy + ')'; - }) - .attr('fill', (d) => d.data.color || d.series.color || ''); + .join('g') + .call(renderMarker) + .attr('fill', (d) => d.point.data.color || d.point.series.color || ''); svgElement .on('mousemove', (e) => { - const point = e.target; + const datum = select(e.target).datum(); - if (!isNodeContainsScatterData(point)) { + if (!datum) { return; } const [pointerX, pointerY] = pointer(e, svgContainer); - const segmentData = extractD3DataFromNode(point); - dispatcher.call('hover-shape', {}, [segmentData], [pointerX, pointerY]); + const data: TooltipDataChunkScatter = { + series: { + id: datum.point.series.id, + type: 'scatter', + name: datum.point.series.name, + }, + data: datum.point.data, + }; + dispatcher.call('hover-shape', {}, [data], [pointerX, pointerY]); }) .on('mouseleave', () => { dispatcher.call('hover-shape', {}, undefined); @@ -88,58 +74,44 @@ export function ScatterSeriesShape(props: ScatterSeriesShapeProps) { const hoverEnabled = hoverOptions?.enabled; const inactiveEnabled = inactiveOptions?.enabled; - dispatcher.on('hover-shape.scatter', (data?: PreparedScatterData[]) => { - const selectedPoint: PreparedScatterData | undefined = data?.[0]; - - const updates: PreparedScatterData[] = []; - preparedData.forEach((p) => { - const hovered = Boolean( - hoverEnabled && - selectedPoint && - p.cx === selectedPoint.cx && - p.cy === selectedPoint.cy, - ); - if (p.hovered !== hovered) { - p.hovered = hovered; - updates.push(p); + dispatcher.on('hover-shape.scatter', (data?: TooltipDataChunkScatter[]) => { + const selected = data?.find((d) => d.series.type === 'scatter'); + const selectedDataItem = selected?.data; + const selectedSeriesId = selected?.series?.id; + + selection.datum((d, index, list) => { + const elementSelection = select(list[index]); + + const hovered = Boolean(hoverEnabled && d.point.data === selectedDataItem); + if (d.hovered !== hovered) { + d.hovered = hovered; + elementSelection.attr('z-index', hovered ? 999 : null); + selectMarkerHalo(elementSelection).attr('visibility', getMarkerHaloVisibility); + selectMarkerSymbol(elementSelection).call( + setMarker, + hovered ? 'hover' : 'normal', + ); } - const active = Boolean( - !inactiveEnabled || !selectedPoint || selectedPoint.series.id === p.series.id, - ); - if (p.active !== active) { - p.active = active; - updates.push(p); + if (hovered) { + elementSelection.raise(); } - }); - selection.data(updates, shapeKey).join( - () => EMPTY_SELECTION, - (update) => { - update - .attr('fill', (d) => { - const initialColor = d.data.color || d.series.color || ''; - if (d.hovered) { - return ( - color(initialColor) - ?.brighter(hoverOptions?.brightness) - .toString() || initialColor - ); - } - return initialColor; - }) - .attr('opacity', function (d) { - if (!d.active) { - return inactiveOptions?.opacity || null; - } - - return null; - }); - - return update; - }, - (exit) => exit, - ); + if (d.point.series.marker.states.normal.enabled) { + const isActive = Boolean( + !inactiveEnabled || + !selectedSeriesId || + selectedSeriesId === d.point.series.id, + ); + setActiveState({ + element: list[index], + state: inactiveOptions, + active: isActive, + datum: d, + }); + } + return d; + }); }); return () => { diff --git a/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts index cf12cabf..c9a37cbd 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts @@ -1,21 +1,10 @@ -import type {TooltipDataChunkScatter, ScatterSeriesData} from '../../../../../../types'; +import type {ScatterSeriesData} from '../../../../../../types'; import type {ChartScale} from '../../useAxisScales'; import type {PreparedAxis} from '../../useChartOptions/types'; import {PreparedScatterSeries} from '../../useSeries/types'; import {getXValue, getYValue} from '../utils'; - -const DEFAULT_SCATTER_POINT_SIZE = 7; - -export type PreparedScatterData = Omit & { - cx: number; - cy: number; - series: PreparedScatterSeries; - hovered: boolean; - active: boolean; - id: number; - size: number; -}; +import {PreparedScatterData} from './types'; const getFilteredLinearScatterData = (data: ScatterSeriesData[]) => { return data.filter((d) => typeof d.x === 'number' && typeof d.y === 'number'); @@ -37,17 +26,15 @@ export const prepareScatterData = (args: { : getFilteredLinearScatterData(s.data); filteredData.forEach((d) => { - const size = d.radius ? d.radius * 2 : DEFAULT_SCATTER_POINT_SIZE; - acc.push({ - data: d, - series: s, - cx: getXValue({point: d, xAxis, xScale}), - cy: getYValue({point: d, yAxis, yScale}), + point: { + data: d, + series: s, + x: getXValue({point: d, xAxis, xScale}), + y: getYValue({point: d, yAxis, yScale}), + }, hovered: false, active: true, - id: acc.length - 1, - size, }); }); diff --git a/src/plugins/d3/renderer/hooks/useShapes/scatter/types.ts b/src/plugins/d3/renderer/hooks/useShapes/scatter/types.ts new file mode 100644 index 00000000..18cf7798 --- /dev/null +++ b/src/plugins/d3/renderer/hooks/useShapes/scatter/types.ts @@ -0,0 +1,17 @@ +import {ScatterSeriesData} from '../../../../../../types'; +import {PreparedScatterSeries} from '../../useSeries/types'; + +type PointData = { + x: number; + y: number; + data: ScatterSeriesData; + series: PreparedScatterSeries; +}; + +export type MarkerData = { + point: PointData; + active: boolean; + hovered: boolean; +}; + +export type PreparedScatterData = MarkerData; diff --git a/src/plugins/d3/renderer/hooks/useShapes/utils.ts b/src/plugins/d3/renderer/hooks/useShapes/utils.ts index 9bdb4158..b4dd17e0 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/utils.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/utils.ts @@ -8,7 +8,6 @@ import type {ChartScale} from '../useAxisScales'; import type {PreparedAxis} from '../useChartOptions/types'; import type {PreparedLineData} from './line/types'; -import type {PreparedScatterData} from './scatter'; import {DashStyle} from '../../../../../constants'; export function getXValue(args: { @@ -47,7 +46,7 @@ export function getYValue(args: { return yLinearScale(point.y as number); } -export const shapeKey = (d: unknown) => (d as PreparedLineData | PreparedScatterData).id || -1; +export const shapeKey = (d: unknown) => (d as PreparedLineData).id || -1; export function setActiveState(args: { element: BaseType; diff --git a/src/plugins/d3/renderer/utils/symbol.ts b/src/plugins/d3/renderer/utils/symbol.ts index 12df24ca..bb7d1828 100644 --- a/src/plugins/d3/renderer/utils/symbol.ts +++ b/src/plugins/d3/renderer/utils/symbol.ts @@ -23,7 +23,7 @@ const triangleDown = { }, }; -export const getSymbol = (symbolType: SymbolType) => { +export const getSymbol = (symbolType: `${SymbolType}`) => { switch (symbolType) { case SymbolType.Diamond: return symbolDiamond2; diff --git a/src/types/widget-data/line.ts b/src/types/widget-data/line.ts index 33d521b4..8c2af3fc 100644 --- a/src/types/widget-data/line.ts +++ b/src/types/widget-data/line.ts @@ -22,12 +22,6 @@ export type LineSeriesData = BaseSeriesData & { label?: string | number; }; -export type LineMarkerSymbol = 'circle' | 'square'; - -export type LineMarkerOptions = PointMarkerOptions & { - symbol?: LineMarkerSymbol; -}; - export type LineSeries = BaseSeries & { type: typeof SeriesType.Line; data: LineSeriesData[]; @@ -45,7 +39,7 @@ export type LineSeries = BaseSeries & { symbol?: RectLegendSymbolOptions; }; /** Options for the point markers of line series */ - marker?: LineMarkerOptions; + marker?: PointMarkerOptions; /** Option for line stroke style */ dashStyle?: `${DashStyle}`; /** Option for line cap style */ diff --git a/src/types/widget-data/marker.ts b/src/types/widget-data/marker.ts index c0535db3..1e113470 100644 --- a/src/types/widget-data/marker.ts +++ b/src/types/widget-data/marker.ts @@ -1,3 +1,5 @@ +import {SymbolType} from '../../constants'; + export type PointMarkerOptions = { /** Enable or disable the point marker */ enabled?: boolean; @@ -7,4 +9,5 @@ export type PointMarkerOptions = { borderColor?: string; /** The width of the point marker's border */ borderWidth?: number; + symbol?: `${SymbolType}`; }; diff --git a/src/types/widget-data/series.ts b/src/types/widget-data/series.ts index a8e99ede..916fdf33 100644 --- a/src/types/widget-data/series.ts +++ b/src/types/widget-data/series.ts @@ -2,7 +2,7 @@ import React from 'react'; import type {PieSeries, PieSeriesData} from './pie'; import type {ScatterSeries, ScatterSeriesData} from './scatter'; import type {BarXSeries, BarXSeriesData} from './bar-x'; -import type {LineSeries, LineSeriesData, LineMarkerOptions} from './line'; +import type {LineSeries, LineSeriesData} from './line'; import type {BarYSeries, BarYSeriesData} from './bar-y'; import type {PointMarkerOptions} from './marker'; import type {AreaSeries, AreaSeriesData} from './area'; @@ -157,7 +157,12 @@ export type ChartKitWidgetSeriesOptions = { scatter?: { /** Options for the series states that provide additional styling information to the series. */ states?: { - hover?: BasicHoverState; + hover?: BasicHoverState & { + marker?: PointMarkerOptions & { + /** Options for the halo appearing around the hovered point */ + halo?: Halo; + }; + }; inactive?: BasicInactiveState; }; }; @@ -178,7 +183,7 @@ export type ChartKitWidgetSeriesOptions = { inactive?: BasicInactiveState; }; /** Options for the point markers of line series */ - marker?: LineMarkerOptions; + marker?: PointMarkerOptions; /** Options for line style * @@ -209,6 +214,6 @@ export type ChartKitWidgetSeriesOptions = { inactive?: BasicInactiveState; }; /** Options for the point markers of line series */ - marker?: LineMarkerOptions; + marker?: PointMarkerOptions; }; }; diff --git a/src/types/widget-data/tooltip.ts b/src/types/widget-data/tooltip.ts index 152a90c8..cf07d92d 100644 --- a/src/types/widget-data/tooltip.ts +++ b/src/types/widget-data/tooltip.ts @@ -26,7 +26,11 @@ export type TooltipDataChunkPie = { export type TooltipDataChunkScatter = { data: ScatterSeriesData; - series: ScatterSeries; + series: { + type: ScatterSeries['type']; + id: string; + name: string; + }; }; export type TooltipDataChunkLine = {