From eccd79a2756a82b9e1728946ba677bc4152159d6 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Fri, 1 Dec 2023 17:54:03 +0300 Subject: [PATCH 1/3] feat(D3): line chart legend symbol --- .../d3/__stories__/Showcase.stories.tsx | 6 +- src/plugins/d3/renderer/components/Legend.tsx | 85 ++++++++++++++----- .../d3/renderer/hooks/useSeries/constants.ts | 2 + .../hooks/useSeries/prepare-legend.ts | 3 +- .../hooks/useSeries/prepare-line-series.ts | 38 +++++++-- .../d3/renderer/hooks/useSeries/types.ts | 8 +- .../d3/renderer/hooks/useSeries/utils.ts | 4 +- src/types/widget-data/legend.ts | 9 ++ 8 files changed, 124 insertions(+), 31 deletions(-) diff --git a/src/plugins/d3/__stories__/Showcase.stories.tsx b/src/plugins/d3/__stories__/Showcase.stories.tsx index 535725e3..658e4bcd 100644 --- a/src/plugins/d3/__stories__/Showcase.stories.tsx +++ b/src/plugins/d3/__stories__/Showcase.stories.tsx @@ -41,9 +41,10 @@ const ShowcaseStory = () => { - Line chart with data labels + With data labels + Bar-x charts @@ -99,6 +100,7 @@ const ShowcaseStory = () => { Donut chart + Scatter charts @@ -108,6 +110,8 @@ const ShowcaseStory = () => { Basic scatter + + Combined chart diff --git a/src/plugins/d3/renderer/components/Legend.tsx b/src/plugins/d3/renderer/components/Legend.tsx index 60274ae5..0397306c 100644 --- a/src/plugins/d3/renderer/components/Legend.tsx +++ b/src/plugins/d3/renderer/components/Legend.tsx @@ -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'; @@ -94,6 +94,69 @@ 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; + 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); + + 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', d.color) + .attr('stroke-width', d.symbol.strokeWidth) + .attr('class', b('item-shape', {unselected: !d.visible})); + + 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', b('item-shape', {unselected: !d.visible})) + .style('fill', d.color); + + break; + } + } + }); +} + export const Legend = (props: Props) => { const {boundsWidth, chartSeries, legend, items, config, onItemClick} = props; const ref = React.useRef(null); @@ -139,25 +202,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') diff --git a/src/plugins/d3/renderer/hooks/useSeries/constants.ts b/src/plugins/d3/renderer/hooks/useSeries/constants.ts index 9a2e271e..ec15021e 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/constants.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/constants.ts @@ -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 = { diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts index 1776e436..a88a237c 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts @@ -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'; @@ -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 { diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-line-series.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-line-series.ts index 86d8db24..37ab442b 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-line-series.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-line-series.ts @@ -1,13 +1,24 @@ 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; series: LineSeries[]; @@ -15,9 +26,24 @@ type PrepareLineSeriesArgs = { 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((series) => { const id = getRandomCKId(); @@ -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: { diff --git a/src/plugins/d3/renderer/hooks/useSeries/types.ts b/src/plugins/d3/renderer/hooks/useSeries/types.ts index 9540d9ff..0548778f 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/types.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/types.ts @@ -14,6 +14,7 @@ import { LineSeriesData, ConnectorShape, ConnectorCurve, + PathLegendSymbolOptions, } from '../../../../../types'; import type {SeriesOptionsDefaults} from '../../constants'; @@ -21,7 +22,12 @@ export type RectLegendSymbol = { shape: 'rect'; } & Required; -export type PreparedLegendSymbol = RectLegendSymbol; +export type PathLegendSymbol = { + shape: 'path'; + strokeWidth: number; +} & Required; + +export type PreparedLegendSymbol = RectLegendSymbol | PathLegendSymbol; export type PreparedLegend = Required & { height: number; diff --git a/src/plugins/d3/renderer/hooks/useSeries/utils.ts b/src/plugins/d3/renderer/hooks/useSeries/utils.ts index d525c338..50e4eda3 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/utils.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/utils.ts @@ -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((acc, s) => { @@ -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, }; } diff --git a/src/types/widget-data/legend.ts b/src/types/widget-data/legend.ts index c1c3f68a..922261f7 100644 --- a/src/types/widget-data/legend.ts +++ b/src/types/widget-data/legend.ts @@ -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 rectangle in the legend + * + * @default 16 + * */ + width?: number; +}; From 1b38f472534bc61c48fb0ad870b38f1ce6ce4a93 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Fri, 1 Dec 2023 18:21:39 +0300 Subject: [PATCH 2/3] fix inactive legend color --- src/plugins/d3/renderer/components/Legend.tsx | 10 +++++---- .../d3/renderer/components/styles.scss | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/plugins/d3/renderer/components/Legend.tsx b/src/plugins/d3/renderer/components/Legend.tsx index 0397306c..2a7c6d0f 100644 --- a/src/plugins/d3/renderer/components/Legend.tsx +++ b/src/plugins/d3/renderer/components/Legend.tsx @@ -120,6 +120,8 @@ function renderLegendSymbol(args: { 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': { @@ -133,9 +135,9 @@ function renderLegendSymbol(args: { .append('path') .attr('d', legendSymbolGenerator(points)) .attr('fill', 'none') - .attr('stroke', d.color) .attr('stroke-width', d.symbol.strokeWidth) - .attr('class', b('item-shape', {unselected: !d.visible})); + .attr('class', className) + .style('stroke', color); break; } @@ -148,8 +150,8 @@ function renderLegendSymbol(args: { .attr('width', d.symbol.width) .attr('height', d.symbol.height) .attr('rx', d.symbol.radius) - .attr('class', b('item-shape', {unselected: !d.visible})) - .style('fill', d.color); + .attr('class', className) + .style('fill', color); break; } diff --git a/src/plugins/d3/renderer/components/styles.scss b/src/plugins/d3/renderer/components/styles.scss index 599156a9..c3ab7f2e 100644 --- a/src/plugins/d3/renderer/components/styles.scss +++ b/src/plugins/d3/renderer/components/styles.scss @@ -24,11 +24,23 @@ user-select: none; } - &__item-shape { - fill: var(--g-color-base-misc-medium); - - &_unselected { - fill: var(--g-color-text-hint); + &__item-symbol { + &_shape { + &_rect { + fill: var(--g-color-base-misc-medium); + + &_unselected { + fill: var(--g-color-text-hint); + } + } + + &_path { + stroke: var(--g-color-base-misc-medium); + + &_unselected { + stroke: var(--g-color-text-hint); + } + } } } From 52d8ebd4419687d4e42321ebb1dfd3516c791094 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Fri, 1 Dec 2023 18:53:34 +0300 Subject: [PATCH 3/3] fix review(1) --- .../d3/renderer/components/styles.scss | 22 +++++-------------- src/types/widget-data/legend.ts | 2 +- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/plugins/d3/renderer/components/styles.scss b/src/plugins/d3/renderer/components/styles.scss index c3ab7f2e..9ef8b469 100644 --- a/src/plugins/d3/renderer/components/styles.scss +++ b/src/plugins/d3/renderer/components/styles.scss @@ -25,22 +25,12 @@ } &__item-symbol { - &_shape { - &_rect { - fill: var(--g-color-base-misc-medium); - - &_unselected { - fill: var(--g-color-text-hint); - } - } - - &_path { - stroke: var(--g-color-base-misc-medium); - - &_unselected { - stroke: var(--g-color-text-hint); - } - } + &_shape_rect#{&}_unselected { + fill: var(--g-color-text-hint); + } + + &_shape_path#{&}_unselected { + stroke: var(--g-color-text-hint); } } diff --git a/src/types/widget-data/legend.ts b/src/types/widget-data/legend.ts index 922261f7..528f9a2c 100644 --- a/src/types/widget-data/legend.ts +++ b/src/types/widget-data/legend.ts @@ -60,7 +60,7 @@ export type RectLegendSymbolOptions = BaseLegendSymbol & { export type PathLegendSymbolOptions = BaseLegendSymbol & { /** - * The pixel width of the symbol for series types that use a rectangle in the legend + * The pixel width of the symbol for series types that use a path in the legend * * @default 16 * */