Skip to content

Commit

Permalink
feat(D3 plugin): add halo to scatter hover series (#394)
Browse files Browse the repository at this point in the history
* feat(D3 plugin): add halo to scatter hover series

* fix types

* fix review(1)
  • Loading branch information
kuzmadom authored Jan 25, 2024
1 parent ada09ed commit af2ac7c
Show file tree
Hide file tree
Showing 21 changed files with 253 additions and 236 deletions.
26 changes: 1 addition & 25 deletions src/plugins/d3/examples/scatter/Basic.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 (
<React.Fragment>
<b>{title}</b>
<br />
Release date: {date}
<br />
User score: {score}
</React.Fragment>
);
},
},
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -119,21 +120,21 @@ export const TooltipTriggerArea = (args: Args) => {
const rectRef = React.useRef<SVGRectElement>(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);
}, [shapesData]);

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<XLineData>((d) => ({
x: d.x,
data: d.data,
series: sd.series,
series: d.series,
})),
);
}, [] as XLineData[]);
Expand All @@ -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;
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
10 changes: 9 additions & 1 deletion src/plugins/d3/renderer/hooks/useSeries/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {BaseTextStyle, Halo} from '../../../../../types';
import {PointMarkerOptions} from '../../../../../types/widget-data/marker';

export const DEFAULT_LEGEND_SYMBOL_SIZE = 8;

Expand All @@ -15,5 +16,12 @@ export const DEFAULT_DATALABELS_STYLE: BaseTextStyle = {
export const DEFAULT_HALO_OPTIONS: Required<Halo> = {
enabled: true,
opacity: 0.25,
size: 10,
size: 6,
};

export const DEFAULT_POINT_MARKER_OPTIONS: Omit<Required<PointMarkerOptions>, 'enabled'> = {
radius: 4,
borderColor: '',
borderWidth: 0,
symbol: 'circle',
};
9 changes: 4 additions & 5 deletions src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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<PointMarkerOptions> = Object.assign(
{},
DEFAULT_MARKER,
seriesOptions?.area?.marker,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 = {
Expand Down
73 changes: 73 additions & 0 deletions src/plugins/d3/renderer/hooks/useSeries/prepare-scatter.ts
Original file line number Diff line number Diff line change
@@ -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<PointMarkerOptions> = {
...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<string, string>;
series: ScatterSeries[];
legend: PreparedLegend;
seriesOptions?: ChartKitWidgetSeriesOptions;
}

export function prepareScatterSeries(args: PrepareScatterSeriesArgs): PreparedScatterSeries[] {
const {colorScale, series, seriesOptions, legend} = args;

return series.map<PreparedScatterSeries>((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;
}, []);
}
46 changes: 5 additions & 41 deletions src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import type {ScaleOrdinal} from 'd3';

import type {
Expand All @@ -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<string, string>;
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'];
Expand All @@ -76,12 +45,7 @@ export function prepareSeries(args: {
return prepareBarYSeries({series: series as BarYSeries[], legend, colorScale});
}
case 'scatter': {
return series.reduce<PreparedSeries[]>((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({
Expand Down
25 changes: 21 additions & 4 deletions src/plugins/d3/renderer/hooks/useSeries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export type PathLegendSymbol = {

export type SymbolLegendSymbol = {
shape: 'symbol';
symbolType: SymbolType;
symbolType: `${SymbolType}`;
} & Required<SymbolLegendSymbolOptions>;

export type PreparedLegendSymbol = RectLegendSymbol | PathLegendSymbol | SymbolLegendSymbol;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -159,7 +176,7 @@ export type PreparedLineSeries = {
marker: {
states: {
normal: {
symbol: string;
symbol: `${SymbolType}`;
enabled: boolean;
radius: number;
borderWidth: number;
Expand Down Expand Up @@ -194,7 +211,7 @@ export type PreparedAreaSeries = {
marker: {
states: {
normal: {
symbol: string;
symbol: `${SymbolType}`;
enabled: boolean;
radius: number;
borderWidth: number;
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/d3/renderer/hooks/useSeries/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const getAllLegendItems = (series: PreparedSeries[]) => {

export function prepareLegendSymbol(
series: ChartKitWidgetSeries,
symbolType?: SymbolType,
symbolType?: `${SymbolType}`,
): PreparedLegendSymbol {
const symbolOptions = series.legend?.symbol || {};

Expand Down
4 changes: 2 additions & 2 deletions src/plugins/d3/renderer/hooks/useShapes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
Loading

0 comments on commit af2ac7c

Please sign in to comment.