From 4033d4d2e3362593d8b98a3014736e5057c76f60 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Mon, 11 Sep 2023 13:30:21 +0300 Subject: [PATCH 1/4] fix(D3 plugin): fix right chart margin --- .../d3/__stories__/scatter-performance.json | 3 ++- .../d3/renderer/hooks/useAxisScales/index.ts | 16 ++++++++++------ .../d3/renderer/hooks/useChartOptions/chart.ts | 9 ++------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/plugins/d3/__stories__/scatter-performance.json b/src/plugins/d3/__stories__/scatter-performance.json index 3fb3d335..85d77af7 100644 --- a/src/plugins/d3/__stories__/scatter-performance.json +++ b/src/plugins/d3/__stories__/scatter-performance.json @@ -32636,7 +32636,8 @@ "yLabel": "109 300" }, "category": "Mbour", - "y": 109300 + "y": 109300, + "radius": 10 } ], "custom": { diff --git a/src/plugins/d3/renderer/hooks/useAxisScales/index.ts b/src/plugins/d3/renderer/hooks/useAxisScales/index.ts index b57d48ff..a85f455e 100644 --- a/src/plugins/d3/renderer/hooks/useAxisScales/index.ts +++ b/src/plugins/d3/renderer/hooks/useAxisScales/index.ts @@ -73,15 +73,17 @@ const createScales = (args: Args) => { let xScale: ChartScale | undefined; let yScale: ChartScale | undefined; + const xAxisMinPadding = boundsWidth * xAxis.maxPadding; + const xRange = [0, boundsWidth - xAxisMinPadding]; + switch (xType) { case 'linear': { const domain = getDomainDataXBySeries(visibleSeries); - const range = [0, boundsWidth - boundsWidth * xAxis.maxPadding]; if (isNumericalArrayData(domain)) { const [domainXMin, xMax] = extent(domain) as [number, number]; const xMinValue = typeof xMin === 'number' ? xMin : domainXMin; - xScale = scaleLinear().domain([xMinValue, xMax]).range(range).nice(); + xScale = scaleLinear().domain([xMinValue, xMax]).range(xRange).nice(); } break; @@ -94,22 +96,24 @@ const createScales = (args: Args) => { series: visibleSeries, }); xScale = scaleBand().domain(filteredCategories).range([0, boundsWidth]); + + if (xScale.step() / 2 < xAxisMinPadding) { + xScale.range(xRange); + } } break; } case 'datetime': { - const range = [0, boundsWidth - boundsWidth * xAxis.maxPadding]; - if (xTimestamps) { const [xMin, xMax] = extent(xTimestamps) as [number, number]; - xScale = scaleUtc().domain([xMin, xMax]).range(range).nice(); + xScale = scaleUtc().domain([xMin, xMax]).range(xRange).nice(); } else { const domain = getDomainDataXBySeries(visibleSeries); if (isNumericalArrayData(domain)) { const [xMin, xMax] = extent(domain) as [number, number]; - xScale = scaleUtc().domain([xMin, xMax]).range(range).nice(); + xScale = scaleUtc().domain([xMin, xMax]).range(xRange).nice(); } } diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts b/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts index d5c08a15..c51d52e7 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts @@ -130,14 +130,9 @@ const getMarginRight = (args: { series: ChartKitWidgetData['series']; preparedXAxis: PreparedAxis; }) => { - const {chart, hasAxisRelatedSeries, series, preparedXAxis} = args; - let marginRight = get(chart, 'margin.right', 0); + const {chart} = args; - if (hasAxisRelatedSeries) { - marginRight += getAxisLabelMaxWidth({axis: preparedXAxis, series: series.data}) / 2; - } - - return marginRight; + return get(chart, 'margin.right', 0); }; export const getPreparedChart = (args: { From 3273c18353789e36f54ecf829e58fef9415e46be Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Mon, 11 Sep 2023 17:09:27 +0300 Subject: [PATCH 2/4] text-overflow --- src/plugins/d3/renderer/components/AxisX.tsx | 53 +++++++++++++------- src/plugins/d3/renderer/components/Chart.tsx | 2 + src/plugins/d3/renderer/utils/index.ts | 1 + src/plugins/d3/renderer/utils/text.ts | 17 +++++++ 4 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 src/plugins/d3/renderer/utils/text.ts diff --git a/src/plugins/d3/renderer/components/AxisX.tsx b/src/plugins/d3/renderer/components/AxisX.tsx index f070dbc3..736809d7 100644 --- a/src/plugins/d3/renderer/components/AxisX.tsx +++ b/src/plugins/d3/renderer/components/AxisX.tsx @@ -4,8 +4,8 @@ import type {AxisScale, AxisDomain} from 'd3'; import {block} from '../../../../utils/cn'; -import type {ChartScale, PreparedAxis} from '../hooks'; -import {formatAxisTickLabel, parseTransformStyle} from '../utils'; +import type {ChartScale, PreparedAxis, PreparedChart} from '../hooks'; +import {formatAxisTickLabel, parseTransformStyle, wrapText} from '../utils'; const b = block('d3-axis'); const EMPTY_SPACE_BETWEEN_LABELS = 10; @@ -15,10 +15,12 @@ type Props = { width: number; height: number; scale: ChartScale; + chart: PreparedChart; + position: {top: number; left: number}; }; // FIXME: add overflow ellipsis for the labels that out of boundaries -export const AxisX = ({axis, width, height, scale}: Props) => { +export const AxisX = ({axis, width, height, scale, chart, position}: Props) => { const ref = React.useRef(null); React.useEffect(() => { @@ -68,20 +70,7 @@ export const AxisX = ({axis, width, height, scale}: Props) => { svgElement.select('.tick').remove(); } - if (axis.title.text) { - const textY = - axis.title.height + parseInt(axis.labels.style.fontSize) + axis.labels.padding; - - svgElement - .append('text') - .attr('class', b('title')) - .attr('text-anchor', 'middle') - .attr('x', width / 2) - .attr('y', textY) - .attr('font-size', axis.title.style.fontSize) - .text(axis.title.text); - } - + // remove overlapping labels let elementX = 0; svgElement .selectAll('.tick') @@ -96,7 +85,35 @@ export const AxisX = ({axis, width, height, scale}: Props) => { return false; }) .remove(); + + // add an ellipsis to the labels on the right that go beyond the boundaries of the chart + // const axisRect = (domain.node() as Element)?.getBoundingClientRect(); + const rightBound = position.left + chart.margin.left + width + chart.margin.right; + + svgElement.selectAll('.tick text').each(function () { + const node = this as unknown as SVGTextElement; + const textRect = node.getBoundingClientRect(); + + if (textRect.right > rightBound) { + const maxWidth = textRect.width - (textRect.right - rightBound) * 2; + select(node).call(wrapText, maxWidth); + } + }); + + if (axis.title.text) { + const textY = + axis.title.height + parseInt(axis.labels.style.fontSize) + axis.labels.padding; + + svgElement + .append('text') + .attr('class', b('title')) + .attr('text-anchor', 'middle') + .attr('x', width / 2) + .attr('y', textY) + .attr('font-size', axis.title.style.fontSize) + .text(axis.title.text); + } }, [axis, width, height, scale]); - return ; + return ; }; diff --git a/src/plugins/d3/renderer/components/Chart.tsx b/src/plugins/d3/renderer/components/Chart.tsx index 8367bb99..b5806435 100644 --- a/src/plugins/d3/renderer/components/Chart.tsx +++ b/src/plugins/d3/renderer/components/Chart.tsx @@ -99,6 +99,8 @@ export const Chart = (props: Props) => { width={boundsWidth} height={boundsHeight} scale={xScale} + chart={chart} + position={{top, left}} /> diff --git a/src/plugins/d3/renderer/utils/index.ts b/src/plugins/d3/renderer/utils/index.ts index 241ab4ef..b3a3d7ae 100644 --- a/src/plugins/d3/renderer/utils/index.ts +++ b/src/plugins/d3/renderer/utils/index.ts @@ -16,6 +16,7 @@ import type {FormatNumberOptions} from '../../../shared'; import {DEFAULT_AXIS_LABEL_FONT_SIZE} from '../constants'; export * from './math'; +export * from './text'; const CHARTS_WITHOUT_AXIS: ChartKitWidgetSeries['type'][] = ['pie']; diff --git a/src/plugins/d3/renderer/utils/text.ts b/src/plugins/d3/renderer/utils/text.ts new file mode 100644 index 00000000..5dc61c8e --- /dev/null +++ b/src/plugins/d3/renderer/utils/text.ts @@ -0,0 +1,17 @@ +import {Selection} from 'd3-selection'; + +export function wrapText( + selection: Selection, + maxWidth: number, +) { + let text = selection.text(); + selection.text(null).attr('text-anchor', 'left').append('title').text(text); + const tSpan = selection.append('tspan').text(text); + + let textLength = tSpan.node()?.getComputedTextLength() || 0; + while (textLength > maxWidth && text.length > 1) { + text = text.slice(0, -1); + tSpan.text(text + '...'); + textLength = tSpan.node()?.getComputedTextLength() || 0; + } +} From 00873db9982bc2eb160b74a66a26c7009818433b Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Mon, 11 Sep 2023 17:13:37 +0300 Subject: [PATCH 3/4] fix --- src/plugins/d3/renderer/components/AxisX.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/d3/renderer/components/AxisX.tsx b/src/plugins/d3/renderer/components/AxisX.tsx index 736809d7..6f90de97 100644 --- a/src/plugins/d3/renderer/components/AxisX.tsx +++ b/src/plugins/d3/renderer/components/AxisX.tsx @@ -87,7 +87,6 @@ export const AxisX = ({axis, width, height, scale, chart, position}: Props) => { .remove(); // add an ellipsis to the labels on the right that go beyond the boundaries of the chart - // const axisRect = (domain.node() as Element)?.getBoundingClientRect(); const rightBound = position.left + chart.margin.left + width + chart.margin.right; svgElement.selectAll('.tick text').each(function () { @@ -100,6 +99,7 @@ export const AxisX = ({axis, width, height, scale, chart, position}: Props) => { } }); + // add an axis header if necessary if (axis.title.text) { const textY = axis.title.height + parseInt(axis.labels.style.fontSize) + axis.labels.padding; @@ -115,5 +115,5 @@ export const AxisX = ({axis, width, height, scale, chart, position}: Props) => { } }, [axis, width, height, scale]); - return ; + return ; }; From 29b3f9fc693cf6c84adeaccdc3f60778b90c4faf Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Mon, 11 Sep 2023 17:48:43 +0300 Subject: [PATCH 4/4] fix review --- src/plugins/d3/renderer/components/AxisX.tsx | 20 +++++++++---------- src/plugins/d3/renderer/components/Chart.tsx | 3 +-- .../renderer/hooks/useChartOptions/chart.ts | 9 ++------- src/plugins/d3/renderer/utils/text.ts | 4 ++-- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/plugins/d3/renderer/components/AxisX.tsx b/src/plugins/d3/renderer/components/AxisX.tsx index 6f90de97..550fee8b 100644 --- a/src/plugins/d3/renderer/components/AxisX.tsx +++ b/src/plugins/d3/renderer/components/AxisX.tsx @@ -4,8 +4,8 @@ import type {AxisScale, AxisDomain} from 'd3'; import {block} from '../../../../utils/cn'; -import type {ChartScale, PreparedAxis, PreparedChart} from '../hooks'; -import {formatAxisTickLabel, parseTransformStyle, wrapText} from '../utils'; +import type {ChartScale, PreparedAxis} from '../hooks'; +import {formatAxisTickLabel, parseTransformStyle, setEllipsisForOverflowText} from '../utils'; const b = block('d3-axis'); const EMPTY_SPACE_BETWEEN_LABELS = 10; @@ -15,12 +15,11 @@ type Props = { width: number; height: number; scale: ChartScale; - chart: PreparedChart; - position: {top: number; left: number}; + chartWidth: number; }; // FIXME: add overflow ellipsis for the labels that out of boundaries -export const AxisX = ({axis, width, height, scale, chart, position}: Props) => { +export const AxisX = ({axis, width, height, scale, chartWidth}: Props) => { const ref = React.useRef(null); React.useEffect(() => { @@ -87,15 +86,13 @@ export const AxisX = ({axis, width, height, scale, chart, position}: Props) => { .remove(); // add an ellipsis to the labels on the right that go beyond the boundaries of the chart - const rightBound = position.left + chart.margin.left + width + chart.margin.right; - svgElement.selectAll('.tick text').each(function () { const node = this as unknown as SVGTextElement; const textRect = node.getBoundingClientRect(); - if (textRect.right > rightBound) { - const maxWidth = textRect.width - (textRect.right - rightBound) * 2; - select(node).call(wrapText, maxWidth); + if (textRect.right > chartWidth) { + const maxWidth = textRect.width - (textRect.right - chartWidth) * 2; + select(node).call(setEllipsisForOverflowText, maxWidth); } }); @@ -111,7 +108,8 @@ export const AxisX = ({axis, width, height, scale, chart, position}: Props) => { .attr('x', width / 2) .attr('y', textY) .attr('font-size', axis.title.style.fontSize) - .text(axis.title.text); + .text(axis.title.text) + .call(setEllipsisForOverflowText, width); } }, [axis, width, height, scale]); diff --git a/src/plugins/d3/renderer/components/Chart.tsx b/src/plugins/d3/renderer/components/Chart.tsx index b5806435..bb6d50d9 100644 --- a/src/plugins/d3/renderer/components/Chart.tsx +++ b/src/plugins/d3/renderer/components/Chart.tsx @@ -99,8 +99,7 @@ export const Chart = (props: Props) => { width={boundsWidth} height={boundsHeight} scale={xScale} - chart={chart} - position={{top, left}} + chartWidth={width} /> diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts b/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts index c51d52e7..d041c42a 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts @@ -124,12 +124,7 @@ const getMarginLeft = (args: { return marginLeft; }; -const getMarginRight = (args: { - chart: ChartKitWidgetData['chart']; - hasAxisRelatedSeries: boolean; - series: ChartKitWidgetData['series']; - preparedXAxis: PreparedAxis; -}) => { +const getMarginRight = (args: {chart: ChartKitWidgetData['chart']}) => { const {chart} = args; return get(chart, 'margin.right', 0); @@ -153,7 +148,7 @@ export const getPreparedChart = (args: { preparedXAxis, }); const marginLeft = getMarginLeft({chart, hasAxisRelatedSeries, series, preparedY1Axis}); - const marginRight = getMarginRight({chart, hasAxisRelatedSeries, series, preparedXAxis}); + const marginRight = getMarginRight({chart}); return { margin: { diff --git a/src/plugins/d3/renderer/utils/text.ts b/src/plugins/d3/renderer/utils/text.ts index 5dc61c8e..d7a95761 100644 --- a/src/plugins/d3/renderer/utils/text.ts +++ b/src/plugins/d3/renderer/utils/text.ts @@ -1,6 +1,6 @@ import {Selection} from 'd3-selection'; -export function wrapText( +export function setEllipsisForOverflowText( selection: Selection, maxWidth: number, ) { @@ -11,7 +11,7 @@ export function wrapText( let textLength = tSpan.node()?.getComputedTextLength() || 0; while (textLength > maxWidth && text.length > 1) { text = text.slice(0, -1); - tSpan.text(text + '...'); + tSpan.text(text + '…'); textLength = tSpan.node()?.getComputedTextLength() || 0; } }