diff --git a/src/plugins/d3/__stories__/treemap/HtmlLabels.stories.tsx b/src/plugins/d3/__stories__/treemap/HtmlLabels.stories.tsx
new file mode 100644
index 00000000..1a5788f7
--- /dev/null
+++ b/src/plugins/d3/__stories__/treemap/HtmlLabels.stories.tsx
@@ -0,0 +1,75 @@
+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 {TreemapSeries} from '../../../../types';
+import {ExampleWrapper} from '../../examples/ExampleWrapper';
+import {D3Plugin} from '../../index';
+
+const TreemapWithHtmlLabels = () => {
+ const [loading, setLoading] = React.useState(true);
+
+ React.useEffect(() => {
+ settings.set({plugins: [D3Plugin]});
+ setLoading(false);
+ }, []);
+
+ if (loading) {
+ return ;
+ }
+
+ const styledLabel = (label: string) =>
+ `${label}`;
+ const treemapSeries: TreemapSeries = {
+ type: 'treemap',
+ name: 'Example',
+ dataLabels: {
+ enabled: true,
+ html: true,
+ align: 'right',
+ },
+ layoutAlgorithm: 'binary',
+ levels: [
+ {index: 1, padding: 3},
+ {index: 2, padding: 1},
+ ],
+ data: [
+ {name: styledLabel('One'), value: 15},
+ {name: styledLabel('Two'), id: 'Two'},
+ {name: [styledLabel('Two'), '1'], value: 2, parentId: 'Two'},
+ {name: [styledLabel('Two'), '2'], value: 8, parentId: 'Two'},
+ ],
+ };
+
+ const getWidgetData = (): ChartKitWidgetData => ({
+ series: {
+ data: [treemapSeries],
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const TreemapWithHtmlLabelsStory: StoryObj = {
+ name: 'Html in labels',
+};
+
+export default {
+ title: 'Plugins/D3/Treemap',
+ component: TreemapWithHtmlLabels,
+};
diff --git a/src/plugins/d3/__stories__/treemap/Playground.stories.tsx b/src/plugins/d3/__stories__/treemap/Playground.stories.tsx
index cf8fdea8..c7b145ce 100644
--- a/src/plugins/d3/__stories__/treemap/Playground.stories.tsx
+++ b/src/plugins/d3/__stories__/treemap/Playground.stories.tsx
@@ -17,13 +17,17 @@ const prepareData = (): ChartKitWidgetData => {
enabled: true,
},
layoutAlgorithm: 'binary',
- levels: [{index: 1}, {index: 2}, {index: 3}],
+ levels: [
+ {index: 1, padding: 5},
+ {index: 2, padding: 3},
+ {index: 3, padding: 1},
+ ],
data: [
{name: 'One', value: 15},
{name: 'Two', value: 10},
{name: 'Three', value: 15},
{name: 'Four'},
- {name: 'Four-1', value: 5, parentId: 'Four'},
+ {name: ['Four', '1'], value: 5, parentId: 'Four'},
{name: 'Four-2', parentId: 'Four'},
{name: 'Four-3', value: 4, parentId: 'Four'},
{name: 'Four-2-1', value: 5, parentId: 'Four-2'},
diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-treemap.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-treemap.ts
index aaf6e1ea..460be2a7 100644
--- a/src/plugins/d3/renderer/hooks/useSeries/prepare-treemap.ts
+++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-treemap.ts
@@ -32,7 +32,8 @@ 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),
+ html: get(s, 'dataLabels.html', false),
+ align: get(s, 'dataLabels.align', 'left'),
},
id,
type: s.type,
diff --git a/src/plugins/d3/renderer/hooks/useSeries/types.ts b/src/plugins/d3/renderer/hooks/useSeries/types.ts
index 62b0a98b..de997dee 100644
--- a/src/plugins/d3/renderer/hooks/useSeries/types.ts
+++ b/src/plugins/d3/renderer/hooks/useSeries/types.ts
@@ -269,6 +269,7 @@ export type PreparedTreemapSeries = {
padding: number;
allowOverlap: boolean;
html: boolean;
+ align: Required['dataLabels']>['align'];
};
layoutAlgorithm: `${LayoutAlgorithm}`;
} & BasePreparedSeries &
diff --git a/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx
index b3cc61fa..a36d7e63 100644
--- a/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx
+++ b/src/plugins/d3/renderer/hooks/useShapes/treemap/index.tsx
@@ -23,7 +23,7 @@ type ShapeProps = {
export const TreemapSeriesShape = (props: ShapeProps) => {
const {dispatcher, preparedData, seriesOptions, htmlLayout} = props;
- const ref = React.useRef(null);
+ const ref = React.useRef(null);
React.useEffect(() => {
if (!ref.current) {
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 f05dbf0f..aa31bf81 100644
--- a/src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts
+++ b/src/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.ts
@@ -11,24 +11,70 @@ import type {HierarchyRectangularNode} from 'd3';
import {LayoutAlgorithm} from '../../../../../../constants';
import type {TreemapSeriesData} from '../../../../../../types';
+import {HtmlItem} from '../../../types';
+import {getLabelsSize} from '../../../utils';
import type {PreparedTreemapSeries} from '../../useSeries/types';
import type {PreparedTreemapData, TreemapLabelData} from './types';
const DEFAULT_PADDING = 1;
-function getLabelData(data: HierarchyRectangularNode[]): TreemapLabelData[] {
- return data.map((d) => {
- const text = d.data.name;
-
- return {
- text,
- x: d.x0,
- y: d.y0,
- width: d.x1 - d.x0,
- nodeData: d.data,
- };
- });
+type LabelItem = HtmlItem | TreemapLabelData;
+
+function getLabels(args: {
+ data: HierarchyRectangularNode[];
+ html: boolean;
+ padding: number;
+ align: PreparedTreemapSeries['dataLabels']['align'];
+}) {
+ const {data, html, padding, align} = args;
+
+ return data.reduce((acc, d) => {
+ const texts = Array.isArray(d.data.name) ? d.data.name : [d.data.name];
+
+ texts.forEach((text, index) => {
+ const {maxHeight: lineHeight, maxWidth: labelWidth} =
+ getLabelsSize({labels: [text], html}) ?? {};
+ const left = d.x0 + padding;
+ const right = d.x1 - padding;
+ const width = Math.max(0, right - left);
+ let x = left;
+ const y = index * lineHeight + d.y0 + padding;
+
+ switch (align) {
+ case 'left': {
+ x = left;
+ break;
+ }
+ case 'center': {
+ x = Math.max(left, left + (width - labelWidth) / 2);
+ break;
+ }
+ case 'right': {
+ x = Math.max(left, right - labelWidth);
+ break;
+ }
+ }
+
+ const item: LabelItem = html
+ ? {
+ content: text,
+ x,
+ y,
+ }
+ : {
+ text,
+ x,
+ y,
+ width,
+ nodeData: d.data,
+ };
+
+ acc.push(item);
+ });
+
+ return acc;
+ }, []);
}
export function prepareTreemapData(args: {
@@ -39,7 +85,13 @@ export function prepareTreemapData(args: {
const {series, width, height} = args;
const dataWithRootNode = getSeriesDataWithRootNode(series);
const hierarchy = stratify()
- .id((d) => d.id || d.name)
+ .id((d) => {
+ if (d.id) {
+ return d.id;
+ }
+
+ return Array.isArray(d.name) ? d.name.join() : d.name;
+ })
.parentId((d) => d.parentId)(dataWithRootNode)
.sum((d) => d.value || 0);
const treemapInstance = treemap();
@@ -72,9 +124,20 @@ export function prepareTreemapData(args: {
return levelOptions?.padding ?? DEFAULT_PADDING;
})(hierarchy);
const leaves = root.leaves();
- const labelData: TreemapLabelData[] = series.dataLabels?.enabled ? getLabelData(leaves) : [];
+ let labelData: TreemapLabelData[] = [];
+ const htmlElements: HtmlItem[] = [];
+
+ if (series.dataLabels?.enabled) {
+ const {html, padding, align} = series.dataLabels;
+ const labels = getLabels({html, padding, align, data: leaves});
+ if (html) {
+ htmlElements.push(...(labels as HtmlItem[]));
+ } else {
+ labelData = labels as TreemapLabelData[];
+ }
+ }
- return {labelData, leaves, series, htmlElements: []};
+ return {labelData, leaves, series, htmlElements};
}
function getSeriesDataWithRootNode(series: PreparedTreemapSeries) {
diff --git a/src/plugins/d3/renderer/validation/index.ts b/src/plugins/d3/renderer/validation/index.ts
index 009a00e8..0b85ed8a 100644
--- a/src/plugins/d3/renderer/validation/index.ts
+++ b/src/plugins/d3/renderer/validation/index.ts
@@ -158,7 +158,10 @@ const validateTreemapSeries = ({series}: {series: TreemapSeries}) => {
}
});
series.data.forEach((d) => {
- const idOrName = d.id || d.name;
+ let idOrName = d.id;
+ if (!idOrName) {
+ idOrName = Array.isArray(d.name) ? d.name.join() : d.name;
+ }
if (parentIds[idOrName] && typeof d.value === 'number') {
throw new ChartKitError({
diff --git a/src/types/widget-data/base.ts b/src/types/widget-data/base.ts
index ca3de71a..7d51831d 100644
--- a/src/types/widget-data/base.ts
+++ b/src/types/widget-data/base.ts
@@ -3,8 +3,6 @@ export type BaseSeries = {
visible?: boolean;
/**
* Options for the series data labels, appearing next to each data point.
- *
- * Note: now this option is supported only for `pie` charts.
* */
dataLabels?: {
/**
diff --git a/src/types/widget-data/treemap.ts b/src/types/widget-data/treemap.ts
index 23633e1e..ded6af3e 100644
--- a/src/types/widget-data/treemap.ts
+++ b/src/types/widget-data/treemap.ts
@@ -5,7 +5,7 @@ import {ChartKitWidgetLegend, RectLegendSymbolOptions} from './legend';
export type TreemapSeriesData = BaseSeriesData & {
/** The name of the node (used in legend, tooltip etc). */
- name: string;
+ name: string | string[];
/** The value of the node. All nodes should have this property except nodes that have children. */
value?: number;
/** An id for the node. Used to group children. */
@@ -38,4 +38,11 @@ export type TreemapSeries = BaseSeries & {
color?: string;
}[];
layoutAlgorithm?: `${LayoutAlgorithm}`;
+ /**
+ * Options for the series data labels, appearing next to each data point.
+ * */
+ dataLabels?: BaseSeries['dataLabels'] & {
+ /** Horizontal alignment of the data label inside the tile. */
+ align?: 'left' | 'center' | 'right';
+ };
};