From 3a28b47b681b760c6c52146751c014e197a0e521 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Wed, 9 Oct 2024 15:52:14 +0300 Subject: [PATCH 1/2] feat(D3 plugin): html labels for most of series --- .../__stories__/area/HtmlLabels.stories.tsx | 80 ++++++++++++++++++ .../__stories__/bar-x/HtmlLabels.stories.tsx | 82 +++++++++++++++++++ .../__stories__/bar-y/HtmlLabels.stories.tsx | 82 +++++++++++++++++++ .../__stories__/line/HtmlLabels.stories.tsx | 80 ++++++++++++++++++ .../renderer/hooks/useSeries/prepare-area.ts | 1 + .../renderer/hooks/useSeries/prepare-bar-x.ts | 1 + .../renderer/hooks/useSeries/prepare-bar-y.ts | 16 ++-- .../renderer/hooks/useSeries/prepare-line.ts | 1 + .../hooks/useSeries/prepare-treemap.ts | 1 + .../hooks/useSeries/prepare-waterfall.ts | 1 + .../d3/renderer/hooks/useSeries/types.ts | 6 ++ .../d3/renderer/hooks/useShapes/HtmlLayer.tsx | 32 ++++++++ .../renderer/hooks/useShapes/area/index.tsx | 19 ++++- .../hooks/useShapes/area/prepare-data.ts | 23 +++++- .../d3/renderer/hooks/useShapes/area/types.ts | 3 +- .../renderer/hooks/useShapes/bar-x/index.tsx | 20 ++++- .../hooks/useShapes/bar-x/prepare-data.ts | 24 +++++- .../renderer/hooks/useShapes/bar-x/types.ts | 3 +- .../renderer/hooks/useShapes/bar-y/index.tsx | 58 +++++++------ .../hooks/useShapes/bar-y/prepare-data.ts | 52 +++++++++++- .../renderer/hooks/useShapes/bar-y/types.ts | 3 + .../d3/renderer/hooks/useShapes/index.tsx | 7 ++ .../renderer/hooks/useShapes/line/index.tsx | 18 +++- .../hooks/useShapes/line/prepare-data.ts | 21 ++++- .../d3/renderer/hooks/useShapes/line/types.ts | 3 +- .../d3/renderer/hooks/useShapes/pie/index.tsx | 16 +--- .../hooks/useShapes/pie/prepare-data.ts | 1 - .../hooks/useShapes/scatter/index.tsx | 18 +++- .../hooks/useShapes/scatter/prepare-data.ts | 1 + .../renderer/hooks/useShapes/scatter/types.ts | 2 + .../hooks/useShapes/treemap/index.tsx | 14 +++- .../hooks/useShapes/treemap/prepare-data.ts | 2 +- .../renderer/hooks/useShapes/treemap/types.ts | 2 + .../hooks/useShapes/waterfall/index.tsx | 18 +++- .../hooks/useShapes/waterfall/prepare-data.ts | 1 + .../hooks/useShapes/waterfall/types.ts | 3 +- src/plugins/d3/renderer/types/index.ts | 1 - src/plugins/d3/renderer/utils/text.ts | 23 ++++-- src/types/widget-data/bar-x.ts | 17 ++-- 39 files changed, 661 insertions(+), 95 deletions(-) create mode 100644 src/plugins/d3/__stories__/area/HtmlLabels.stories.tsx create mode 100644 src/plugins/d3/__stories__/bar-x/HtmlLabels.stories.tsx create mode 100644 src/plugins/d3/__stories__/bar-y/HtmlLabels.stories.tsx create mode 100644 src/plugins/d3/__stories__/line/HtmlLabels.stories.tsx create mode 100644 src/plugins/d3/renderer/hooks/useShapes/HtmlLayer.tsx diff --git a/src/plugins/d3/__stories__/area/HtmlLabels.stories.tsx b/src/plugins/d3/__stories__/area/HtmlLabels.stories.tsx new file mode 100644 index 00000000..eb2e4937 --- /dev/null +++ b/src/plugins/d3/__stories__/area/HtmlLabels.stories.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +import {Col, Container, Row} from '@gravity-ui/uikit'; +import type {StoryObj} from '@storybook/react'; + +import {ChartKit} from '../../../../components/ChartKit'; +import {Loader} from '../../../../components/Loader/Loader'; +import {settings} from '../../../../libs'; +import type {ChartKitWidgetData} from '../../../../types'; +import {ExampleWrapper} from '../../examples/ExampleWrapper'; +import {D3Plugin} from '../../index'; + +const AreaWithHtmlLabels = () => { + const [loading, setLoading] = React.useState(true); + + React.useEffect(() => { + settings.set({plugins: [D3Plugin]}); + setLoading(false); + }, []); + + if (loading) { + return ; + } + + const getLabelData = (value: string, color: string) => { + const labelStyle = `background: ${color};color: #fff;padding: 4px;border-radius: 4px;`; + return { + label: `${value}`, + }; + }; + + const widgetData: ChartKitWidgetData = { + series: { + data: [ + { + type: 'area', + name: 'Series 1', + dataLabels: { + enabled: true, + html: true, + }, + data: [ + { + x: 1, + y: Math.random() * 1000, + ...getLabelData('First', '#4fc4b7'), + }, + { + x: 100, + y: Math.random() * 1000, + ...getLabelData('Last', '#8ccce3'), + }, + ], + }, + ], + }, + title: {text: 'Area with html labels'}, + }; + + return ( + + + + + + + + + + ); +}; + +export const AreaWithHtmlLabelsStory: StoryObj = { + name: 'Html in labels', +}; + +export default { + title: 'Plugins/D3/Area', + component: AreaWithHtmlLabels, +}; diff --git a/src/plugins/d3/__stories__/bar-x/HtmlLabels.stories.tsx b/src/plugins/d3/__stories__/bar-x/HtmlLabels.stories.tsx new file mode 100644 index 00000000..3e17393a --- /dev/null +++ b/src/plugins/d3/__stories__/bar-x/HtmlLabels.stories.tsx @@ -0,0 +1,82 @@ +import React from 'react'; + +import {Col, Container, Row} from '@gravity-ui/uikit'; +import type {StoryObj} from '@storybook/react'; + +import {ChartKit} from '../../../../components/ChartKit'; +import {Loader} from '../../../../components/Loader/Loader'; +import {settings} from '../../../../libs'; +import type {ChartKitWidgetData} from '../../../../types'; +import {ExampleWrapper} from '../../examples/ExampleWrapper'; +import {D3Plugin} from '../../index'; + +const BarXWithHtmlLabels = () => { + const [loading, setLoading] = React.useState(true); + + React.useEffect(() => { + settings.set({plugins: [D3Plugin]}); + setLoading(false); + }, []); + + if (loading) { + return ; + } + + const getLabelData = (value: string, color: string) => { + const labelStyle = `background: ${color};color: #fff;padding: 4px;border-radius: 4px;border: 1px solid #fff;`; + return { + label: `${value}`, + color, + }; + }; + + const widgetData: ChartKitWidgetData = { + series: { + data: [ + { + type: 'bar-x', + name: 'Series 1', + dataLabels: { + enabled: true, + html: true, + }, + data: [ + { + x: 0, + y: Math.random() * 1000, + ...getLabelData('First', '#4fc4b7'), + }, + { + x: 1, + y: Math.random() * 1000, + ...getLabelData('Last', '#8ccce3'), + }, + ], + }, + ], + }, + xAxis: {type: 'category', categories: ['First', 'Second']}, + title: {text: 'Bar-x with html labels'}, + }; + + return ( + + + + + + + + + + ); +}; + +export const BarXWithHtmlLabelsStory: StoryObj = { + name: 'Html in labels', +}; + +export default { + title: 'Plugins/D3/Bar-x', + component: BarXWithHtmlLabels, +}; diff --git a/src/plugins/d3/__stories__/bar-y/HtmlLabels.stories.tsx b/src/plugins/d3/__stories__/bar-y/HtmlLabels.stories.tsx new file mode 100644 index 00000000..a5e55381 --- /dev/null +++ b/src/plugins/d3/__stories__/bar-y/HtmlLabels.stories.tsx @@ -0,0 +1,82 @@ +import React from 'react'; + +import {Col, Container, Row} from '@gravity-ui/uikit'; +import type {StoryObj} from '@storybook/react'; + +import {ChartKit} from '../../../../components/ChartKit'; +import {Loader} from '../../../../components/Loader/Loader'; +import {settings} from '../../../../libs'; +import type {ChartKitWidgetData} from '../../../../types'; +import {ExampleWrapper} from '../../examples/ExampleWrapper'; +import {D3Plugin} from '../../index'; + +const BarYWithHtmlLabels = () => { + const [loading, setLoading] = React.useState(true); + + React.useEffect(() => { + settings.set({plugins: [D3Plugin]}); + setLoading(false); + }, []); + + if (loading) { + return ; + } + + const getLabelData = (value: string, color: string) => { + const labelStyle = `background: ${color};color: #fff;padding: 4px;border-radius: 4px;border: 1px solid #fff;`; + return { + label: `${value}`, + color, + }; + }; + + const widgetData: ChartKitWidgetData = { + series: { + data: [ + { + type: 'bar-y', + name: 'Series 1', + dataLabels: { + enabled: true, + html: true, + }, + data: [ + { + y: 0, + x: Math.random() * 1000, + ...getLabelData('First', '#4fc4b7'), + }, + { + y: 1, + x: Math.random() * 1000, + ...getLabelData('Last', '#8ccce3'), + }, + ], + }, + ], + }, + yAxis: [{type: 'category', categories: ['First', 'Second']}], + title: {text: 'Bar-y with html labels'}, + }; + + return ( + + + + + + + + + + ); +}; + +export const BarYWithHtmlLabelsStory: StoryObj = { + name: 'Html in labels', +}; + +export default { + title: 'Plugins/D3/Bar-y', + component: BarYWithHtmlLabels, +}; diff --git a/src/plugins/d3/__stories__/line/HtmlLabels.stories.tsx b/src/plugins/d3/__stories__/line/HtmlLabels.stories.tsx new file mode 100644 index 00000000..a2db3476 --- /dev/null +++ b/src/plugins/d3/__stories__/line/HtmlLabels.stories.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +import {Col, Container, Row} from '@gravity-ui/uikit'; +import type {StoryObj} from '@storybook/react'; + +import {ChartKit} from '../../../../components/ChartKit'; +import {Loader} from '../../../../components/Loader/Loader'; +import {settings} from '../../../../libs'; +import type {ChartKitWidgetData} from '../../../../types'; +import {ExampleWrapper} from '../../examples/ExampleWrapper'; +import {D3Plugin} from '../../index'; + +const LineWithHtmlLabels = () => { + const [loading, setLoading] = React.useState(true); + + React.useEffect(() => { + settings.set({plugins: [D3Plugin]}); + setLoading(false); + }, []); + + if (loading) { + return ; + } + + const getLabelData = (value: string, color: string) => { + const labelStyle = `background: ${color};color: #fff;padding: 4px;border-radius: 4px;`; + return { + label: `${value}`, + }; + }; + + const widgetData: ChartKitWidgetData = { + series: { + data: [ + { + type: 'line', + name: 'Series 1', + dataLabels: { + enabled: true, + html: true, + }, + data: [ + { + x: 1, + y: Math.random() * 1000, + ...getLabelData('First', '#4fc4b7'), + }, + { + x: 100, + y: Math.random() * 1000, + ...getLabelData('Last', '#8ccce3'), + }, + ], + }, + ], + }, + title: {text: 'Line with html labels'}, + }; + + return ( + + + + + + + + + + ); +}; + +export const LineWithHtmlLabelsStory: StoryObj = { + name: 'Html in labels', +}; + +export default { + title: 'Plugins/D3/Line', + component: LineWithHtmlLabels, +}; diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts index 11a04efe..f74ea92f 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-area.ts @@ -83,6 +83,7 @@ export function prepareArea(args: PrepareAreaSeriesArgs) { style: Object.assign({}, DEFAULT_DATALABELS_STYLE, series.dataLabels?.style), padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING), allowOverlap: get(series, 'dataLabels.allowOverlap', false), + html: get(series, 'dataLabels.html', false), }, marker: prepareMarker(series, seriesOptions), cursor: get(series, 'cursor', null), diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-x.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-x.ts index baa8669a..61d2ee9e 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-x.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-x.ts @@ -43,6 +43,7 @@ export function prepareBarXSeries(args: PrepareBarXSeriesArgs): PreparedSeries[] style: Object.assign({}, DEFAULT_DATALABELS_STYLE, series.dataLabels?.style), allowOverlap: series.dataLabels?.allowOverlap || false, padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING), + html: get(series, 'dataLabels.html', false), }, cursor: get(series, 'cursor', null), yAxis: get(series, 'yAxis', 0), diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-y.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-y.ts index aabfd592..6c4c1734 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-y.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-y.ts @@ -18,20 +18,24 @@ type PrepareBarYSeriesArgs = { function prepareDataLabels(series: BarYSeries) { const enabled = get(series, 'dataLabels.enabled', false); const style = Object.assign({}, DEFAULT_DATALABELS_STYLE, series.dataLabels?.style); - const {maxHeight = 0, maxWidth = 0} = enabled - ? getLabelsSize({ - labels: series.data.map((d) => String(d.label || d.x)), - style, - }) - : {}; + const html = get(series, 'dataLabels.html', false); + const labels = enabled ? series.data.map((d) => String(d.label || d.x)) : []; + const {maxHeight = 0, maxWidth = 0} = getLabelsSize({ + labels, + style, + html, + }); const inside = series.stacking === 'percent' ? true : get(series, 'dataLabels.inside', false); + console.log('prepareDataLabels', {maxWidth}); + return { enabled, inside, style, maxHeight, maxWidth, + html, }; } diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts index 0997854b..f52ce885 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-line.ts @@ -115,6 +115,7 @@ export function prepareLineSeries(args: PrepareLineSeriesArgs) { style: Object.assign({}, DEFAULT_DATALABELS_STYLE, series.dataLabels?.style), padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING), allowOverlap: get(series, 'dataLabels.allowOverlap', false), + html: get(series, 'dataLabels.html', false), }, marker: prepareMarker(series, seriesOptions), dashStyle: dashStyle as DashStyle, diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-treemap.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-treemap.ts index 717c85df..aaf6e1ea 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-treemap.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-treemap.ts @@ -32,6 +32,7 @@ export function prepareTreemap(args: PrepareTreemapSeriesArgs) { style: Object.assign({}, DEFAULT_DATALABELS_STYLE, s.dataLabels?.style), padding: get(s, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING), allowOverlap: get(s, 'dataLabels.allowOverlap', false), + html: get(series, 'dataLabels.html', false), }, id, type: s.type, diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-waterfall.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-waterfall.ts index cc2fe339..467085fb 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-waterfall.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-waterfall.ts @@ -41,6 +41,7 @@ export function prepareWaterfallSeries(args: PrepareWaterfallSeriesArgs): Prepar style: Object.assign({}, DEFAULT_DATALABELS_STYLE, series.dataLabels?.style), allowOverlap: series.dataLabels?.allowOverlap || false, padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING), + html: get(series, 'dataLabels.html', false), }, cursor: get(series, 'cursor', null), }; diff --git a/src/plugins/d3/renderer/hooks/useSeries/types.ts b/src/plugins/d3/renderer/hooks/useSeries/types.ts index 1156bd1f..f9d74c4c 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/types.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/types.ts @@ -122,6 +122,7 @@ export type PreparedBarXSeries = { style: BaseTextStyle; allowOverlap: boolean; padding: number; + html: boolean; }; yAxis: number; } & BasePreparedSeries; @@ -137,6 +138,7 @@ export type PreparedBarYSeries = { style: BaseTextStyle; maxHeight: number; maxWidth: number; + html: boolean; }; } & BasePreparedSeries; @@ -181,6 +183,7 @@ export type PreparedLineSeries = { style: BaseTextStyle; padding: number; allowOverlap: boolean; + html: boolean; }; marker: { states: { @@ -218,6 +221,7 @@ export type PreparedAreaSeries = { style: BaseTextStyle; padding: number; allowOverlap: boolean; + html: boolean; }; marker: { states: { @@ -248,6 +252,7 @@ export type PreparedTreemapSeries = { style: BaseTextStyle; padding: number; allowOverlap: boolean; + html: boolean; }; layoutAlgorithm: `${LayoutAlgorithm}`; } & BasePreparedSeries & @@ -261,6 +266,7 @@ export type PreparedWaterfallSeries = { style: BaseTextStyle; allowOverlap: boolean; padding: number; + html: boolean; }; positiveColor: string; negativeColor: string; diff --git a/src/plugins/d3/renderer/hooks/useShapes/HtmlLayer.tsx b/src/plugins/d3/renderer/hooks/useShapes/HtmlLayer.tsx new file mode 100644 index 00000000..3f1cd486 --- /dev/null +++ b/src/plugins/d3/renderer/hooks/useShapes/HtmlLayer.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import {Portal} from '@gravity-ui/uikit'; + +import {HtmlItem} from '../../types'; + +type Props = { + htmlLayout: HTMLElement | null; + items: HtmlItem[]; +}; + +export const HtmlLayer = (props: Props) => { + const {items, htmlLayout} = props; + + if (!htmlLayout) { + return null; + } + + return ( + + {items.map((item, index) => { + return ( +
+ ); + })} + + ); +}; diff --git a/src/plugins/d3/renderer/hooks/useShapes/area/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/area/index.tsx index 9f02dafd..4a457919 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/area/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/area/index.tsx @@ -7,8 +7,10 @@ import get from 'lodash/get'; import type {TooltipDataChunkArea} from '../../../../../../types'; import {block} from '../../../../../../utils/cn'; import type {LabelData} from '../../../types'; +import {HtmlItem} from '../../../types'; import {filterOverlappingLabels} from '../../../utils'; import type {PreparedSeriesOptions} from '../../useSeries/types'; +import {HtmlLayer} from '../HtmlLayer'; import { getMarkerHaloVisibility, getMarkerVisibility, @@ -27,13 +29,21 @@ type Args = { dispatcher: Dispatch; preparedData: PreparedAreaData[]; seriesOptions: PreparedSeriesOptions; + htmlLayout: HTMLElement | null; }; export const AreaSeriesShapes = (args: Args) => { - const {dispatcher, preparedData, seriesOptions} = args; + const {dispatcher, preparedData, seriesOptions, htmlLayout} = args; const ref = React.useRef(null); + const htmlItems = React.useMemo(() => { + return preparedData.reduce((result, d) => { + result.push(...d.htmlElements); + return result; + }, []); + }, [preparedData]); + React.useEffect(() => { if (!ref.current) { return () => {}; @@ -192,5 +202,10 @@ export const AreaSeriesShapes = (args: Args) => { }; }, [dispatcher, preparedData, seriesOptions]); - return ; + return ( + + + + + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts index 85a554f1..9ef8df60 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts @@ -1,7 +1,7 @@ import {group, sort} from 'd3'; import type {AreaSeriesData} from '../../../../../../types'; -import type {LabelData} from '../../../types'; +import type {HtmlItem, LabelData} from '../../../types'; import {getDataCategoryValue, getLabelsSize, getLeftPosition} from '../../../utils'; import type {ChartScale} from '../../useAxisScales'; import type {PreparedAxis} from '../../useChartOptions/types'; @@ -13,7 +13,7 @@ import type {MarkerData, PointData, PreparedAreaData} from './types'; function getLabelData(point: PointData, series: PreparedAreaSeries, xMax: number) { const text = String(point.data.label || point.data.y); const style = series.dataLabels.style; - const size = getLabelsSize({labels: [text], style}); + const size = getLabelsSize({labels: [text], style, html: series.dataLabels.html}); const labelData: LabelData = { text, @@ -32,7 +32,7 @@ function getLabelData(point: PointData, series: PreparedAreaSeries, xMax: number } else { const right = left + labelData.size.width; if (right > xMax) { - labelData.x = labelData.x - xMax - right; + labelData.x = labelData.x - (right - xMax); } } @@ -132,8 +132,22 @@ export const prepareAreaData = (args: { }, []); let labels: LabelData[] = []; + const htmlElements: HtmlItem[] = []; + if (s.dataLabels.enabled) { - labels = points.map((p) => getLabelData(p, s, xMax)); + const labelItems = points.map((p) => getLabelData(p, s, xMax)); + if (s.dataLabels.html) { + const htmlLabels = labelItems.map((l) => { + return { + x: l.x - l.size.width / 2, + y: l.y, + content: l.text, + }; + }); + htmlElements.push(...htmlLabels); + } else { + labels = labelItems; + } } let markers: MarkerData[] = []; @@ -156,6 +170,7 @@ export const prepareAreaData = (args: { hovered: false, active: true, id: s.id, + htmlElements, }); return acc; diff --git a/src/plugins/d3/renderer/hooks/useShapes/area/types.ts b/src/plugins/d3/renderer/hooks/useShapes/area/types.ts index 98ad7434..065c8d1a 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/area/types.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/area/types.ts @@ -1,5 +1,5 @@ import {AreaSeriesData} from '../../../../../../types'; -import {LabelData} from '../../../types'; +import {HtmlItem, LabelData} from '../../../types'; import {PreparedAreaSeries} from '../../useSeries/types'; export type PointData = { @@ -27,4 +27,5 @@ export type PreparedAreaData = { hovered: boolean; active: boolean; labels: LabelData[]; + htmlElements: HtmlItem[]; }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-x/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/bar-x/index.tsx index 152cff96..05465181 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-x/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-x/index.tsx @@ -5,9 +5,10 @@ import type {Dispatch} from 'd3'; import get from 'lodash/get'; import {block} from '../../../../../../utils/cn'; -import {LabelData} from '../../../types'; +import {HtmlItem, LabelData} from '../../../types'; import {filterOverlappingLabels} from '../../../utils'; import type {PreparedSeriesOptions} from '../../useSeries/types'; +import {HtmlLayer} from '../HtmlLayer'; import type {PreparedBarXData} from './types'; @@ -20,13 +21,21 @@ type Args = { dispatcher: Dispatch; preparedData: PreparedBarXData[]; seriesOptions: PreparedSeriesOptions; + htmlLayout: HTMLElement | null; }; export const BarXSeriesShapes = (args: Args) => { - const {dispatcher, preparedData, seriesOptions} = args; + const {dispatcher, preparedData, seriesOptions, htmlLayout} = args; const ref = React.useRef(null); + const htmlItems = React.useMemo(() => { + return preparedData.reduce((result, d) => { + result.push(...d.htmlElements); + return result; + }, []); + }, [preparedData]); + React.useEffect(() => { if (!ref.current) { return () => {}; @@ -120,5 +129,10 @@ export const BarXSeriesShapes = (args: Args) => { }; }, [dispatcher, preparedData, seriesOptions]); - return ; + return ( + + + + + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts index 37cc1e6b..a58bd0dd 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts @@ -19,17 +19,23 @@ function getLabelData(d: PreparedBarXData): LabelData | undefined { const text = String(d.data.label || d.data.y); const style = d.series.dataLabels.style; - const {maxHeight: height, maxWidth: width} = getLabelsSize({labels: [text], style}); + const html = d.series.dataLabels.html; + const {maxHeight: height, maxWidth: width} = getLabelsSize({ + labels: [text], + style, + html, + }); let y = Math.max(height, d.y - d.series.dataLabels.padding); if (d.series.dataLabels.inside) { y = d.y + d.height / 2; } + const x = d.x + d.width / 2; return { text, - x: d.x + d.width / 2, - y, + x: html ? x - width / 2 : x, + y: html ? y - height : y, style, size: {width, height}, textAnchor: 'middle', @@ -165,9 +171,19 @@ export const prepareBarXData = (args: { opacity: get(yValue.data, 'opacity', null), data: yValue.data, series: yValue.series, + htmlElements: [], }; - barData.label = getLabelData(barData); + const label = getLabelData(barData); + if (yValue.series.dataLabels.html && label) { + barData.htmlElements.push({ + x: label.x, + y: label.y, + content: label.text, + }); + } else { + barData.label = getLabelData(barData); + } stackItems.push(barData); diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-x/types.ts b/src/plugins/d3/renderer/hooks/useShapes/bar-x/types.ts index 7ede99bc..4b2431d5 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-x/types.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-x/types.ts @@ -1,5 +1,5 @@ import {TooltipDataChunkBarX} from '../../../../../../types'; -import {LabelData} from '../../../types'; +import {HtmlItem, LabelData} from '../../../types'; import {PreparedBarXSeries} from '../../useSeries/types'; export type PreparedBarXData = Omit & { @@ -10,4 +10,5 @@ export type PreparedBarXData = Omit & { opacity: number | null; series: PreparedBarXSeries; label?: LabelData; + htmlElements: HtmlItem[]; }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-y/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/bar-y/index.tsx index 7332c0ce..141cb185 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-y/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-y/index.tsx @@ -5,25 +5,33 @@ import type {Dispatch} from 'd3'; import get from 'lodash/get'; import {block} from '../../../../../../utils/cn'; +import {HtmlItem, LabelData} from '../../../types'; import type {PreparedSeriesOptions} from '../../useSeries/types'; +import {HtmlLayer} from '../HtmlLayer'; import type {PreparedBarYData} from './types'; export {prepareBarYData} from './prepare-data'; -const DEFAULT_LABEL_PADDING = 7; - const b = block('d3-bar-y'); type Args = { dispatcher: Dispatch; preparedData: PreparedBarYData[]; seriesOptions: PreparedSeriesOptions; + htmlLayout: HTMLElement | null; }; export const BarYSeriesShapes = (args: Args) => { - const {dispatcher, preparedData, seriesOptions} = args; + const {dispatcher, preparedData, seriesOptions, htmlLayout} = args; const ref = React.useRef(null); + const htmlItems = React.useMemo(() => { + return preparedData.reduce((result, d) => { + result.push(...d.htmlElements); + return result; + }, []); + }, [preparedData]); + React.useEffect(() => { if (!ref.current) { return () => {}; @@ -44,33 +52,24 @@ export const BarYSeriesShapes = (args: Args) => { .attr('opacity', (d) => d.data.opacity || null) .attr('cursor', (d) => d.series.cursor); - const dataLabels = preparedData.filter((d) => d.series.dataLabels.enabled); + const dataLabels = preparedData.reduce((acc, d) => { + if (d.label) { + acc.push(d.label); + } + return acc; + }, []); const labelSelection = svgElement .selectAll('text') .data(dataLabels) .join('text') - .text((d) => String(d.data.label || d.data.x)) + .text((d) => d.text) .attr('class', b('label')) - .attr('x', (d) => { - if (d.series.dataLabels.inside) { - return d.x + d.width / 2; - } - - return d.x + d.width + DEFAULT_LABEL_PADDING; - }) - .attr('y', (d) => { - return d.y + d.height / 2 + d.series.dataLabels.maxHeight / 2; - }) - .attr('text-anchor', (d) => { - if (d.series.dataLabels.inside) { - return 'middle'; - } - - return 'right'; - }) - .style('font-size', (d) => d.series.dataLabels.style.fontSize) - .style('font-weight', (d) => d.series.dataLabels.style.fontWeight || null) - .style('fill', (d) => d.series.dataLabels.style.fontColor || null); + .attr('x', (d) => d.x) + .attr('y', (d) => d.y) + .attr('text-anchor', (d) => d.textAnchor) + .style('font-size', (d) => d.style.fontSize) + .style('font-weight', (d) => d.style.fontWeight || null) + .style('fill', (d) => d.style.fontColor || null); const hoverOptions = get(seriesOptions, 'bar-y.states.hover'); const inactiveOptions = get(seriesOptions, 'bar-y.states.inactive'); @@ -98,7 +97,7 @@ export const BarYSeriesShapes = (args: Args) => { if (inactiveOptions?.enabled) { const hoveredSeries = data?.map((d) => d.series.id); - const newOpacity = (d: PreparedBarYData) => { + const newOpacity = (d: PreparedBarYData | LabelData) => { if (hoveredSeries?.length && !hoveredSeries.includes(d.series.id)) { return inactiveOptions.opacity || null; } @@ -115,5 +114,10 @@ export const BarYSeriesShapes = (args: Args) => { }; }, [dispatcher, preparedData, seriesOptions]); - return ; + return ( + + + + + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts index cfd982a5..29aacf93 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts @@ -3,7 +3,8 @@ import type {ScaleBand, ScaleLinear, ScaleTime} from 'd3'; import get from 'lodash/get'; import type {BarYSeriesData} from '../../../../../../types'; -import {getDataCategoryValue} from '../../../utils'; +import {LabelData} from '../../../types'; +import {getDataCategoryValue, getLabelsSize} from '../../../utils'; import type {ChartScale} from '../../useAxisScales'; import type {PreparedAxis} from '../../useChartOptions/types'; import type {PreparedBarYSeries, PreparedSeriesOptions} from '../../useSeries/types'; @@ -11,6 +12,8 @@ import {MIN_BAR_GAP, MIN_BAR_GROUP_GAP, MIN_BAR_WIDTH} from '../constants'; import type {PreparedBarYData} from './types'; +const DEFAULT_LABEL_PADDING = 7; + function groupByYValue(series: PreparedBarYSeries[], yAxis: PreparedAxis[]) { const data: Record< string | number, @@ -68,6 +71,43 @@ function getBandWidth(series: PreparedBarYSeries[], yAxis: PreparedAxis[], yScal return bandWidth; } +function setLabel(prepared: PreparedBarYData) { + const dataLabels = prepared.series.dataLabels; + if (!dataLabels.enabled) { + return; + } + + const data = prepared.data; + const content = String(data.label || data.x); + const {maxHeight: height, maxWidth: width} = getLabelsSize({ + labels: [content], + style: dataLabels.style, + html: dataLabels.html, + }); + const x = dataLabels.inside + ? prepared.x + prepared.width / 2 + : prepared.x + prepared.width + DEFAULT_LABEL_PADDING; + const y = prepared.y + prepared.height / 2; + + if (dataLabels.html) { + prepared.htmlElements.push({ + x, + y: y - height / 2, + content, + }); + } else { + prepared.label = { + x, + y: y + height / 2, + text: content, + textAnchor: dataLabels.inside ? 'middle' : 'right', + style: dataLabels.style, + series: prepared.series, + size: {width, height}, + } as LabelData; + } +} + export const prepareBarYData = (args: { series: PreparedBarYSeries[]; seriesOptions: PreparedSeriesOptions; @@ -146,7 +186,7 @@ export const prepareBarYData = (args: { const width = xValue > 0 ? xLinearScale(xValue) - base : base - xLinearScale(xValue); - stackItems.push({ + const item: PreparedBarYData = { x: xValue > 0 ? stackSum : stackSum - width, y, width, @@ -155,8 +195,10 @@ export const prepareBarYData = (args: { opacity: get(data, 'opacity', null), data, series: s, - }); + htmlElements: [], + }; + stackItems.push(item); stackSum += width + 1; }); @@ -175,5 +217,9 @@ export const prepareBarYData = (args: { }); }); + result.forEach((d) => { + setLabel(d); + }); + return result; }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-y/types.ts b/src/plugins/d3/renderer/hooks/useShapes/bar-y/types.ts index 49d4ff37..4b5bfa12 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-y/types.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-y/types.ts @@ -1,4 +1,5 @@ import {TooltipDataChunkBarX} from '../../../../../../types'; +import {HtmlItem, LabelData} from '../../../types'; import {PreparedBarYSeries} from '../../useSeries/types'; export type PreparedBarYData = Omit & { @@ -9,4 +10,6 @@ export type PreparedBarYData = Omit & { color: string; opacity: number | null; series: PreparedBarYSeries; + label?: LabelData; + htmlElements: HtmlItem[]; }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/index.tsx index fdaf2f82..c1582253 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/index.tsx @@ -104,6 +104,7 @@ export const useShapes = (args: Args) => { dispatcher={dispatcher} seriesOptions={seriesOptions} preparedData={preparedData} + htmlLayout={htmlLayout} />, ); shapesData.push(...preparedData); @@ -126,6 +127,7 @@ export const useShapes = (args: Args) => { dispatcher={dispatcher} seriesOptions={seriesOptions} preparedData={preparedData} + htmlLayout={htmlLayout} />, ); shapesData.push(...preparedData); @@ -148,6 +150,7 @@ export const useShapes = (args: Args) => { dispatcher={dispatcher} seriesOptions={seriesOptions} preparedData={preparedData} + htmlLayout={htmlLayout} />, ); shapesData.push(...preparedData); @@ -170,6 +173,7 @@ export const useShapes = (args: Args) => { dispatcher={dispatcher} seriesOptions={seriesOptions} preparedData={preparedData} + htmlLayout={htmlLayout} />, ); shapesData.push(...preparedData); @@ -192,6 +196,7 @@ export const useShapes = (args: Args) => { dispatcher={dispatcher} seriesOptions={seriesOptions} preparedData={preparedData} + htmlLayout={htmlLayout} />, ); shapesData.push(...preparedData); @@ -213,6 +218,7 @@ export const useShapes = (args: Args) => { dispatcher={dispatcher} preparedData={preparedData} seriesOptions={seriesOptions} + htmlLayout={htmlLayout} />, ); shapesData.push(...preparedData); @@ -251,6 +257,7 @@ export const useShapes = (args: Args) => { dispatcher={dispatcher} preparedData={preparedData} seriesOptions={seriesOptions} + htmlLayout={htmlLayout} />, ); shapesData.push(preparedData as unknown as ShapeData); diff --git a/src/plugins/d3/renderer/hooks/useShapes/line/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/line/index.tsx index 6f3dc85e..ce5553bc 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/line/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/line/index.tsx @@ -7,8 +7,10 @@ import get from 'lodash/get'; import type {TooltipDataChunkLine} from '../../../../../../types'; import {block} from '../../../../../../utils/cn'; import type {LabelData} from '../../../types'; +import {HtmlItem} from '../../../types'; import {filterOverlappingLabels} from '../../../utils'; import type {PreparedSeriesOptions} from '../../useSeries/types'; +import {HtmlLayer} from '../HtmlLayer'; import { getMarkerHaloVisibility, getMarkerVisibility, @@ -27,12 +29,19 @@ type Args = { dispatcher: Dispatch; preparedData: PreparedLineData[]; seriesOptions: PreparedSeriesOptions; + htmlLayout: HTMLElement | null; }; export const LineSeriesShapes = (args: Args) => { - const {dispatcher, preparedData, seriesOptions} = args; + const {dispatcher, preparedData, seriesOptions, htmlLayout} = args; const ref = React.useRef(null); + const htmlItems = React.useMemo(() => { + return preparedData.reduce((result, d) => { + result.push(...d.htmlElements); + return result; + }, []); + }, [preparedData]); React.useEffect(() => { if (!ref.current) { @@ -179,5 +188,10 @@ export const LineSeriesShapes = (args: Args) => { }; }, [dispatcher, preparedData, seriesOptions]); - return ; + return ( + + + + + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/line/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/line/prepare-data.ts index 0b187a10..777b95cc 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/line/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/line/prepare-data.ts @@ -1,4 +1,4 @@ -import type {LabelData} from '../../../types'; +import type {HtmlItem, LabelData} from '../../../types'; import {getLabelsSize, getLeftPosition} from '../../../utils'; import type {ChartScale} from '../../useAxisScales'; import type {PreparedAxis} from '../../useChartOptions/types'; @@ -37,6 +37,17 @@ function getLabelData(point: PointData, series: PreparedLineSeries, xMax: number return labelData; } +function getHtmlLabel(point: PointData, series: PreparedLineSeries, xMax: number): HtmlItem { + const content = String(point.data.label || point.data.y); + const size = getLabelsSize({labels: [content], html: true}); + + return { + x: Math.min(xMax - size.maxWidth, Math.max(0, point.x)), + y: Math.max(0, point.y - series.dataLabels.padding - size.maxHeight), + content, + }; +} + export const prepareLineData = (args: { series: PreparedLineSeries[]; xAxis: PreparedAxis; @@ -62,9 +73,14 @@ export const prepareLineData = (args: { series: s, })); + const htmlElements: HtmlItem[] = []; let labels: LabelData[] = []; if (s.dataLabels.enabled) { - labels = points.map((p) => getLabelData(p, s, xMax)); + if (s.dataLabels.html) { + htmlElements.push(...points.map((p) => getHtmlLabel(p, s, xMax))); + } else { + labels = points.map((p) => getLabelData(p, s, xMax)); + } } let markers: MarkerData[] = []; @@ -89,6 +105,7 @@ export const prepareLineData = (args: { dashStyle: s.dashStyle, linecap: s.linecap, opacity: s.opacity, + htmlElements, }; acc.push(result); diff --git a/src/plugins/d3/renderer/hooks/useShapes/line/types.ts b/src/plugins/d3/renderer/hooks/useShapes/line/types.ts index 7611fb23..11fe8133 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/line/types.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/line/types.ts @@ -1,6 +1,6 @@ import {DashStyle, LineCap} from '../../../../../../constants'; import {LineSeriesData} from '../../../../../../types'; -import {LabelData} from '../../../types'; +import {HtmlItem, LabelData} from '../../../types'; import {PreparedLineSeries} from '../../useSeries/types'; export type PointData = { @@ -29,4 +29,5 @@ export type PreparedLineData = { dashStyle: DashStyle; linecap: LineCap; opacity: number | null; + htmlElements: HtmlItem[]; }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/pie/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/pie/index.tsx index e3856299..de594f74 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/pie/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/pie/index.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import {Portal} from '@gravity-ui/uikit'; import {arc, color, select} from 'd3'; import type {BaseType, Dispatch, PieArcDatum} from 'd3'; import get from 'lodash/get'; @@ -10,6 +9,7 @@ import {block} from '../../../../../../utils/cn'; import {HtmlItem} from '../../../types'; import {setEllipsisForOverflowTexts} from '../../../utils'; import {PreparedSeriesOptions} from '../../useSeries/types'; +import {HtmlLayer} from '../HtmlLayer'; import {PreparedLineData} from '../line/types'; import {setActiveState} from '../utils'; @@ -229,19 +229,7 @@ export function PieSeriesShapes(args: PreparePieSeriesArgs) { return ( - {htmlLayout && ( - - {htmlItems.map((item, index) => { - return ( -
- ); - })} - - )} + ); } diff --git a/src/plugins/d3/renderer/hooks/useShapes/pie/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/pie/prepare-data.ts index 46791600..7bd2b000 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/pie/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/pie/prepare-data.ts @@ -185,7 +185,6 @@ export function preparePieData(args: Args): PreparedPieData[] { active: true, segment: relatedSegment.data, angle: midAngle, - html: shouldUseHtml, }; let overlap = false; diff --git a/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx index e9fdecb6..0e06e4d5 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx @@ -6,7 +6,9 @@ import get from 'lodash/get'; import {TooltipDataChunkScatter} from '../../../../../../types'; import {block} from '../../../../../../utils/cn'; +import {HtmlItem} from '../../../types'; import {PreparedSeriesOptions} from '../../useSeries/types'; +import {HtmlLayer} from '../HtmlLayer'; import { getMarkerHaloVisibility, renderMarker, @@ -23,13 +25,20 @@ type ScatterSeriesShapeProps = { dispatcher: Dispatch; preparedData: PreparedScatterData[]; seriesOptions: PreparedSeriesOptions; + htmlLayout: HTMLElement | null; }; const b = block('d3-scatter'); export function ScatterSeriesShape(props: ScatterSeriesShapeProps) { - const {dispatcher, preparedData, seriesOptions} = props; + const {dispatcher, preparedData, seriesOptions, htmlLayout} = props; const ref = React.useRef(null); + const htmlItems = React.useMemo(() => { + return preparedData.reduce((result, d) => { + result.push(...d.htmlElements); + return result; + }, []); + }, [preparedData]); React.useEffect(() => { if (!ref.current) { @@ -99,5 +108,10 @@ export function ScatterSeriesShape(props: ScatterSeriesShapeProps) { }; }, [dispatcher, preparedData, seriesOptions]); - return ; + return ( + + + + + ); } diff --git a/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts index df76c256..30716b1a 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.ts @@ -41,6 +41,7 @@ export const prepareScatterData = (args: { }, hovered: false, active: true, + htmlElements: [], }); }); diff --git a/src/plugins/d3/renderer/hooks/useShapes/scatter/types.ts b/src/plugins/d3/renderer/hooks/useShapes/scatter/types.ts index b0363fa2..78767fa1 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/scatter/types.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/scatter/types.ts @@ -1,4 +1,5 @@ import {ScatterSeriesData} from '../../../../../../types'; +import {HtmlItem} from '../../../types'; import {PreparedScatterSeries} from '../../useSeries/types'; type PointData = { @@ -13,6 +14,7 @@ export type MarkerData = { point: PointData; active: boolean; hovered: boolean; + htmlElements: HtmlItem[]; }; export type PreparedScatterData = MarkerData; diff --git a/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx index a6626335..46d6c9dd 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx @@ -8,6 +8,7 @@ import type {TooltipDataChunkTreemap, TreemapSeriesData} from '../../../../../.. import {block} from '../../../../../../utils/cn'; import {setEllipsisForOverflowTexts} from '../../../utils'; import {PreparedSeriesOptions} from '../../useSeries/types'; +import {HtmlLayer} from '../HtmlLayer'; import type {PreparedTreemapData, TreemapLabelData} from './types'; @@ -17,11 +18,15 @@ type ShapeProps = { dispatcher: Dispatch; preparedData: PreparedTreemapData; seriesOptions: PreparedSeriesOptions; + htmlLayout: HTMLElement | null; }; export const TreemapSeriesShape = (props: ShapeProps) => { - const {dispatcher, preparedData, seriesOptions} = props; + const {dispatcher, preparedData, seriesOptions, htmlLayout} = props; const ref = React.useRef(null); + const htmlItems = React.useMemo(() => { + return preparedData.htmlElements ?? []; + }, [preparedData]); React.useEffect(() => { if (!ref.current) { @@ -118,5 +123,10 @@ export const TreemapSeriesShape = (props: ShapeProps) => { }; }, [dispatcher, preparedData, seriesOptions]); - return ; + return ( + + + + + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts index 6aded393..f05dbf0f 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts @@ -74,7 +74,7 @@ export function prepareTreemapData(args: { const leaves = root.leaves(); const labelData: TreemapLabelData[] = series.dataLabels?.enabled ? getLabelData(leaves) : []; - return {labelData, leaves, series}; + return {labelData, leaves, series, htmlElements: []}; } function getSeriesDataWithRootNode(series: PreparedTreemapSeries) { diff --git a/src/plugins/d3/renderer/hooks/useShapes/treemap/types.ts b/src/plugins/d3/renderer/hooks/useShapes/treemap/types.ts index d5fab99d..180a69f2 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/treemap/types.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/treemap/types.ts @@ -1,6 +1,7 @@ import type {HierarchyRectangularNode} from 'd3'; import type {TreemapSeriesData} from '../../../../../../types'; +import {HtmlItem} from '../../../types'; import type {PreparedTreemapSeries} from '../../useSeries/types'; export type TreemapLabelData = { @@ -15,4 +16,5 @@ export type PreparedTreemapData = { labelData: TreemapLabelData[]; leaves: HierarchyRectangularNode>[]; series: PreparedTreemapSeries; + htmlElements: HtmlItem[]; }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/waterfall/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/waterfall/index.tsx index 77bd9b69..048a7d26 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/waterfall/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/waterfall/index.tsx @@ -7,8 +7,10 @@ import get from 'lodash/get'; import {DashStyle} from '../../../../../../constants'; import {block} from '../../../../../../utils/cn'; import type {LabelData} from '../../../types'; +import {HtmlItem} from '../../../types'; import {filterOverlappingLabels, getWaterfallPointColor} from '../../../utils'; import type {PreparedSeriesOptions} from '../../useSeries/types'; +import {HtmlLayer} from '../HtmlLayer'; import {getLineDashArray} from '../utils'; import type {PreparedWaterfallData} from './types'; @@ -22,13 +24,20 @@ type Args = { dispatcher: Dispatch; preparedData: PreparedWaterfallData[]; seriesOptions: PreparedSeriesOptions; + htmlLayout: HTMLElement | null; }; export const WaterfallSeriesShapes = (args: Args) => { - const {dispatcher, preparedData, seriesOptions} = args; + const {dispatcher, preparedData, seriesOptions, htmlLayout} = args; const ref = React.useRef(null); const connectorSelector = `.${b('connector')}`; + const htmlItems = React.useMemo(() => { + return preparedData.reduce((result, d) => { + result.push(...d.htmlElements); + return result; + }, []); + }, [preparedData]); React.useEffect(() => { if (!ref.current) { @@ -153,5 +162,10 @@ export const WaterfallSeriesShapes = (args: Args) => { }; }, [dispatcher, preparedData, seriesOptions]); - return ; + return ( + + + + + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/waterfall/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/waterfall/prepare-data.ts index 48f3cf1b..2643dcc6 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/waterfall/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/waterfall/prepare-data.ts @@ -169,6 +169,7 @@ export const prepareWaterfallData = (args: { data: item.data, series: item.series, subTotal: totalValue, + htmlElements: [], }; preparedData.label = getLabelData(preparedData, plotHeight); diff --git a/src/plugins/d3/renderer/hooks/useShapes/waterfall/types.ts b/src/plugins/d3/renderer/hooks/useShapes/waterfall/types.ts index ce25813b..0f38e7bb 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/waterfall/types.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/waterfall/types.ts @@ -1,5 +1,5 @@ import {WaterfallSeriesData} from '../../../../../../types'; -import {LabelData} from '../../../types'; +import {HtmlItem, LabelData} from '../../../types'; import {PreparedWaterfallSeries} from '../../useSeries/types'; export type PreparedWaterfallData = { @@ -12,4 +12,5 @@ export type PreparedWaterfallData = { data: WaterfallSeriesData; label?: LabelData; subTotal: number; + htmlElements: HtmlItem[]; }; diff --git a/src/plugins/d3/renderer/types/index.ts b/src/plugins/d3/renderer/types/index.ts index bd0708bb..c11a6014 100644 --- a/src/plugins/d3/renderer/types/index.ts +++ b/src/plugins/d3/renderer/types/index.ts @@ -9,7 +9,6 @@ export type LabelData = { textAnchor: 'start' | 'end' | 'middle'; series: {id: string}; active?: boolean; - html?: boolean; }; export type HtmlItem = { diff --git a/src/plugins/d3/renderer/utils/text.ts b/src/plugins/d3/renderer/utils/text.ts index 71ec8b05..40492097 100644 --- a/src/plugins/d3/renderer/utils/text.ts +++ b/src/plugins/d3/renderer/utils/text.ts @@ -135,13 +135,22 @@ export function getLabelsSize({ let labelWrapper: HTMLElement | null; if (html) { labelWrapper = container.append('div').style('position', 'absolute').node(); - labels.forEach((l) => { - labelWrapper?.insertAdjacentHTML('beforeend', l); - }); - - const rect = labelWrapper?.getBoundingClientRect(); - result.maxWidth = rect?.width ?? 0; - result.maxHeight = rect?.height ?? 0; + const {height, width} = labels.reduce( + (acc, l) => { + if (labelWrapper) { + labelWrapper.innerHTML = l; + } + const rect = labelWrapper?.getBoundingClientRect(); + return { + width: Math.max(acc.width, rect?.width ?? 0), + height: Math.max(acc.height, rect?.height ?? 0), + }; + }, + {height: 0, width: 0}, + ); + + result.maxWidth = width; + result.maxHeight = height; } else { const svg = container.append('svg'); const textSelection = renderLabels(svg, {labels, style}); diff --git a/src/types/widget-data/bar-x.ts b/src/types/widget-data/bar-x.ts index fd859e7f..379d5c59 100644 --- a/src/types/widget-data/bar-x.ts +++ b/src/types/widget-data/bar-x.ts @@ -52,14 +52,15 @@ export type BarXSeries = BaseSeries & { * @default true * */ grouping?: boolean; - dataLabels?: ChartKitWidgetSeriesOptions['dataLabels'] & { - /** - * Whether to align the data label inside or outside the box - * - * @default false - * */ - inside?: boolean; - }; + dataLabels?: BaseSeries['dataLabels'] & + ChartKitWidgetSeriesOptions['dataLabels'] & { + /** + * Whether to align the data label inside or outside the box + * + * @default false + * */ + inside?: boolean; + }; /** Individual series legend options. Has higher priority than legend options in widget data */ legend?: ChartKitWidgetLegend & { symbol?: RectLegendSymbolOptions; From c86ff6bb64c5f69947b7eb3203fe98bcb16379f4 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Thu, 10 Oct 2024 10:46:21 +0300 Subject: [PATCH 2/2] fix --- .eslintrc | 3 ++- .../renderer/hooks/useSeries/prepare-bar-y.ts | 2 -- .../d3/renderer/hooks/useShapes/HtmlLayer.tsx | 17 ++++++++++++++--- .../d3/renderer/hooks/useShapes/area/index.tsx | 10 +--------- .../d3/renderer/hooks/useShapes/bar-x/index.tsx | 11 ++--------- .../d3/renderer/hooks/useShapes/bar-y/index.tsx | 11 ++--------- .../d3/renderer/hooks/useShapes/line/index.tsx | 9 +-------- .../d3/renderer/hooks/useShapes/pie/index.tsx | 10 +--------- .../renderer/hooks/useShapes/scatter/index.tsx | 9 +-------- .../renderer/hooks/useShapes/treemap/index.tsx | 5 +---- .../hooks/useShapes/waterfall/index.tsx | 9 +-------- src/plugins/d3/renderer/types/index.ts | 4 ++++ 12 files changed, 30 insertions(+), 70 deletions(-) diff --git a/.eslintrc b/.eslintrc index 3067068c..34a0d261 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,7 +6,8 @@ "@gravity-ui/eslint-config/import-order" ], "rules": { - "valid-jsdoc": 0 + "valid-jsdoc": 0, + "no-console": ["error", {"allow": ["warn", "error"]}] }, "root": true, "overrides": [{ diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-y.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-y.ts index 6c4c1734..e0c70ac0 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-y.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-y.ts @@ -27,8 +27,6 @@ function prepareDataLabels(series: BarYSeries) { }); const inside = series.stacking === 'percent' ? true : get(series, 'dataLabels.inside', false); - console.log('prepareDataLabels', {maxWidth}); - return { enabled, inside, diff --git a/src/plugins/d3/renderer/hooks/useShapes/HtmlLayer.tsx b/src/plugins/d3/renderer/hooks/useShapes/HtmlLayer.tsx index 3f1cd486..3560eff8 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/HtmlLayer.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/HtmlLayer.tsx @@ -2,15 +2,26 @@ import React from 'react'; import {Portal} from '@gravity-ui/uikit'; -import {HtmlItem} from '../../types'; +import {HtmlItem, ShapeDataWithHtmlItems} from '../../types'; type Props = { htmlLayout: HTMLElement | null; - items: HtmlItem[]; + preparedData: ShapeDataWithHtmlItems | ShapeDataWithHtmlItems[]; }; export const HtmlLayer = (props: Props) => { - const {items, htmlLayout} = props; + const {htmlLayout, preparedData} = props; + + const items = React.useMemo(() => { + if (Array.isArray(preparedData)) { + return preparedData.reduce((result, d) => { + result.push(...d.htmlElements); + return result; + }, []); + } else { + return preparedData.htmlElements; + } + }, [preparedData]); if (!htmlLayout) { return null; diff --git a/src/plugins/d3/renderer/hooks/useShapes/area/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/area/index.tsx index 4a457919..a820f758 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/area/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/area/index.tsx @@ -7,7 +7,6 @@ import get from 'lodash/get'; import type {TooltipDataChunkArea} from '../../../../../../types'; import {block} from '../../../../../../utils/cn'; import type {LabelData} from '../../../types'; -import {HtmlItem} from '../../../types'; import {filterOverlappingLabels} from '../../../utils'; import type {PreparedSeriesOptions} from '../../useSeries/types'; import {HtmlLayer} from '../HtmlLayer'; @@ -37,13 +36,6 @@ export const AreaSeriesShapes = (args: Args) => { const ref = React.useRef(null); - const htmlItems = React.useMemo(() => { - return preparedData.reduce((result, d) => { - result.push(...d.htmlElements); - return result; - }, []); - }, [preparedData]); - React.useEffect(() => { if (!ref.current) { return () => {}; @@ -205,7 +197,7 @@ export const AreaSeriesShapes = (args: Args) => { return ( - + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-x/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/bar-x/index.tsx index 05465181..9e232dd0 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-x/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-x/index.tsx @@ -5,7 +5,7 @@ import type {Dispatch} from 'd3'; import get from 'lodash/get'; import {block} from '../../../../../../utils/cn'; -import {HtmlItem, LabelData} from '../../../types'; +import {LabelData} from '../../../types'; import {filterOverlappingLabels} from '../../../utils'; import type {PreparedSeriesOptions} from '../../useSeries/types'; import {HtmlLayer} from '../HtmlLayer'; @@ -29,13 +29,6 @@ export const BarXSeriesShapes = (args: Args) => { const ref = React.useRef(null); - const htmlItems = React.useMemo(() => { - return preparedData.reduce((result, d) => { - result.push(...d.htmlElements); - return result; - }, []); - }, [preparedData]); - React.useEffect(() => { if (!ref.current) { return () => {}; @@ -132,7 +125,7 @@ export const BarXSeriesShapes = (args: Args) => { return ( - + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-y/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/bar-y/index.tsx index 141cb185..1923f64b 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-y/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-y/index.tsx @@ -5,7 +5,7 @@ import type {Dispatch} from 'd3'; import get from 'lodash/get'; import {block} from '../../../../../../utils/cn'; -import {HtmlItem, LabelData} from '../../../types'; +import {LabelData} from '../../../types'; import type {PreparedSeriesOptions} from '../../useSeries/types'; import {HtmlLayer} from '../HtmlLayer'; @@ -25,13 +25,6 @@ export const BarYSeriesShapes = (args: Args) => { const {dispatcher, preparedData, seriesOptions, htmlLayout} = args; const ref = React.useRef(null); - const htmlItems = React.useMemo(() => { - return preparedData.reduce((result, d) => { - result.push(...d.htmlElements); - return result; - }, []); - }, [preparedData]); - React.useEffect(() => { if (!ref.current) { return () => {}; @@ -117,7 +110,7 @@ export const BarYSeriesShapes = (args: Args) => { return ( - + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/line/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/line/index.tsx index ce5553bc..714a09a7 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/line/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/line/index.tsx @@ -7,7 +7,6 @@ import get from 'lodash/get'; import type {TooltipDataChunkLine} from '../../../../../../types'; import {block} from '../../../../../../utils/cn'; import type {LabelData} from '../../../types'; -import {HtmlItem} from '../../../types'; import {filterOverlappingLabels} from '../../../utils'; import type {PreparedSeriesOptions} from '../../useSeries/types'; import {HtmlLayer} from '../HtmlLayer'; @@ -36,12 +35,6 @@ export const LineSeriesShapes = (args: Args) => { const {dispatcher, preparedData, seriesOptions, htmlLayout} = args; const ref = React.useRef(null); - const htmlItems = React.useMemo(() => { - return preparedData.reduce((result, d) => { - result.push(...d.htmlElements); - return result; - }, []); - }, [preparedData]); React.useEffect(() => { if (!ref.current) { @@ -191,7 +184,7 @@ export const LineSeriesShapes = (args: Args) => { return ( - + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/pie/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/pie/index.tsx index de594f74..7ad65059 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/pie/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/pie/index.tsx @@ -6,7 +6,6 @@ import get from 'lodash/get'; import {TooltipDataChunkPie} from '../../../../../../types'; import {block} from '../../../../../../utils/cn'; -import {HtmlItem} from '../../../types'; import {setEllipsisForOverflowTexts} from '../../../utils'; import {PreparedSeriesOptions} from '../../useSeries/types'; import {HtmlLayer} from '../HtmlLayer'; @@ -33,13 +32,6 @@ export function PieSeriesShapes(args: PreparePieSeriesArgs) { const {dispatcher, preparedData, seriesOptions, htmlLayout} = args; const ref = React.useRef(null); - const htmlItems = React.useMemo(() => { - return preparedData.reduce((result, d) => { - result.push(...d.htmlElements); - return result; - }, []); - }, [preparedData]); - React.useEffect(() => { if (!ref.current) { return () => {}; @@ -229,7 +221,7 @@ export function PieSeriesShapes(args: PreparePieSeriesArgs) { return ( - + ); } diff --git a/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx index 0e06e4d5..95dedb3e 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/scatter/index.tsx @@ -6,7 +6,6 @@ import get from 'lodash/get'; import {TooltipDataChunkScatter} from '../../../../../../types'; import {block} from '../../../../../../utils/cn'; -import {HtmlItem} from '../../../types'; import {PreparedSeriesOptions} from '../../useSeries/types'; import {HtmlLayer} from '../HtmlLayer'; import { @@ -33,12 +32,6 @@ const b = block('d3-scatter'); export function ScatterSeriesShape(props: ScatterSeriesShapeProps) { const {dispatcher, preparedData, seriesOptions, htmlLayout} = props; const ref = React.useRef(null); - const htmlItems = React.useMemo(() => { - return preparedData.reduce((result, d) => { - result.push(...d.htmlElements); - return result; - }, []); - }, [preparedData]); React.useEffect(() => { if (!ref.current) { @@ -111,7 +104,7 @@ export function ScatterSeriesShape(props: ScatterSeriesShapeProps) { return ( - + ); } diff --git a/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx index 46d6c9dd..b3cc61fa 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx @@ -24,9 +24,6 @@ type ShapeProps = { export const TreemapSeriesShape = (props: ShapeProps) => { const {dispatcher, preparedData, seriesOptions, htmlLayout} = props; const ref = React.useRef(null); - const htmlItems = React.useMemo(() => { - return preparedData.htmlElements ?? []; - }, [preparedData]); React.useEffect(() => { if (!ref.current) { @@ -126,7 +123,7 @@ export const TreemapSeriesShape = (props: ShapeProps) => { return ( - + ); }; diff --git a/src/plugins/d3/renderer/hooks/useShapes/waterfall/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/waterfall/index.tsx index 048a7d26..e8a071b1 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/waterfall/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/waterfall/index.tsx @@ -7,7 +7,6 @@ import get from 'lodash/get'; import {DashStyle} from '../../../../../../constants'; import {block} from '../../../../../../utils/cn'; import type {LabelData} from '../../../types'; -import {HtmlItem} from '../../../types'; import {filterOverlappingLabels, getWaterfallPointColor} from '../../../utils'; import type {PreparedSeriesOptions} from '../../useSeries/types'; import {HtmlLayer} from '../HtmlLayer'; @@ -32,12 +31,6 @@ export const WaterfallSeriesShapes = (args: Args) => { const ref = React.useRef(null); const connectorSelector = `.${b('connector')}`; - const htmlItems = React.useMemo(() => { - return preparedData.reduce((result, d) => { - result.push(...d.htmlElements); - return result; - }, []); - }, [preparedData]); React.useEffect(() => { if (!ref.current) { @@ -165,7 +158,7 @@ export const WaterfallSeriesShapes = (args: Args) => { return ( - + ); }; diff --git a/src/plugins/d3/renderer/types/index.ts b/src/plugins/d3/renderer/types/index.ts index c11a6014..6fc5f49f 100644 --- a/src/plugins/d3/renderer/types/index.ts +++ b/src/plugins/d3/renderer/types/index.ts @@ -16,3 +16,7 @@ export type HtmlItem = { y: number; content: string; }; + +export type ShapeDataWithHtmlItems = { + htmlElements: HtmlItem[]; +};