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 d2b37820c..10f67ebee 100644 Binary files a/src/__snapshots__/sankey-series.visual.test.tsx-snapshots/Sankey-series-Basic-1-chromium-linux.png and b/src/__snapshots__/sankey-series.visual.test.tsx-snapshots/Sankey-series-Basic-1-chromium-linux.png differ 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');