Skip to content

Commit

Permalink
feat(D3 plugin): add halo to pie series (#371)
Browse files Browse the repository at this point in the history
* Share code for the marker in the lines and areas series

* feat(D3 plugin): add halo to pie series

* fix review comment 1
  • Loading branch information
kuzmadom authored Dec 28, 2023
1 parent 6f84d02 commit 5a8dab7
Show file tree
Hide file tree
Showing 15 changed files with 221 additions and 184 deletions.
7 changes: 3 additions & 4 deletions src/plugins/d3/renderer/hooks/useSeries/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type {BaseTextStyle} from '../../../../../types';
import type {PointMarkerHalo} from '../../../../../types/widget-data/marker';
import type {BaseTextStyle, Halo} from '../../../../../types';

export const DEFAULT_LEGEND_SYMBOL_SIZE = 8;

Expand All @@ -13,8 +12,8 @@ export const DEFAULT_DATALABELS_STYLE: BaseTextStyle = {
fontColor: 'var(--d3-data-labels)',
};

export const DEFAULT_HALO_OPTIONS: Required<PointMarkerHalo> = {
export const DEFAULT_HALO_OPTIONS: Required<Halo> = {
enabled: true,
opacity: 0.25,
radius: 10,
size: 10,
};
15 changes: 13 additions & 2 deletions src/plugins/d3/renderer/hooks/useSeries/prepare-pie.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {PieSeries} from '../../../../../types';
import {ChartKitWidgetSeriesOptions, PieSeries} from '../../../../../types';
import {PreparedLegend, PreparedPieSeries, PreparedSeries} from './types';
import {scaleOrdinal} from 'd3';
import {DEFAULT_PALETTE} from '../../constants';
Expand All @@ -9,14 +9,16 @@ import {prepareLegendSymbol} from './utils';

type PreparePieSeriesArgs = {
series: PieSeries;
seriesOptions?: ChartKitWidgetSeriesOptions;
legend: PreparedLegend;
};

export function preparePieSeries(args: PreparePieSeriesArgs) {
const {series, legend} = args;
const {series, seriesOptions, legend} = args;
const dataNames = series.data.map((d) => d.name);
const colorScale = scaleOrdinal(dataNames, DEFAULT_PALETTE);
const stackId = getRandomCKId();
const seriesHoverState = get(seriesOptions, 'pie.states.hover');

const preparedSeries: PreparedSeries[] = series.data.map<PreparedPieSeries>((dataItem) => {
const result: PreparedPieSeries = {
Expand Down Expand Up @@ -49,6 +51,15 @@ export function preparePieSeries(args: PreparePieSeriesArgs) {
radius: series.radius || '100%',
innerRadius: series.innerRadius || 0,
stackId,
states: {
hover: {
halo: {
enabled: get(seriesHoverState, 'halo.enabled', true),
opacity: get(seriesHoverState, 'halo.opacity', 0.25),
size: get(seriesHoverState, 'halo.size', 10),
},
},
},
};

return result;
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ export function prepareSeries(args: {
switch (type) {
case 'pie': {
return series.reduce<PreparedSeries[]>((acc, singleSeries) => {
acc.push(...preparePieSeries({series: singleSeries as PieSeries, legend}));
acc.push(
...preparePieSeries({series: singleSeries as PieSeries, seriesOptions, legend}),
);
return acc;
}, []);
}
Expand Down
23 changes: 13 additions & 10 deletions src/plugins/d3/renderer/hooks/useSeries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export type LegendConfig = {
};
};

export type PreparedHaloOptions = {
enabled: boolean;
opacity: number;
size: number;
};

type BasePreparedSeries = {
color: string;
name: string;
Expand Down Expand Up @@ -123,6 +129,11 @@ export type PreparedPieSeries = {
distance: number;
connectorCurve: ConnectorCurve;
};
states: {
hover: {
halo: PreparedHaloOptions;
};
};
} & BasePreparedSeries;

export type PreparedLineSeries = {
Expand All @@ -149,11 +160,7 @@ export type PreparedLineSeries = {
radius: number;
borderWidth: number;
borderColor: string;
halo: {
enabled: boolean;
opacity: number;
radius: number;
};
halo: PreparedHaloOptions;
};
};
};
Expand Down Expand Up @@ -188,11 +195,7 @@ export type PreparedAreaSeries = {
radius: number;
borderWidth: number;
borderColor: string;
halo: {
enabled: boolean;
opacity: number;
radius: number;
};
halo: PreparedHaloOptions;
};
};
};
Expand Down
95 changes: 16 additions & 79 deletions src/plugins/d3/renderer/hooks/useShapes/area/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import React from 'react';
import type {Dispatch, Selection, BaseType} from 'd3';
import {
color,
line as lineGenerator,
area as areaGenerator,
select,
symbol,
symbolCircle,
symbolSquare,
} from 'd3';
import type {Dispatch, BaseType} from 'd3';
import {color, line as lineGenerator, area as areaGenerator, select} from 'd3';
import get from 'lodash/get';

import {block} from '../../../../../../utils/cn';
Expand All @@ -18,6 +10,14 @@ import type {TooltipDataChunkArea} from '../../../../../../types';
import type {LabelData} from '../../../types';
import {filterOverlappingLabels} from '../../../utils';
import {setActiveState} from '../utils';
import {
getMarkerHaloVisibility,
getMarkerVisibility,
renderMarker,
selectMarkerHalo,
selectMarkerSymbol,
setMarker,
} from '../marker';

const b = block('d3-area');

Expand All @@ -27,47 +27,6 @@ type Args = {
seriesOptions: PreparedSeriesOptions;
};

function setMarker<T extends BaseType>(
selection: Selection<T, MarkerData, BaseType | null, unknown>,
state: 'normal' | 'hover',
) {
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);
})
.attr('stroke-width', (d) => d.point.series.marker.states[state].borderWidth)
.attr('stroke', (d) => d.point.series.marker.states[state].borderColor);
}

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)();
}
}
}

