Skip to content

Commit

Permalink
feat(D3 plugin): line chart legend symbol (#358)
Browse files Browse the repository at this point in the history
* feat(D3): line chart legend symbol

* fix inactive legend color

* fix review(1)
  • Loading branch information
kuzmadom authored Dec 1, 2023
1 parent 001a0e3 commit 334f5a2
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 35 deletions.
6 changes: 5 additions & 1 deletion src/plugins/d3/__stories__/Showcase.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ const ShowcaseStory = () => {
<BasicLine />
</Col>
<Col>
<Text variant="subheader-1">Line chart with data labels</Text>
<Text variant="subheader-1">With data labels</Text>
<LineWithDataLabels />
</Col>
<Col></Col>
</Row>
<Row space={1}>
<Text variant="header-2">Bar-x charts</Text>
Expand Down Expand Up @@ -99,6 +100,7 @@ const ShowcaseStory = () => {
<Text variant="subheader-1">Donut chart</Text>
<Donut />
</Col>
<Col></Col>
</Row>
<Row space={1}>
<Text variant="header-2">Scatter charts</Text>
Expand All @@ -108,6 +110,8 @@ const ShowcaseStory = () => {
<Text variant="subheader-1">Basic scatter</Text>
<BasicScatter />
</Col>
<Col></Col>
<Col></Col>
</Row>
<Row space={1}>
<Text variant="header-2">Combined chart</Text>
Expand Down
87 changes: 67 additions & 20 deletions src/plugins/d3/renderer/components/Legend.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {select} from 'd3';
import {BaseType, select, line as lineGenerator} from 'd3';
import type {Selection} from 'd3';

import {block} from '../../../../utils/cn';
Expand Down Expand Up @@ -94,6 +94,71 @@ const appendPaginator = (args: {
paginationLine.attr('transform', transform);
};

const legendSymbolGenerator = lineGenerator<{x: number; y: number}>()
.x((d) => d.x)
.y((d) => d.y);

function renderLegendSymbol(args: {
selection: Selection<SVGGElement, LegendItem, BaseType, unknown>;
legend: PreparedLegend;
}) {
const {selection, legend} = args;
const line = selection.data();

const getXPosition = (i: number) => {
return line.slice(0, i).reduce((acc, legendItem) => {
return (
acc +
legendItem.symbol.width +
legendItem.symbol.padding +
legendItem.textWidth +
legend.itemDistance
);
}, 0);
};

selection.each(function (d, i) {
const element = select(this);
const x = getXPosition(i);
const className = b('item-symbol', {shape: d.symbol.shape, unselected: !d.visible});
const color = d.visible ? d.color : '';

switch (d.symbol.shape) {
case 'path': {
const y = legend.lineHeight / 2;
const points = [
{x: x, y},
{x: x + d.symbol.width, y},
];

element
.append('path')
.attr('d', legendSymbolGenerator(points))
.attr('fill', 'none')
.attr('stroke-width', d.symbol.strokeWidth)
.attr('class', className)
.style('stroke', color);

break;
}
case 'rect': {
const y = (legend.lineHeight - d.symbol.height) / 2;
element
.append('rect')
.attr('x', x)
.attr('y', y)
.attr('width', d.symbol.width)
.attr('height', d.symbol.height)
.attr('rx', d.symbol.radius)
.attr('class', className)
.style('fill', color);

break;
}
}
});
}

export const Legend = (props: Props) => {
const {boundsWidth, chartSeries, legend, items, config, onItemClick} = props;
const ref = React.useRef<SVGGElement>(null);
Expand Down Expand Up @@ -139,25 +204,7 @@ export const Legend = (props: Props) => {
}, 0);
};

legendItemTemplate
.append('rect')
.attr('x', function (_d, i) {
return getXPosition(i);
})
.attr('y', (legendItem) => {
return (legend.lineHeight - legendItem.symbol.height) / 2;
})
.attr('width', (legendItem) => {
return legendItem.symbol.width;
})
.attr('height', (legendItem) => legendItem.symbol.height)
.attr('rx', (legendItem) => legendItem.symbol.radius)
.attr('class', function (d) {
return b('item-shape', {unselected: !d.visible});
})
.style('fill', function (d) {
return d.visible ? d.color : '';
});
renderLegendSymbol({selection: legendItemTemplate, legend});

legendItemTemplate
.append('text')
Expand Down
10 changes: 6 additions & 4 deletions src/plugins/d3/renderer/components/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@
user-select: none;
}

&__item-shape {
fill: var(--g-color-base-misc-medium);

&_unselected {
&__item-symbol {
&_shape_rect#{&}_unselected {
fill: var(--g-color-text-hint);
}

&_shape_path#{&}_unselected {
stroke: var(--g-color-text-hint);
}
}

&__item-text {
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/d3/renderer/hooks/useSeries/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {BaseTextStyle} from '../../../../../types';

export const DEFAULT_LEGEND_SYMBOL_SIZE = 8;

export const DEFAULT_LEGEND_SYMBOL_PADDING = 5;

export const DEFAULT_DATALABELS_PADDING = 5;

export const DEFAULT_DATALABELS_STYLE: BaseTextStyle = {
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import get from 'lodash/get';
import merge from 'lodash/merge';
import {select} from 'd3';

import type {ChartKitWidgetData} from '../../../../../types/widget-data';
import type {ChartKitWidgetData} from '../../../../../types';

import {legendDefaults} from '../../constants';
import {getHorisontalSvgTextHeight} from '../../utils';
Expand All @@ -24,6 +24,7 @@ export const getPreparedLegend = (args: {
const itemStyle = get(legend, 'itemStyle');
const computedItemStyle = merge(defaultItemStyle, itemStyle);
const lineHeight = getHorisontalSvgTextHeight({text: 'Tmp', style: computedItemStyle});

const height = enabled ? lineHeight : 0;

return {
Expand Down
38 changes: 32 additions & 6 deletions src/plugins/d3/renderer/hooks/useSeries/prepare-line-series.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,49 @@
import {ScaleOrdinal} from 'd3';
import get from 'lodash/get';

import {ChartKitWidgetSeriesOptions, LineSeries} from '../../../../../types';
import {PreparedLineSeries, PreparedLegend, PreparedSeries} from './types';
import {
ChartKitWidgetSeries,
ChartKitWidgetSeriesOptions,
LineSeries,
RectLegendSymbolOptions,
} from '../../../../../types';
import {PreparedLineSeries, PreparedLegend, PreparedSeries, PreparedLegendSymbol} from './types';

import {DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE} from './constants';
import {prepareLegendSymbol} from './utils';
import {
DEFAULT_DATALABELS_PADDING,
DEFAULT_DATALABELS_STYLE,
DEFAULT_LEGEND_SYMBOL_PADDING,
} from './constants';
import {getRandomCKId} from '../../../../../utils';

export const DEFAULT_LEGEND_SYMBOL_SIZE = 16;
export const DEFAULT_LINE_WIDTH = 1;

type PrepareLineSeriesArgs = {
colorScale: ScaleOrdinal<string, string>;
series: LineSeries[];
seriesOptions?: ChartKitWidgetSeriesOptions;
legend: PreparedLegend;
};

function prepareLineLegendSymbol(
series: ChartKitWidgetSeries,
seriesOptions?: ChartKitWidgetSeriesOptions,
): PreparedLegendSymbol {
const symbolOptions: RectLegendSymbolOptions = series.legend?.symbol || {};
const defaultLineWidth = get(seriesOptions, 'line.lineWidth', DEFAULT_LINE_WIDTH);

return {
shape: 'path',
width: symbolOptions?.width || DEFAULT_LEGEND_SYMBOL_SIZE,
padding: symbolOptions?.padding || DEFAULT_LEGEND_SYMBOL_PADDING,
strokeWidth: get(series, 'lineWidth', defaultLineWidth),
};
}

export function prepareLineSeries(args: PrepareLineSeriesArgs): PreparedSeries[] {
const {colorScale, series: seriesList, seriesOptions, legend} = args;
const defaultLineWidth = get(seriesOptions, 'line.lineWidth', 1);
const defaultLineWidth = get(seriesOptions, 'line.lineWidth', DEFAULT_LINE_WIDTH);

return seriesList.map<PreparedLineSeries>((series) => {
const id = getRandomCKId();
Expand All @@ -33,7 +59,7 @@ export function prepareLineSeries(args: PrepareLineSeriesArgs): PreparedSeries[]
visible: get(series, 'visible', true),
legend: {
enabled: get(series, 'legend.enabled', legend.enabled),
symbol: prepareLegendSymbol(series),
symbol: prepareLineLegendSymbol(series, seriesOptions),
},
data: series.data,
dataLabels: {
Expand Down
8 changes: 7 additions & 1 deletion src/plugins/d3/renderer/hooks/useSeries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@ import {
LineSeriesData,
ConnectorShape,
ConnectorCurve,
PathLegendSymbolOptions,
} from '../../../../../types';
import type {SeriesOptionsDefaults} from '../../constants';

export type RectLegendSymbol = {
shape: 'rect';
} & Required<RectLegendSymbolOptions>;

export type PreparedLegendSymbol = RectLegendSymbol;
export type PathLegendSymbol = {
shape: 'path';
strokeWidth: number;
} & Required<PathLegendSymbolOptions>;

export type PreparedLegendSymbol = RectLegendSymbol | PathLegendSymbol;

export type PreparedLegend = Required<ChartKitWidgetLegend> & {
height: number;
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/d3/renderer/hooks/useSeries/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {PreparedLegendSymbol, PreparedSeries} from './types';
import {ChartKitWidgetSeries, RectLegendSymbolOptions} from '../../../../../types';
import {DEFAULT_LEGEND_SYMBOL_SIZE} from './constants';
import {DEFAULT_LEGEND_SYMBOL_PADDING, DEFAULT_LEGEND_SYMBOL_SIZE} from './constants';

export const getActiveLegendItems = (series: PreparedSeries[]) => {
return series.reduce<string[]>((acc, s) => {
Expand All @@ -25,6 +25,6 @@ export function prepareLegendSymbol(series: ChartKitWidgetSeries): PreparedLegen
width: symbolOptions?.width || DEFAULT_LEGEND_SYMBOL_SIZE,
height: symbolHeight,
radius: symbolOptions?.radius || symbolHeight / 2,
padding: symbolOptions?.padding || 5,
padding: symbolOptions?.padding || DEFAULT_LEGEND_SYMBOL_PADDING,
};
}
9 changes: 9 additions & 0 deletions src/types/widget-data/legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,12 @@ export type RectLegendSymbolOptions = BaseLegendSymbol & {
*/
radius?: number;
};

export type PathLegendSymbolOptions = BaseLegendSymbol & {
/**
* The pixel width of the symbol for series types that use a path in the legend
*
* @default 16
* */
width?: number;
};

0 comments on commit 334f5a2

Please sign in to comment.