diff --git a/src/plugins/d3/renderer/components/Chart.tsx b/src/plugins/d3/renderer/components/Chart.tsx index 5f47c011..fdd15757 100644 --- a/src/plugins/d3/renderer/components/Chart.tsx +++ b/src/plugins/d3/renderer/components/Chart.tsx @@ -12,7 +12,6 @@ import { useChartDimensions, useChartEvents, useChartOptions, - useLegend, useAxisScales, useSeries, useShapes, @@ -35,12 +34,10 @@ type Props = { export const Chart = (props: Props) => { const {top, left, width, height, data} = props; // FIXME: add data validation - const {series} = data; const svgRef = React.createRef(); - const hasAxisRelatedSeries = series.data.some(isAxisRelatedSeries); const {chartHovered, handleMouseEnter, handleMouseLeave} = useChartEvents(); const {chart, legend, title, tooltip, xAxis, yAxis} = useChartOptions(data); - const {boundsWidth, boundsHeight, legendHeight} = useChartDimensions({ + const {boundsWidth, boundsHeight} = useChartDimensions({ width, height, margin: chart.margin, @@ -49,8 +46,7 @@ export const Chart = (props: Props) => { xAxis, yAxis, }); - const {activeLegendItems, handleLegendItemClick} = useLegend({series: series.data}); - const {chartSeries} = useSeries({activeLegendItems, series: series.data}); + const {chartSeries, handleLegendItemClick} = useSeries({series: data.series, legend}); const {xScale, yScale} = useAxisScales({ boundsWidth, boundsHeight, @@ -75,6 +71,7 @@ export const Chart = (props: Props) => { onSeriesMouseMove: handleSeriesMouseMove, onSeriesMouseLeave: handleSeriesMouseLeave, }); + const hasAxisRelatedSeries = chartSeries.some(isAxisRelatedSeries); return ( @@ -119,8 +116,9 @@ export const Chart = (props: Props) => { diff --git a/src/plugins/d3/renderer/components/Legend.tsx b/src/plugins/d3/renderer/components/Legend.tsx index cdaa36cb..cb537d0d 100644 --- a/src/plugins/d3/renderer/components/Legend.tsx +++ b/src/plugins/d3/renderer/components/Legend.tsx @@ -1,9 +1,14 @@ import React from 'react'; -import {select} from 'd3'; +import {select, sum} from 'd3'; import get from 'lodash/get'; import {block} from '../../../../utils/cn'; -import type {ChartSeries, OnLegendItemClick} from '../hooks'; +import type { + OnLegendItemClick, + PreparedLegend, + PreparedLegendSymbol, + PreparedSeries, +} from '../hooks'; import {isAxisRelatedSeries} from '../utils'; const b = block('d3-legend'); @@ -11,31 +16,68 @@ const b = block('d3-legend'); type Props = { width: number; height: number; + legend: PreparedLegend; offsetWidth: number; offsetHeight: number; - chartSeries: ChartSeries[]; + chartSeries: PreparedSeries[]; onItemClick: OnLegendItemClick; }; -type LegendItem = {color: string; name: string; visible?: boolean}; +type LegendItem = { + color: string; + name: string; + visible?: boolean; + symbol: PreparedLegendSymbol; +}; -const getLegendItems = (series: ChartSeries[]) => { +const getLegendItems = (series: PreparedSeries[]) => { return series.reduce((acc, s) => { const isAxisRelated = isAxisRelatedSeries(s); const legendEnabled = get(s, 'legend.enabled', true); - if (isAxisRelated) { - acc.push(s); - } else if (!isAxisRelated && legendEnabled) { - acc.push(...(s.data as LegendItem[])); + if (legendEnabled) { + if (isAxisRelated) { + acc.push({ + ...s, + symbol: s.legend.symbol, + }); + } else { + const legendItems = s.data.map((item) => { + return { + ...item, + symbol: s.legend.symbol, + } as LegendItem; + }); + acc.push(...legendItems); + } } return acc; }, []); }; +function getLegendPosition(args: { + align: PreparedLegend['align']; + contentWidth: number; + width: number; + offsetWidth: number; +}) { + const {align, offsetWidth, width, contentWidth} = args; + const top = 0; + + if (align === 'left') { + return {top, left: offsetWidth}; + } + + if (align === 'right') { + return {top, left: offsetWidth + width - contentWidth}; + } + + return {top, left: offsetWidth + width / 2 - contentWidth / 2}; +} + export const Legend = (props: Props) => { - const {width, offsetWidth, height, offsetHeight, chartSeries, onItemClick} = props; + const {width, offsetWidth, height, offsetHeight, chartSeries, legend, onItemClick} = props; const ref = React.useRef(null); React.useEffect(() => { @@ -44,7 +86,6 @@ export const Legend = (props: Props) => { } const legendItems = getLegendItems(chartSeries); - const size = 10; const textWidths: number[] = [0]; const svgElement = select(ref.current); svgElement.selectAll('*').remove(); @@ -71,27 +112,33 @@ export const Legend = (props: Props) => { legendItemTemplate .append('rect') - .attr('x', function (_d, i) { + .attr('x', function (legendItem, i) { return ( - offsetWidth + - i * size + + i * legendItem.symbol.width + + i * legend.itemDistance + + i * legendItem.symbol.padding + textWidths.slice(0, i + 1).reduce((acc, tw) => acc + tw, 0) ); }) - .attr('y', offsetHeight - size / 2) - .attr('width', size) - .attr('height', size) + .attr('y', (legendItem) => offsetHeight - 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', b('item-shape')) .style('fill', function (d) { return d.color; }); legendItemTemplate .append('text') - .attr('x', function (_d, i) { + .attr('x', function (legendItem, i) { return ( - offsetWidth + - i * size + - size + + i * legendItem.symbol.width + + i * legend.itemDistance + + i * legendItem.symbol.padding + + legendItem.symbol.width + + legendItem.symbol.padding + textWidths.slice(0, i + 1).reduce((acc, tw) => acc + tw, 0) ); }) @@ -104,7 +151,21 @@ export const Legend = (props: Props) => { return ('name' in d && d.name) as string; }) .style('alignment-baseline', 'middle'); - }, [width, offsetWidth, height, offsetHeight, chartSeries, onItemClick]); + + const contentWidth = + sum(textWidths) + + sum(legendItems, (item) => item.symbol.width + item.symbol.padding) + + legend.itemDistance * (legendItems.length - 1); + + const {left} = getLegendPosition({ + align: legend.align, + width, + offsetWidth, + contentWidth, + }); + + svgElement.attr('transform', `translate(${[left, 0].join(',')})`); + }, [width, offsetWidth, height, offsetHeight, chartSeries, onItemClick, legend]); return ; }; diff --git a/src/plugins/d3/renderer/hooks/index.ts b/src/plugins/d3/renderer/hooks/index.ts index c2dcaa5d..498be3df 100644 --- a/src/plugins/d3/renderer/hooks/index.ts +++ b/src/plugins/d3/renderer/hooks/index.ts @@ -2,9 +2,9 @@ export * from './useChartDimensions'; export * from './useChartEvents'; export * from './useChartOptions'; export * from './useChartOptions/types'; -export * from './useLegend'; export * from './useAxisScales'; export * from './useSeries'; +export * from './useSeries/types'; export * from './useShapes'; export * from './useTooltip'; export * from './useTooltip/types'; diff --git a/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts b/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts index af9613c0..4cc5fb75 100644 --- a/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts +++ b/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts @@ -1,14 +1,12 @@ import type {ChartMargin} from '../../../../../types/widget-data'; -import type {ChartOptions, PreparedAxis, PreparedTitle} from '../useChartOptions/types'; - -const LEGEND_LINE_HEIGHT = 15; +import type {PreparedAxis, PreparedLegend, PreparedTitle} from '../useChartOptions/types'; type Args = { width: number; height: number; margin: ChartMargin; - legend: ChartOptions['legend']; + legend: PreparedLegend; title?: PreparedTitle; xAxis?: PreparedAxis; yAxis?: PreparedAxis[]; @@ -16,7 +14,6 @@ type Args = { export const useChartDimensions = (args: Args) => { const {margin, legend, title, width, height, xAxis, yAxis} = args; - const legendHeight = legend.enabled ? LEGEND_LINE_HEIGHT : 0; const titleHeight = title?.height || 0; const xAxisTitleHeight = xAxis?.title.height || 0; const yAxisTitleHeight = @@ -26,7 +23,7 @@ export const useChartDimensions = (args: Args) => { const boundsWidth = width - margin.right - margin.left - yAxisTitleHeight; const boundsHeight = - height - margin.top - margin.bottom - legendHeight - titleHeight - xAxisTitleHeight; + height - margin.top - margin.bottom - legend.height - titleHeight - xAxisTitleHeight; - return {boundsWidth, boundsHeight, legendHeight}; + return {boundsWidth, boundsHeight, legendHeight: legend.height}; }; diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/legend.ts b/src/plugins/d3/renderer/hooks/useChartOptions/legend.ts index a1f9ee33..8d0b3b13 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/legend.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/legend.ts @@ -2,14 +2,20 @@ import type {ChartKitWidgetData} from '../../../../../types/widget-data'; import type {PreparedLegend} from './types'; +const LEGEND_LINE_HEIGHT = 15; + export const getPreparedLegend = (args: { legend: ChartKitWidgetData['legend']; series: ChartKitWidgetData['series']; }): PreparedLegend => { const {legend, series} = args; - const enabled = legend?.enabled; + const enabled = typeof legend?.enabled === 'boolean' ? legend?.enabled : series.data.length > 1; + const height = enabled ? LEGEND_LINE_HEIGHT : 0; return { - enabled: typeof enabled === 'boolean' ? enabled : series.data.length > 1, + align: legend?.align || 'center', + enabled, + itemDistance: legend?.itemDistance || 20, + height, }; }; diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts index bcf561ef..0962c79f 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts @@ -17,7 +17,9 @@ export type PreparedChart = { margin: ChartMargin; }; -export type PreparedLegend = Required; +export type PreparedLegend = Required & { + height: number; +}; export type PreparedAxis = Omit & { type: ChartKitWidgetAxisType; diff --git a/src/plugins/d3/renderer/hooks/useLegend/index.ts b/src/plugins/d3/renderer/hooks/useLegend/index.ts deleted file mode 100644 index 288b6b72..00000000 --- a/src/plugins/d3/renderer/hooks/useLegend/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import get from 'lodash/get'; - -import type {ChartKitWidgetSeries} from '../../../../../types/widget-data'; - -import {isAxisRelatedSeries} from '../../utils'; - -export type OnLegendItemClick = (data: {name: string; metaKey: boolean}) => void; - -type Args = { - series: ChartKitWidgetSeries[]; -}; - -const getActiveLegendItems = (series: ChartKitWidgetSeries[]) => { - return series.reduce((acc, s) => { - const isAxisRelated = isAxisRelatedSeries(s); - const isLegendEnabled = get(s, 'legend.enabled', true); - const isSeriesVisible = get(s, 'visible', true); - - if (isLegendEnabled && isAxisRelated && isSeriesVisible && 'name' in s) { - acc.push(s.name); - } else if (isLegendEnabled && !isAxisRelated) { - s.data.forEach((d) => { - const isDataVisible = get(d, 'visible', true); - - if (isDataVisible && 'name' in d) { - acc.push(d.name); - } - }); - } - - return acc; - }, []); -}; - -const getAllLegendItems = (series: ChartKitWidgetSeries[]) => { - return series.reduce((acc, s) => { - if (isAxisRelatedSeries(s) && 'name' in s) { - acc.push(s.name); - } else { - acc.push(...s.data.map((d) => ('name' in d && d.name) || '')); - } - - return acc; - }, []); -}; - -export const useLegend = (args: Args) => { - const {series} = args; - const [activeLegendItems, setActiveLegendItems] = React.useState(getActiveLegendItems(series)); - - const handleLegendItemClick: OnLegendItemClick = React.useCallback( - ({name, metaKey}) => { - const onlyItemSelected = - activeLegendItems.length === 1 && activeLegendItems.includes(name); - let nextActiveLegendItems: string[]; - - if (metaKey && activeLegendItems.includes(name)) { - nextActiveLegendItems = activeLegendItems.filter((item) => item !== name); - } else if (metaKey && !activeLegendItems.includes(name)) { - nextActiveLegendItems = activeLegendItems.concat(name); - } else if (onlyItemSelected) { - nextActiveLegendItems = getAllLegendItems(series); - } else { - nextActiveLegendItems = [name]; - } - - setActiveLegendItems(nextActiveLegendItems); - }, - [series, activeLegendItems], - ); - - // FIXME: remove effect. It initiates extra rerender - React.useEffect(() => { - setActiveLegendItems(getActiveLegendItems(series)); - }, [series]); - - return {activeLegendItems, handleLegendItemClick}; -}; diff --git a/src/plugins/d3/renderer/hooks/useSeries/index.ts b/src/plugins/d3/renderer/hooks/useSeries/index.ts index 76a4de0b..f291ddc6 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/index.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/index.ts @@ -4,6 +4,7 @@ import get from 'lodash/get'; import {ScaleOrdinal, scaleOrdinal} from 'd3'; import type { + ChartKitWidgetData, ChartKitWidgetSeries, PieSeries, PieSeriesData, @@ -11,40 +12,48 @@ import type { import {DEFAULT_PALETTE} from '../../constants'; import {getSeriesNames, isAxisRelatedSeries} from '../../utils'; +import {PreparedLegend} from '../useChartOptions/types'; +import {getActiveLegendItems, getAllLegendItems, prepareLegendSymbol} from './utils'; +import {PreparedSeries} from './types'; -export type ChartSeries = ChartKitWidgetSeries & { - color: string; - name: string; - visible: boolean; -}; +export type OnLegendItemClick = (data: {name: string; metaKey: boolean}) => void; type Args = { - activeLegendItems: string[]; - series: ChartKitWidgetSeries[]; + legend: PreparedLegend; + series: ChartKitWidgetData['series']; }; const prepareAxisRelatedSeries = (args: { activeLegendItems: string[]; colorScale: ScaleOrdinal; series: ChartKitWidgetSeries; + legend: PreparedLegend; }) => { - const {activeLegendItems, colorScale, series} = args; - const preparedSeries = cloneDeep(series) as ChartSeries; - const legendEnabled = get(preparedSeries, 'legend.enabled', true); + const {activeLegendItems, colorScale, series, legend} = args; + const preparedSeries = cloneDeep(series) as PreparedSeries; + const legendEnabled = get(preparedSeries, 'legend.enabled', legend.enabled); const defaultVisible = get(preparedSeries, 'visible', true); const name = 'name' in series && series.name ? series.name : ''; const color = 'color' in series && series.color ? series.color : colorScale(name); preparedSeries.color = color; preparedSeries.name = name; preparedSeries.visible = legendEnabled ? activeLegendItems.includes(name) : defaultVisible; + preparedSeries.legend = { + enabled: legendEnabled, + symbol: prepareLegendSymbol(preparedSeries), + }; return preparedSeries; }; -const preparePieSeries = (args: {activeLegendItems: string[]; series: PieSeries}) => { - const {activeLegendItems, series} = args; - const preparedSeries = cloneDeep(series) as ChartSeries; - const legendEnabled = get(preparedSeries, 'legend.enabled', true); +const preparePieSeries = (args: { + activeLegendItems: string[]; + series: PieSeries; + legend: PreparedLegend; +}) => { + const {activeLegendItems, series, legend} = args; + const preparedSeries = cloneDeep(series) as PreparedSeries; + const legendEnabled = get(preparedSeries, 'legend.enabled', legend.enabled); const dataNames = series.data.map((d) => d.name); const colorScale = scaleOrdinal(dataNames, DEFAULT_PALETTE); preparedSeries.data = (preparedSeries.data as PieSeriesData[]).map((d) => { @@ -56,6 +65,10 @@ const preparePieSeries = (args: {activeLegendItems: string[]; series: PieSeries} // Not axis related series manages their own data visibility inside their data preparedSeries.visible = true; + preparedSeries.legend = { + enabled: legendEnabled, + symbol: prepareLegendSymbol(preparedSeries), + }; return preparedSeries; }; @@ -63,12 +76,13 @@ const preparePieSeries = (args: {activeLegendItems: string[]; series: PieSeries} const prepareNotAxisRelatedSeries = (args: { activeLegendItems: string[]; series: ChartKitWidgetSeries; + legend: PreparedLegend; }) => { - const {activeLegendItems, series} = args; + const {activeLegendItems, series, legend} = args; switch (series.type) { case 'pie': { - return preparePieSeries({activeLegendItems, series}); + return preparePieSeries({activeLegendItems, series, legend}); } default: { throw new Error( @@ -79,20 +93,52 @@ const prepareNotAxisRelatedSeries = (args: { }; export const useSeries = (args: Args) => { - const {activeLegendItems, series} = args; - const chartSeries = React.useMemo(() => { + const { + series: {data: series}, + legend, + } = args; + const [activeLegendItems, setActiveLegendItems] = React.useState(getActiveLegendItems(series)); + + // FIXME: remove effect. It initiates extra rerender + React.useEffect(() => { + setActiveLegendItems(getActiveLegendItems(series)); + }, [series]); + + const chartSeries = React.useMemo(() => { const seriesNames = getSeriesNames(series); const colorScale = scaleOrdinal(seriesNames, DEFAULT_PALETTE); return series.map((s) => { return isAxisRelatedSeries(s) - ? prepareAxisRelatedSeries({activeLegendItems, colorScale, series: s}) + ? prepareAxisRelatedSeries({activeLegendItems, colorScale, series: s, legend}) : prepareNotAxisRelatedSeries({ activeLegendItems, series: s, + legend, }); }); }, [activeLegendItems, series]); - return {chartSeries}; + const handleLegendItemClick: OnLegendItemClick = React.useCallback( + ({name, metaKey}) => { + const onlyItemSelected = + activeLegendItems.length === 1 && activeLegendItems.includes(name); + let nextActiveLegendItems: string[]; + + if (metaKey && activeLegendItems.includes(name)) { + nextActiveLegendItems = activeLegendItems.filter((item) => item !== name); + } else if (metaKey && !activeLegendItems.includes(name)) { + nextActiveLegendItems = activeLegendItems.concat(name); + } else if (onlyItemSelected) { + nextActiveLegendItems = getAllLegendItems(series); + } else { + nextActiveLegendItems = [name]; + } + + setActiveLegendItems(nextActiveLegendItems); + }, + [series, activeLegendItems], + ); + + return {chartSeries, handleLegendItemClick}; }; diff --git a/src/plugins/d3/renderer/hooks/useSeries/types.ts b/src/plugins/d3/renderer/hooks/useSeries/types.ts new file mode 100644 index 00000000..47d996c4 --- /dev/null +++ b/src/plugins/d3/renderer/hooks/useSeries/types.ts @@ -0,0 +1,16 @@ +import {ChartKitWidgetSeries, RectLegendSymbolOptions} from '../../../../../types/widget-data'; + +export type RectLegendSymbol = { + shape: 'rect'; +} & Required; + +export type PreparedLegendSymbol = RectLegendSymbol; + +export type PreparedSeries = ChartKitWidgetSeries & { + color: string; + name: string; + visible: boolean; + legend: ChartKitWidgetSeries['legend'] & { + symbol: PreparedLegendSymbol; + }; +}; diff --git a/src/plugins/d3/renderer/hooks/useSeries/utils.ts b/src/plugins/d3/renderer/hooks/useSeries/utils.ts new file mode 100644 index 00000000..c49b3b02 --- /dev/null +++ b/src/plugins/d3/renderer/hooks/useSeries/utils.ts @@ -0,0 +1,57 @@ +import {ChartKitWidgetSeries, RectLegendSymbolOptions} from '../../../../../types/widget-data'; +import {isAxisRelatedSeries} from '../../utils'; +import get from 'lodash/get'; +import {PreparedLegendSymbol} from './types'; + +export const getActiveLegendItems = (series: ChartKitWidgetSeries[]) => { + return series.reduce((acc, s) => { + const isAxisRelated = isAxisRelatedSeries(s); + const isLegendEnabled = get(s, 'legend.enabled', true); + const isSeriesVisible = get(s, 'visible', true); + + if (isLegendEnabled && isAxisRelated && isSeriesVisible && 'name' in s) { + acc.push(s.name); + } else if (isLegendEnabled && !isAxisRelated) { + s.data.forEach((d) => { + const isDataVisible = get(d, 'visible', true); + + if (isDataVisible && 'name' in d) { + acc.push(d.name); + } + }); + } + + return acc; + }, []); +}; + +export const getAllLegendItems = (series: ChartKitWidgetSeries[]) => { + return series.reduce((acc, s) => { + if (isAxisRelatedSeries(s) && 'name' in s) { + acc.push(s.name); + } else { + acc.push(...s.data.map((d) => ('name' in d && d.name) || '')); + } + + return acc; + }, []); +}; + +const DEFAULT_LEGEND_SYMBOL_SIZE = 10; + +export function prepareLegendSymbol(series: ChartKitWidgetSeries): PreparedLegendSymbol { + switch (series.type) { + default: { + const symbolOptions: RectLegendSymbolOptions = series.legend?.symbol || {}; + const symbolHeight = symbolOptions?.height || DEFAULT_LEGEND_SYMBOL_SIZE; + + return { + shape: 'rect', + width: symbolOptions?.width || DEFAULT_LEGEND_SYMBOL_SIZE, + height: symbolHeight, + radius: symbolOptions?.radius || symbolHeight / 2, + padding: symbolOptions?.padding || 5, + }; + } + } +} diff --git a/src/plugins/d3/renderer/hooks/useShapes/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/index.tsx index 56cb3689..f5e09aa8 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/index.tsx @@ -6,7 +6,7 @@ import type {BarXSeries, PieSeries, ScatterSeries} from '../../../../../types/wi import {getOnlyVisibleSeries} from '../../utils'; import type {ChartOptions} from '../useChartOptions/types'; import type {ChartScale} from '../useAxisScales'; -import type {ChartSeries} from '../useSeries'; +import type {PreparedSeries} from '../'; import type {OnSeriesMouseMove, OnSeriesMouseLeave} from '../useTooltip/types'; import {prepareBarXSeries} from './bar-x'; import {prepareScatterSeries} from './scatter'; @@ -19,7 +19,7 @@ type Args = { left: number; boundsWidth: number; boundsHeight: number; - series: ChartSeries[]; + series: PreparedSeries[]; xAxis: ChartOptions['xAxis']; yAxis: ChartOptions['yAxis']; svgContainer: SVGSVGElement | null; diff --git a/src/plugins/d3/renderer/hooks/useShapes/scatter.tsx b/src/plugins/d3/renderer/hooks/useShapes/scatter.tsx index a08d35d9..850e5506 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/scatter.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/scatter.tsx @@ -17,7 +17,6 @@ type PrepareScatterSeriesArgs = { svgContainer: SVGSVGElement | null; onSeriesMouseMove?: OnSeriesMouseMove; onSeriesMouseLeave?: OnSeriesMouseLeave; - key?: string; }; const b = block('d3-scatter'); @@ -73,11 +72,11 @@ export function prepareScatterSeries(args: PrepareScatterSeriesArgs) { yScale, onSeriesMouseMove, onSeriesMouseLeave, - key, svgContainer, } = args; return series.reduce((result, s) => { + const randomKey = Math.random().toString(); const preparedData = xAxis.type === 'category' || yAxis[0]?.type === 'category' ? prepareCategoricalScatterData(s.data) @@ -95,7 +94,7 @@ export function prepareScatterSeries(args: PrepareScatterSeriesArgs) { return ( = BaseSeriesData & { /** The x value of the point */ @@ -39,4 +40,9 @@ export type BarXSeries = BaseSeries & { /** Whether to align the data label inside the box or to the actual value point */ inside?: boolean; }; + + /** Individual series legend options. Has higher priority than legend options in widget data */ + legend?: ChartKitWidgetLegend & { + symbol?: RectLegendSymbolOptions; + }; }; diff --git a/src/types/widget-data/base.ts b/src/types/widget-data/base.ts index f687ab5c..54b55123 100644 --- a/src/types/widget-data/base.ts +++ b/src/types/widget-data/base.ts @@ -1,8 +1,4 @@ -import type {ChartKitWidgetLegend} from './legend'; - export type BaseSeries = { - /** Individual series legend options. Has higher priority than legend options in widget data */ - legend?: ChartKitWidgetLegend; /** Initial visibility of the series */ visible?: boolean; }; diff --git a/src/types/widget-data/legend.ts b/src/types/widget-data/legend.ts index 206d6684..d3a9e241 100644 --- a/src/types/widget-data/legend.ts +++ b/src/types/widget-data/legend.ts @@ -1,3 +1,41 @@ export type ChartKitWidgetLegend = { enabled?: boolean; + + /** The horizontal alignment of the legend box within the chart area. + * + * @default center + * */ + align?: 'left' | 'center' | 'right'; + + /** Defines the pixel distance between each legend item + * + * @default 20 + * */ + itemDistance?: number; +}; + +export type BaseLegendSymbol = { + /** The pixel padding between the legend item symbol and the legend item text. + * + * @default 5 + * */ + padding?: number; +}; + +export type RectLegendSymbolOptions = BaseLegendSymbol & { + /** The pixel width of the symbol for series types that use a rectangle in the legend + * + * @default 10 + * */ + width?: number; + + /** The pixel width of the symbol for series types that use a rectangle in the legend + * + * @default 10 + * */ + height?: number; + + /** The border radius of the symbol for series types that use a rectangle in the legend. + * Defaults to half the symbolHeight, effectively creating a circle. */ + radius?: number; }; diff --git a/src/types/widget-data/pie.ts b/src/types/widget-data/pie.ts index 10a875cc..b55b17c0 100644 --- a/src/types/widget-data/pie.ts +++ b/src/types/widget-data/pie.ts @@ -1,4 +1,5 @@ import type {BaseSeries, BaseSeriesData} from './base'; +import {ChartKitWidgetLegend, RectLegendSymbolOptions} from './legend'; export type PieSeriesData = BaseSeriesData & { /** The value of the pie segment. */ @@ -26,4 +27,9 @@ export type PieSeries = BaseSeries & { innerRadius?: string | number; /** The radius of the pie relative to the chart area. The default behaviour is to scale to the chart area. */ radius?: string | number; + + /** Individual series legend options. Has higher priority than legend options in widget data */ + legend?: ChartKitWidgetLegend & { + symbol?: RectLegendSymbolOptions; + }; }; diff --git a/src/types/widget-data/scatter.ts b/src/types/widget-data/scatter.ts index 3d42d3de..90143501 100644 --- a/src/types/widget-data/scatter.ts +++ b/src/types/widget-data/scatter.ts @@ -1,4 +1,5 @@ import type {BaseSeries, BaseSeriesData} from './base'; +import type {ChartKitWidgetLegend, RectLegendSymbolOptions} from './legend'; export type ScatterSeriesData = BaseSeriesData & { /** The x value of the point */ @@ -20,4 +21,9 @@ export type ScatterSeries = BaseSeries & { /** A predefined shape or symbol for the dot */ symbol?: string; // yAxisIndex?: number; + + /** Individual series legend options. Has higher priority than legend options in widget data */ + legend?: ChartKitWidgetLegend & { + symbol?: RectLegendSymbolOptions; + }; };