From b92b2264c3120f7e518663f057da078d3b83577d Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Tue, 14 Apr 2026 16:16:37 +0300 Subject: [PATCH 1/2] refactor(shapes): extract shared renderDataLabels utility --- src/core/shapes/area/renderer.ts | 18 +++++--------- src/core/shapes/bar-x/renderer.ts | 18 +++++--------- src/core/shapes/bar-y/renderer.ts | 18 +++++--------- src/core/shapes/data-labels.ts | 32 +++++++++++++++++++++++++ src/core/shapes/funnel/renderer.ts | 17 +++++-------- src/core/shapes/heatmap/prepare-data.ts | 1 + src/core/shapes/heatmap/renderer.ts | 17 +++++-------- src/core/shapes/heatmap/types.ts | 1 + src/core/shapes/line/renderer.ts | 18 +++++--------- src/core/shapes/radar/renderer.ts | 22 +++++++---------- src/core/shapes/sankey/renderer.ts | 18 +++++--------- src/core/shapes/waterfall/renderer.ts | 18 +++++--------- src/core/shapes/x-range/renderer.ts | 20 ++++++---------- 13 files changed, 98 insertions(+), 120 deletions(-) create mode 100644 src/core/shapes/data-labels.ts diff --git a/src/core/shapes/area/renderer.ts b/src/core/shapes/area/renderer.ts index daab17fb4..868f7e404 100644 --- a/src/core/shapes/area/renderer.ts +++ b/src/core/shapes/area/renderer.ts @@ -10,6 +10,7 @@ import {block} from '../../../utils'; import type {PreparedSeriesOptions} from '../../series/types'; import {filterOverlappingLabels} from '../../utils'; import {renderAnnotations} from '../annotation'; +import {renderDataLabels} from '../data-labels'; import { getMarkerHaloVisibility, getMarkerVisibility, @@ -90,18 +91,11 @@ export function renderArea( dataLabels = filterOverlappingLabels(dataLabels); } - const labelsSelection = plotSvgElement - .selectAll('text') - .data(dataLabels) - .join('text') - .html((d) => d.text) - .attr('class', b('label')) - .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 labelsSelection = renderDataLabels({ + container: plotSvgElement, + data: dataLabels, + className: b('label'), + }); const markers = preparedData.reduce((acc, d) => acc.concat(d.markers), []); const markerSelection = markersSvgElement diff --git a/src/core/shapes/bar-x/renderer.ts b/src/core/shapes/bar-x/renderer.ts index a34be35e5..00239f538 100644 --- a/src/core/shapes/bar-x/renderer.ts +++ b/src/core/shapes/bar-x/renderer.ts @@ -7,6 +7,7 @@ import {block} from '../../../utils'; import type {AnnotationAnchor, PreparedSeriesOptions} from '../../series/types'; import {filterOverlappingLabels} from '../../utils'; import {renderAnnotations} from '../annotation'; +import {renderDataLabels} from '../data-labels'; import {getRectPath} from '../utils'; import type {PreparedBarXData} from './types'; @@ -59,18 +60,11 @@ export function renderBarX( dataLabels = filterOverlappingLabels(dataLabels); } - const labelSelection = svgElement - .selectAll('text') - .data(dataLabels) - .join('text') - .html((d) => d.text) - .attr('class', b('label')) - .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 labelSelection = renderDataLabels({ + container: svgElement, + data: dataLabels, + className: b('label'), + }); const annotationAnchors: AnnotationAnchor[] = []; for (const d of preparedData) { diff --git a/src/core/shapes/bar-y/renderer.ts b/src/core/shapes/bar-y/renderer.ts index 3c3cbf9b6..e8440dce5 100644 --- a/src/core/shapes/bar-y/renderer.ts +++ b/src/core/shapes/bar-y/renderer.ts @@ -6,6 +6,7 @@ import get from 'lodash/get'; import type {LabelData} from '../../../types'; import {block} from '../../../utils'; import type {PreparedSeriesOptions} from '../../series/types'; +import {renderDataLabels} from '../data-labels'; import type {BarYShapesArgs, PreparedBarYData} from './types'; import {getAdjustedRectBorderPath, getAdjustedRectPath} from './utils'; @@ -48,18 +49,11 @@ export function renderBarY( .attr('opacity', (d) => d.data.opacity || null) .attr('pointer-events', 'none'); - const labelSelection = svgElement - .selectAll('text') - .data(dataLabels) - .join('text') - .html((d) => d.text) - .attr('class', b('label')) - .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 labelSelection = renderDataLabels({ + container: svgElement, + data: dataLabels, + className: b('label'), + }); const hoverOptions = get(seriesOptions, 'bar-y.states.hover'); const inactiveOptions = get(seriesOptions, 'bar-y.states.inactive'); diff --git a/src/core/shapes/data-labels.ts b/src/core/shapes/data-labels.ts new file mode 100644 index 000000000..144d3800d --- /dev/null +++ b/src/core/shapes/data-labels.ts @@ -0,0 +1,32 @@ +import type {Selection} from 'd3-selection'; + +import type {BaseTextStyle} from '../types/chart/base'; + +type RenderableLabelData = { + text: string; + x: number; + y: number; + textAnchor: 'start' | 'end' | 'middle'; + style: BaseTextStyle; +}; + +export function renderDataLabels(args: { + container: Selection; + data: T[]; + className: string; +}): Selection { + const {container, data, className} = args; + + return container + .selectAll('text') + .data(data) + .join('text') + .html((d) => d.text) + .attr('class', className) + .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); +} diff --git a/src/core/shapes/funnel/renderer.ts b/src/core/shapes/funnel/renderer.ts index b6ee0a0d0..236afa57e 100644 --- a/src/core/shapes/funnel/renderer.ts +++ b/src/core/shapes/funnel/renderer.ts @@ -6,6 +6,7 @@ import type {TooltipDataChunkFunnel} from '../../../types'; import {block} from '../../../utils'; import type {PreparedSeriesOptions} from '../../series/types'; import {getLineDashArray} from '../../utils'; +import {renderDataLabels} from '../data-labels'; import type {PreparedFunnelData} from './types'; @@ -62,17 +63,11 @@ export function renderFunnel( connectorLines.append('path').attr('d', (d) => d.linePath[1].toString()); // dataLabels - svgElement - .selectAll('text') - .data(preparedData.svgLabels) - .join('text') - .html((d) => d.text) - .attr('class', b('label')) - .attr('x', (d) => d.x) - .attr('y', (d) => d.y) - .style('font-size', (d) => d.style.fontSize) - .style('font-weight', (d) => d.style.fontWeight || null) - .style('fill', (d) => d.style.fontColor || null); + renderDataLabels({ + container: svgElement, + data: preparedData.svgLabels, + className: b('label'), + }); function handleShapeHover(data?: TooltipDataChunkFunnel[]) { const hoverEnabled = hoverOptions?.enabled; diff --git a/src/core/shapes/heatmap/prepare-data.ts b/src/core/shapes/heatmap/prepare-data.ts index 0e274276f..01c3cb118 100644 --- a/src/core/shapes/heatmap/prepare-data.ts +++ b/src/core/shapes/heatmap/prepare-data.ts @@ -131,6 +131,7 @@ export async function prepareHeatmapData({ x: item.x + item.width / 2 - size.width / 2, y: item.y + item.height / 2 - size.height / 2 + size.hangingOffset, text, + textAnchor: 'start', style: series.dataLabels.style, }); } diff --git a/src/core/shapes/heatmap/renderer.ts b/src/core/shapes/heatmap/renderer.ts index d38a8d740..58de6f74f 100644 --- a/src/core/shapes/heatmap/renderer.ts +++ b/src/core/shapes/heatmap/renderer.ts @@ -5,6 +5,7 @@ import {select} from 'd3-selection'; import type {TooltipDataChunkHeatmap} from '../../../types'; import {block} from '../../../utils'; import type {PreparedSeriesOptions} from '../../series/types'; +import {renderDataLabels} from '../data-labels'; import type {PreparedHeatmapData} from './types'; @@ -36,17 +37,11 @@ export function renderHeatmap( .attr('stroke-width', (d) => d.borderWidth); // dataLabels - svgElement - .selectAll('text') - .data(preparedData.labels) - .join('text') - .html((d) => d.text) - .attr('class', b('label')) - .attr('x', (d) => d.x) - .attr('y', (d) => d.y) - .style('font-size', (d) => d.style.fontSize) - .style('font-weight', (d) => d.style.fontWeight || null) - .style('fill', (d) => d.style.fontColor || null); + renderDataLabels({ + container: svgElement, + data: preparedData.labels, + className: b('label'), + }); function handleShapeHover(data?: TooltipDataChunkHeatmap[]) { const hoverEnabled = hoverOptions?.enabled; diff --git a/src/core/shapes/heatmap/types.ts b/src/core/shapes/heatmap/types.ts index 542249305..e76ebd769 100644 --- a/src/core/shapes/heatmap/types.ts +++ b/src/core/shapes/heatmap/types.ts @@ -16,6 +16,7 @@ export type HeatmapLabel = { x: number; y: number; text: string; + textAnchor: 'start' | 'end' | 'middle'; style: BaseTextStyle; }; diff --git a/src/core/shapes/line/renderer.ts b/src/core/shapes/line/renderer.ts index b1601a398..297d63e73 100644 --- a/src/core/shapes/line/renderer.ts +++ b/src/core/shapes/line/renderer.ts @@ -10,6 +10,7 @@ import {block} from '../../../utils'; import type {PreparedSeriesOptions} from '../../series/types'; import {getLineDashArray} from '../../utils'; import {renderAnnotations} from '../annotation'; +import {renderDataLabels} from '../data-labels'; import { getMarkerHaloVisibility, getMarkerVisibility, @@ -69,18 +70,11 @@ export function renderLine( return acc.concat(d.svgLabels); }, [] as LabelData[]); - const labelsSelection = plotSvgElement - .selectAll('text') - .data(dataLabels) - .join('text') - .html((d) => d.text) - .attr('class', b('label')) - .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 labelsSelection = renderDataLabels({ + container: plotSvgElement, + data: dataLabels, + className: b('label'), + }); const markers = preparedData.reduce((acc, d) => acc.concat(d.markers), []); const markerSelection = markersSvgElement diff --git a/src/core/shapes/radar/renderer.ts b/src/core/shapes/radar/renderer.ts index b9a066d53..cb1f94e5a 100644 --- a/src/core/shapes/radar/renderer.ts +++ b/src/core/shapes/radar/renderer.ts @@ -1,13 +1,14 @@ import {color} from 'd3-color'; import type {Dispatch} from 'd3-dispatch'; import {select} from 'd3-selection'; -import type {BaseType} from 'd3-selection'; +import type {BaseType, Selection} from 'd3-selection'; import {line} from 'd3-shape'; import get from 'lodash/get'; import type {TooltipDataChunkRadar} from '../../../types'; import {block} from '../../../utils'; import type {PreparedRadarSeries, PreparedSeriesOptions} from '../../series/types'; +import {renderDataLabels} from '../../shapes/data-labels'; import { getMarkerHaloVisibility, getMarkerVisibility, @@ -89,18 +90,13 @@ export function renderRadar( .call(renderMarker); // Render labels - radarSelection - .selectAll('text') - .data((radarData) => radarData.labels) - .join('text') - .html((d) => d.text) - .attr('class', b('label')) - .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); + radarSelection.each(function (radarData) { + renderDataLabels({ + container: select(this) as Selection, + data: radarData.labels, + className: b('label'), + }); + }); // Handle hover events const eventName = `hover-shape.radar`; diff --git a/src/core/shapes/sankey/renderer.ts b/src/core/shapes/sankey/renderer.ts index ebb32cd3d..391df43e1 100644 --- a/src/core/shapes/sankey/renderer.ts +++ b/src/core/shapes/sankey/renderer.ts @@ -4,6 +4,7 @@ import {select} from 'd3-selection'; import type {TooltipDataChunkTreemap} from '../../../types'; import {block} from '../../../utils'; import type {PreparedSeriesOptions} from '../../series/types'; +import {renderDataLabels} from '../data-labels'; import type {PreparedSankeyData} from './types'; @@ -46,18 +47,11 @@ export function renderSankey( .attr('stroke-width', (d) => d.strokeWidth); // dataLabels - svgElement - .append('g') - .selectAll() - .data(preparedData.labels) - .join('text') - .html((d) => d.text) - .attr('class', b('label')) - .attr('x', (d) => d.x) - .attr('y', (d) => d.y) - .attr('dy', '0.35em') - .attr('text-anchor', (d) => d.textAnchor) - .attr('fill', (d) => d.style.fontColor ?? null); + renderDataLabels({ + container: svgElement.append('g'), + data: preparedData.labels, + className: b('label'), + }).attr('dy', '0.35em'); const eventName = `hover-shape.sankey`; diff --git a/src/core/shapes/waterfall/renderer.ts b/src/core/shapes/waterfall/renderer.ts index c302f6231..ca0dd8b7a 100644 --- a/src/core/shapes/waterfall/renderer.ts +++ b/src/core/shapes/waterfall/renderer.ts @@ -9,6 +9,7 @@ import {block} from '../../../utils'; import {DASH_STYLE} from '../../constants'; import type {PreparedSeriesOptions} from '../../series/types'; import {filterOverlappingLabels, getLineDashArray, getWaterfallPointColor} from '../../utils'; +import {renderDataLabels} from '../data-labels'; import type {PreparedWaterfallData} from './types'; @@ -46,18 +47,11 @@ export function renderWaterfall( dataLabels = filterOverlappingLabels(dataLabels); } - const labelSelection = svgElement - .selectAll('text') - .data(dataLabels) - .join('text') - .html((d) => d.text) - .attr('class', b('label')) - .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 labelSelection = renderDataLabels({ + container: svgElement, + data: dataLabels, + className: b('label'), + }); // Add the connector line between bars svgElement diff --git a/src/core/shapes/x-range/renderer.ts b/src/core/shapes/x-range/renderer.ts index da5d68529..198044a7e 100644 --- a/src/core/shapes/x-range/renderer.ts +++ b/src/core/shapes/x-range/renderer.ts @@ -7,6 +7,7 @@ import {block} from '../../../utils'; import type {PreparedSeriesOptions} from '../../series/types'; import {getRectPath} from '../../shapes/utils'; import {getLineDashArray} from '../../utils'; +import {renderDataLabels} from '../data-labels'; import type {PreparedXRangeData} from './types'; @@ -67,20 +68,13 @@ export function renderXRange( .attr('pointer-events', 'none'); const svgLabels = preparedData.flatMap((d) => d.svgLabels); - svgElement - .selectAll(`text.${b('label')}`) - .data(svgLabels) - .join('text') - .attr('class', b('label')) - .attr('x', (d) => d.x) - .attr('y', (d) => d.y) - .attr('text-anchor', (d) => d.textAnchor) + renderDataLabels({ + container: svgElement, + data: svgLabels, + className: b('label'), + }) .attr('dominant-baseline', 'central') - .attr('pointer-events', 'none') - .style('font-size', (d) => d.style.fontSize) - .style('font-weight', (d) => d.style.fontWeight || null) - .style('fill', (d) => d.style.fontColor || null) - .html((d) => d.text); + .attr('pointer-events', 'none'); const hoverOptions = get(seriesOptions, 'x-range.states.hover'); const inactiveOptions = get(seriesOptions, 'x-range.states.inactive'); From 805c8b03bd3d3eae0aa43d5e0386761ab619e3bd Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Tue, 14 Apr 2026 16:34:45 +0300 Subject: [PATCH 2/2] fix: fix sankey --- .../Sankey-series-Basic-1-chromium-linux.png | Bin 6986 -> 6858 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/__snapshots__/sankey-series.visual.test.tsx-snapshots/Sankey-series-Basic-1-chromium-linux.png b/src/__snapshots__/sankey-series.visual.test.tsx-snapshots/Sankey-series-Basic-1-chromium-linux.png index d2b37820c175997ccdc97d3c34e30b24a20f2ec5..10f67ebee0618bbed5a27948d905397cb0790e08 100644 GIT binary patch delta 6205 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