const getMarkerVisibility = (d: MarkerData) => {
const markerStates = d.point.series.marker.states;
const enabled = (markerStates.hover.enabled && d.hovered) || markerStates.normal.enabled;
return enabled ? '' : 'hidden';
};

const getMarkerHaloVisibility = (d: MarkerData) => {
const markerStates = d.point.series.marker.states;
const enabled = markerStates.hover.halo.enabled && d.hovered;
return enabled ? '' : 'hidden';
};

export const AreaSeriesShapes = (args: Args) => {
const {dispatcher, preparedData, seriesOptions} = args;

Expand Down Expand Up @@ -141,28 +100,7 @@ export const AreaSeriesShapes = (args: Args) => {
.selectAll('marker')
.data(markers)
.join('g')
.attr('class', b('marker'))
.attr('visibility', getMarkerVisibility)
.attr('transform', (d) => {
return `translate(${d.point.x},${d.point.y})`;
});
markerSelection
.append('path')
.attr('class', b('marker-halo'))
.attr('d', (d) => {
const type = d.point.series.marker.states.normal.symbol;
const radius = d.point.series.marker.states.hover.halo.radius;
return getMarkerSymbol(type, radius);
})
.attr('fill', (d) => d.point.series.color)
.attr('opacity', (d) => d.point.series.marker.states.hover.halo.opacity)
.attr('z-index', -1)
.attr('visibility', getMarkerHaloVisibility);
markerSelection
.append('path')
.attr('class', b('marker-symbol'))
.call(setMarker, 'normal')
.attr('fill', (d) => d.point.series.color);
.call(renderMarker);

const hoverEnabled = hoverOptions?.enabled;
const inactiveEnabled = inactiveOptions?.enabled;
Expand Down Expand Up @@ -218,12 +156,11 @@ export const AreaSeriesShapes = (args: Args) => {
if (d.hovered !== hovered) {
d.hovered = hovered;
elementSelection.attr('visibility', getMarkerVisibility(d));
elementSelection
.select(`.${b('marker-halo')}`)
.attr('visibility', getMarkerHaloVisibility);
elementSelection
.select(`.${b('marker-symbol')}`)
.call(setMarker, hovered ? 'hover' : 'normal');
selectMarkerHalo(elementSelection).attr('visibility', getMarkerHaloVisibility);
selectMarkerSymbol(elementSelection).call(
setMarker,
hovered ? 'hover' : 'normal',
);
}

if (d.point.series.marker.states.normal.enabled) {
Expand Down
87 changes: 16 additions & 71 deletions src/plugins/d3/renderer/hooks/useShapes/line/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import type {Dispatch, Selection, BaseType} from 'd3';
import {color, line as lineGenerator, select, symbol, symbolCircle, symbolSquare} from 'd3';
import type {Dispatch, BaseType} from 'd3';
import {color, line as lineGenerator, select} from 'd3';
import get from 'lodash/get';

import {block} from '../../../../../../utils/cn';
Expand All @@ -10,6 +10,14 @@ import type {TooltipDataChunkLine} from '../../../../../../types';
import type {LabelData} from '../../../types';
import {filterOverlappingLabels} from '../../../utils';
import {getLineDashArray, setActiveState} from '../utils';
import {
getMarkerHaloVisibility,
getMarkerVisibility,
renderMarker,
selectMarkerHalo,
selectMarkerSymbol,
setMarker,
} from '../marker';

const b = block('d3-line');

Expand All @@ -19,47 +27,6 @@ type Args = {
seriesOptions: PreparedSeriesOptions;
};

function setMarker<T extends BaseType>(
selection: Selection<T, MarkerData, BaseType | null, unknown>,
state: 'normal' | 'hover',
) {
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);
})
.attr('stroke-width', (d) => d.point.series.marker.states[state].borderWidth)
.attr('stroke', (d) => d.point.series.marker.states[state].borderColor);
}

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)();
}
}
}

