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/__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..e0c70ac0 100644
--- a/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-y.ts
+++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-bar-y.ts
@@ -18,12 +18,13 @@ 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);
return {
@@ -32,6 +33,7 @@ function prepareDataLabels(series: BarYSeries) {
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..3560eff8
--- /dev/null
+++ b/src/plugins/d3/renderer/hooks/useShapes/HtmlLayer.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+
+import {Portal} from '@gravity-ui/uikit';
+
+import {HtmlItem, ShapeDataWithHtmlItems} from '../../types';
+
+type Props = {
+ htmlLayout: HTMLElement | null;
+ preparedData: ShapeDataWithHtmlItems | ShapeDataWithHtmlItems[];
+};
+
+export const HtmlLayer = (props: 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;
+ }
+
+ 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..a820f758 100644
--- a/src/plugins/d3/renderer/hooks/useShapes/area/index.tsx
+++ b/src/plugins/d3/renderer/hooks/useShapes/area/index.tsx
@@ -9,6 +9,7 @@ import {block} from '../../../../../../utils/cn';
import type {LabelData} from '../../../types';
import {filterOverlappingLabels} from '../../../utils';
import type {PreparedSeriesOptions} from '../../useSeries/types';
+import {HtmlLayer} from '../HtmlLayer';
import {
getMarkerHaloVisibility,
getMarkerVisibility,
@@ -27,10 +28,11 @@ type Args = {
dispatcher: Dispatch