const getMarkerVisibility = (d: MarkerData) => {
const markerStates = d.point.series.marker.states;
const enabled = (markerStates.hover.enabled && d.hovered) || markerStates.normal.enabled;
return enabled ? '' : 'hidden';
};

const getMarkerHaloVisibility = (d: MarkerData) => {
const markerStates = d.point.series.marker.states;
const enabled = markerStates.hover.halo.enabled && d.hovered;
return enabled ? '' : 'hidden';
};

export const LineSeriesShapes = (args: Args) => {
const {dispatcher, preparedData, seriesOptions} = args;

Expand Down Expand Up @@ -118,28 +85,7 @@ export const LineSeriesShapes = (args: Args) => {
.selectAll('marker')
.data(markers)
.join('g')
.attr('class', b('marker'))
.attr('visibility', getMarkerVisibility)
.attr('transform', (d) => {
return `translate(${d.point.x},${d.point.y})`;
});
markerSelection
.append('path')
.attr('class', b('marker-halo'))
.attr('d', (d) => {
const type = d.point.series.marker.states.normal.symbol;
const radius = d.point.series.marker.states.hover.halo.radius;
return getMarkerSymbol(type, radius);
})
.attr('fill', (d) => d.point.series.color)
.attr('opacity', (d) => d.point.series.marker.states.hover.halo.opacity)
.attr('z-index', -1)
.attr('visibility', getMarkerHaloVisibility);
markerSelection
.append('path')
.attr('class', b('marker-symbol'))
.call(setMarker, 'normal')
.attr('fill', (d) => d.point.series.color);
.call(renderMarker);

const hoverEnabled = hoverOptions?.enabled;
const inactiveEnabled = inactiveOptions?.enabled;
Expand Down Expand Up @@ -196,12 +142,11 @@ export const LineSeriesShapes = (args: Args) => {
if (d.hovered !== hovered) {
d.hovered = hovered;
elementSelection.attr('visibility', getMarkerVisibility(d));
elementSelection
.select(`.${b('marker-halo')}`)
.attr('visibility', getMarkerHaloVisibility);
elementSelection
.select(`.${b('marker-symbol')}`)
.call(setMarker, hovered ? 'hover' : 'normal');
selectMarkerHalo(elementSelection).attr('visibility', getMarkerHaloVisibility);
selectMarkerSymbol(elementSelection).call(
setMarker,
hovered ? 'hover' : 'normal',
);
}

if (d.point.series.marker.states.normal.enabled) {
Expand Down
Loading

0 comments on commit 5a8dab7

Please sign in to comment.