From d635714589842609b19f0abd9ae88423f8cb9487 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Wed, 20 Sep 2023 00:28:29 +0300 Subject: [PATCH 1/2] fix(D3 plugin): bar-x min column width --- .../__stories__/bar-x/LinearAxis.stories.tsx | 361 +++++++++++++++++- src/plugins/d3/renderer/components/AxisX.tsx | 29 +- src/plugins/d3/renderer/components/Chart.tsx | 1 - .../d3/renderer/hooks/useShapes/bar-x.tsx | 6 +- .../renderer/utils/axis-generators/bottom.ts | 101 +++-- src/plugins/d3/renderer/utils/axis.ts | 7 - src/plugins/d3/renderer/utils/text.ts | 2 +- 7 files changed, 432 insertions(+), 75 deletions(-) diff --git a/src/plugins/d3/__stories__/bar-x/LinearAxis.stories.tsx b/src/plugins/d3/__stories__/bar-x/LinearAxis.stories.tsx index 9d7c516c..3cff28a6 100644 --- a/src/plugins/d3/__stories__/bar-x/LinearAxis.stories.tsx +++ b/src/plugins/d3/__stories__/bar-x/LinearAxis.stories.tsx @@ -19,26 +19,359 @@ const Template: Story = () => { visible: true, data: [ { - x: 10, - y: 100, + y: 1, + x: -1523, }, { - x: 12, - y: 80, + y: 1, + x: -1000, + }, + { + y: 1, + x: -660, + }, + { + y: 1, + x: 800, + }, + { + y: 1, + x: 836, + }, + { + y: 1, + x: 843, + }, + { + y: 1, + x: 885, + }, + { + y: 1, + x: 1066, + }, + { + y: 1, + x: 1143, + }, + { + y: 1, + x: 1278, + }, + { + y: 1, + x: 1350, + }, + { + y: 1, + x: 1492, + }, + { + y: 1, + x: 1499, + }, + { + y: 1, + x: 1581, + }, + { + y: 1, + x: 1769, + }, + { + y: 1, + x: 1776, + }, + { + y: 1, + x: 1804, + }, + { + y: 1, + x: 1806, + }, + { + y: 2, + x: 1810, + }, + { + y: 1, + x: 1811, + }, + { + y: 1, + x: 1816, + }, + { + y: 2, + x: 1821, + }, + { + y: 1, + x: 1822, + }, + { + y: 1, + x: 1825, + }, + { + y: 1, + x: 1828, + }, + { + y: 2, + x: 1830, + }, + { + y: 1, + x: 1838, + }, + { + y: 1, + x: 1841, + }, + { + y: 1, + x: 1844, + }, + { + y: 1, + x: 1847, + }, + { + y: 2, + x: 1861, + }, + { + y: 2, + x: 1867, + }, + { + y: 1, + x: 1878, + }, + { + y: 1, + x: 1901, + }, + { + y: 1, + x: 1902, + }, + { + y: 1, + x: 1903, + }, + { + y: 1, + x: 1905, + }, + { + y: 1, + x: 1906, + }, + { + y: 1, + x: 1907, + }, + { + y: 1, + x: 1908, + }, + { + y: 2, + x: 1910, + }, + { + y: 1, + x: 1912, + }, + { + y: 1, + x: 1917, }, - ], - name: 'AB', - }, - { - type: 'bar-x', - visible: true, - data: [ { - x: 95.5, - y: 120, + y: 4, + x: 1918, + }, + { + y: 1, + x: 1919, + }, + { + y: 2, + x: 1921, + }, + { + y: 1, + x: 1922, + }, + { + y: 1, + x: 1923, + }, + { + y: 1, + x: 1929, + }, + { + y: 1, + x: 1932, + }, + { + y: 1, + x: 1941, + }, + { + y: 1, + x: 1944, + }, + { + y: 2, + x: 1945, + }, + { + y: 2, + x: 1946, + }, + { + y: 1, + x: 1947, + }, + { + y: 4, + x: 1948, + }, + { + y: 2, + x: 1951, + }, + { + y: 1, + x: 1953, + }, + { + y: 1, + x: 1955, + }, + { + y: 1, + x: 1956, + }, + { + y: 2, + x: 1957, + }, + { + y: 1, + x: 1958, + }, + { + y: 4, + x: 1960, + }, + { + y: 3, + x: 1961, + }, + { + y: 4, + x: 1962, + }, + { + y: 1, + x: 1963, + }, + { + y: 2, + x: 1964, + }, + { + y: 3, + x: 1965, + }, + { + y: 3, + x: 1966, + }, + { + y: 4, + x: 1968, + }, + { + y: 2, + x: 1970, + }, + { + y: 2, + x: 1971, + }, + { + y: 1, + x: 1973, + }, + { + y: 2, + x: 1974, + }, + { + y: 5, + x: 1975, + }, + { + y: 1, + x: 1976, + }, + { + y: 1, + x: 1977, + }, + { + y: 3, + x: 1978, + }, + { + y: 2, + x: 1979, + }, + { + y: 2, + x: 1980, + }, + { + y: 2, + x: 1981, + }, + { + y: 1, + x: 1983, + }, + { + y: 1, + x: 1984, + }, + { + y: 2, + x: 1990, + }, + { + y: 5, + x: 1991, + }, + { + y: 1, + x: 1992, + }, + { + y: 2, + x: 1993, + }, + { + y: 1, + x: 1994, }, ], - name: 'C', + name: 'AB', }, ], }, diff --git a/src/plugins/d3/renderer/components/AxisX.tsx b/src/plugins/d3/renderer/components/AxisX.tsx index bd2d2f42..4c3f0e7c 100644 --- a/src/plugins/d3/renderer/components/AxisX.tsx +++ b/src/plugins/d3/renderer/components/AxisX.tsx @@ -22,7 +22,6 @@ type Props = { width: number; height: number; scale: ChartScale; - chartWidth: number; }; function getLabelFormatter({axis, scale}: {axis: PreparedAxis; scale: ChartScale}) { @@ -42,7 +41,7 @@ function getLabelFormatter({axis, scale}: {axis: PreparedAxis; scale: ChartScale }; } -export const AxisX = React.memo(({axis, width, height, scale, chartWidth}: Props) => { +export const AxisX = React.memo(({axis, width, height, scale}: Props) => { const ref = React.useRef(null); React.useEffect(() => { @@ -50,9 +49,6 @@ export const AxisX = React.memo(({axis, width, height, scale, chartWidth}: Props return; } - const svgElement = select(ref.current); - svgElement.selectAll('*').remove(); - const xAxisGenerator = axisBottom({ scale: scale as AxisScale, ticks: { @@ -71,24 +67,13 @@ export const AxisX = React.memo(({axis, width, height, scale, chartWidth}: Props }, }); - svgElement.call(xAxisGenerator).attr('class', b()); - - if (axis.labels.enabled) { - svgElement.style('font-size', axis.labels.style.fontSize); - } - - // add an ellipsis to the labels on the right that go beyond the boundaries of the chart - svgElement.selectAll('.tick text').each(function () { - const node = this as unknown as SVGTextElement; - const textRect = node.getBBox(); - const matrix = node.transform.baseVal.consolidate()?.matrix || ({} as SVGMatrix); - const right = matrix.a * textRect.right + matrix.c * textRect.bottom + matrix.e; + const svgElement = select(ref.current); + svgElement.selectAll('*').remove(); - if (right > chartWidth) { - const maxWidth = textRect.width - (right - chartWidth) * 2; - select(node).call(setEllipsisForOverflowText, maxWidth); - } - }); + svgElement + .call(xAxisGenerator) + .attr('class', b()) + .style('font-size', axis.labels.style.fontSize); // add an axis header if necessary if (axis.title.text) { diff --git a/src/plugins/d3/renderer/components/Chart.tsx b/src/plugins/d3/renderer/components/Chart.tsx index 3b92b784..43f40095 100644 --- a/src/plugins/d3/renderer/components/Chart.tsx +++ b/src/plugins/d3/renderer/components/Chart.tsx @@ -112,7 +112,6 @@ export const Chart = (props: Props) => { width={boundsWidth} height={boundsHeight} scale={xScale} - chartWidth={width} /> diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-x.tsx b/src/plugins/d3/renderer/hooks/useShapes/bar-x.tsx index be30be7c..02d184da 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-x.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-x.tsx @@ -13,6 +13,7 @@ import type {OnSeriesMouseLeave, OnSeriesMouseMove} from '../useTooltip/types'; import type {PreparedBarXSeries} from '../useSeries/types'; import {DEFAULT_BAR_X_SERIES_OPTIONS} from './defaults'; +const MIN_RECT_WIDTH = 1; const MIN_RECT_GAP = 1; const MIN_GROUP_GAP = 1; const DEFAULT_LABEL_PADDING = 7; @@ -130,7 +131,10 @@ function prepareData(args: { const groupGap = Math.max(bandWidth * groupPadding, MIN_GROUP_GAP); const groupWidth = bandWidth - groupGap; const rectGap = Math.max(bandWidth * barPadding, MIN_RECT_GAP); - const rectWidth = Math.min(groupWidth / maxGroupSize - rectGap, barMaxWidth); + const rectWidth = Math.max( + MIN_RECT_WIDTH, + Math.min(groupWidth / maxGroupSize - rectGap, barMaxWidth), + ); const result: ShapeData[] = []; diff --git a/src/plugins/d3/renderer/utils/axis-generators/bottom.ts b/src/plugins/d3/renderer/utils/axis-generators/bottom.ts index afb126af..7ddc8ea1 100644 --- a/src/plugins/d3/renderer/utils/axis-generators/bottom.ts +++ b/src/plugins/d3/renderer/utils/axis-generators/bottom.ts @@ -1,7 +1,8 @@ import type {AxisDomain, AxisScale, Selection} from 'd3'; -import {getXAxisItems, getXAxisOffset, getXTickPosition} from '../axis'; +import {select} from 'd3'; import {BaseTextStyle} from '../../../../../types'; -import {hasOverlappingLabels} from '../text'; +import {getXAxisItems, getXAxisOffset, getXTickPosition} from '../axis'; +import {hasOverlappingLabels, setEllipsisForOverflowText} from '../text'; type AxisBottomArgs = { scale: AxisScale; @@ -61,6 +62,9 @@ export function axisBottom(args: AxisBottomArgs) { const values = getXAxisItems({scale, count: ticksCount, maxCount: maxTickCount}); return function (selection: Selection) { + const x = selection.node()?.getBoundingClientRect()?.x || 0; + const right = x + domainSize; + selection .selectAll('.tick') .data(values) @@ -80,6 +84,15 @@ export function axisBottom(args: AxisBottomArgs) { return `translate(${position(d as AxisDomain) + offset},0)`; }); + // Remove tick that has the same x coordinate like domain + selection + .select('.tick') + .filter((d) => { + return position(d as AxisDomain) === 0; + }) + .select('line') + .remove(); + const labels = selection.selectAll('.tick text'); const labelNodes = labels.nodes() as SVGTextElement[]; @@ -90,34 +103,64 @@ export function axisBottom(args: AxisBottomArgs) { style: labelsStyle, }); - if (overlapping) { - if (autoRotation) { - const labelHeight = labelNodes[0]?.getBoundingClientRect()?.height; - const labelOffset = (labelHeight / 2 + labelsMargin) / 2; - labels - .attr('text-anchor', 'end') - .attr('transform', `rotate(-45) translate(-${labelOffset}, -${labelOffset})`); - } else { - // remove overlapping labels - let elementX = 0; - selection - .selectAll('.tick') - .filter(function () { - const node = this as unknown as Element; - const r = node.getBoundingClientRect(); - - if (r.left < elementX) { - return true; - } - elementX = r.right + labelsPaddings; - return false; - }) - .remove(); - } - } + const rotationAngle = overlapping && autoRotation ? '-45' : undefined; + + if (rotationAngle) { + const labelHeight = labelNodes[0]?.getBoundingClientRect()?.height; + const labelOffset = (labelHeight / 2 + labelsMargin) / 2; + labels + .attr('text-anchor', 'end') + .attr( + 'transform', + `rotate(${rotationAngle}) translate(-${labelOffset}, -${labelOffset})`, + ); + } else { + // remove overlapping labels + let elementX = 0; + selection + .selectAll('.tick') + .filter(function () { + const node = this as unknown as Element; + const r = node.getBoundingClientRect(); - selection.call(addDomain, {size: domainSize, color: domainColor}); + if (r.left < elementX) { + return true; + } + elementX = r.right + labelsPaddings; + return false; + }) + .remove(); - selection.attr('text-anchor', 'middle').style('font-size', labelsStyle?.fontSize || ''); + // add an ellipsis to the labels that go beyond the boundaries of the chart + labels.each(function (_d, i, nodes) { + if (i === nodes.length - 1) { + const currentElement = this as SVGTextElement; + const prevElement = nodes[i - 1] as SVGTextElement; + const text = select(currentElement); + + const currentElementPosition = currentElement.getBoundingClientRect(); + const prevElementPosition = prevElement?.getBoundingClientRect(); + + const lackingSpace = Math.max(0, currentElementPosition.right - right); + if (lackingSpace) { + const remainSpace = + right - (prevElementPosition?.right || 0) - labelsPaddings; + + const translateX = currentElementPosition.width / 2 - lackingSpace; + text.attr('text-anchor', 'end').attr( + 'transform', + `translate(${translateX},0)`, + ); + + setEllipsisForOverflowText(text, remainSpace); + } + } + }); + } + + selection + .call(addDomain, {size: domainSize, color: domainColor}) + .attr('text-anchor', 'middle') + .style('font-size', labelsStyle?.fontSize || ''); }; } diff --git a/src/plugins/d3/renderer/utils/axis.ts b/src/plugins/d3/renderer/utils/axis.ts index 2b1ab80e..1a0927bc 100644 --- a/src/plugins/d3/renderer/utils/axis.ts +++ b/src/plugins/d3/renderer/utils/axis.ts @@ -50,20 +50,13 @@ export function getXAxisItems({ count?: number; maxCount: number; }) { - const offset = getXAxisOffset(); let values = getScaleTicks(scale, count); - const position = getXTickPosition({scale, offset}); if (values.length > maxCount) { const step = Math.ceil(values.length / maxCount); values = values.filter((_: AxisDomain, i: number) => i % step === 0); } - // Remove tick that has the same x coordinate like domain - if (values.length && position(values[0]) === 0) { - values = values.slice(1); - } - return values; } diff --git a/src/plugins/d3/renderer/utils/text.ts b/src/plugins/d3/renderer/utils/text.ts index 48eabd26..5b1bf1d4 100644 --- a/src/plugins/d3/renderer/utils/text.ts +++ b/src/plugins/d3/renderer/utils/text.ts @@ -7,7 +7,7 @@ export function setEllipsisForOverflowText( maxWidth: number, ) { let text = selection.text(); - selection.text(null).attr('text-anchor', 'left').append('title').text(text); + selection.text(null).append('title').text(text); const tSpan = selection.append('tspan').text(text); let textLength = tSpan.node()?.getComputedTextLength() || 0; From 40c2409a46352227f2159c63db9dab8d583ad0d5 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Wed, 20 Sep 2023 00:31:53 +0300 Subject: [PATCH 2/2] fix story --- .../__stories__/bar-x/LinearAxis.stories.tsx | 361 +----------------- 1 file changed, 14 insertions(+), 347 deletions(-) diff --git a/src/plugins/d3/__stories__/bar-x/LinearAxis.stories.tsx b/src/plugins/d3/__stories__/bar-x/LinearAxis.stories.tsx index 3cff28a6..9d7c516c 100644 --- a/src/plugins/d3/__stories__/bar-x/LinearAxis.stories.tsx +++ b/src/plugins/d3/__stories__/bar-x/LinearAxis.stories.tsx @@ -19,359 +19,26 @@ const Template: Story = () => { visible: true, data: [ { - y: 1, - x: -1523, + x: 10, + y: 100, }, { - y: 1, - x: -1000, - }, - { - y: 1, - x: -660, - }, - { - y: 1, - x: 800, - }, - { - y: 1, - x: 836, - }, - { - y: 1, - x: 843, - }, - { - y: 1, - x: 885, - }, - { - y: 1, - x: 1066, - }, - { - y: 1, - x: 1143, - }, - { - y: 1, - x: 1278, - }, - { - y: 1, - x: 1350, - }, - { - y: 1, - x: 1492, - }, - { - y: 1, - x: 1499, - }, - { - y: 1, - x: 1581, - }, - { - y: 1, - x: 1769, - }, - { - y: 1, - x: 1776, - }, - { - y: 1, - x: 1804, - }, - { - y: 1, - x: 1806, - }, - { - y: 2, - x: 1810, - }, - { - y: 1, - x: 1811, - }, - { - y: 1, - x: 1816, - }, - { - y: 2, - x: 1821, - }, - { - y: 1, - x: 1822, - }, - { - y: 1, - x: 1825, - }, - { - y: 1, - x: 1828, - }, - { - y: 2, - x: 1830, - }, - { - y: 1, - x: 1838, - }, - { - y: 1, - x: 1841, - }, - { - y: 1, - x: 1844, - }, - { - y: 1, - x: 1847, - }, - { - y: 2, - x: 1861, - }, - { - y: 2, - x: 1867, - }, - { - y: 1, - x: 1878, - }, - { - y: 1, - x: 1901, - }, - { - y: 1, - x: 1902, - }, - { - y: 1, - x: 1903, - }, - { - y: 1, - x: 1905, - }, - { - y: 1, - x: 1906, - }, - { - y: 1, - x: 1907, - }, - { - y: 1, - x: 1908, - }, - { - y: 2, - x: 1910, - }, - { - y: 1, - x: 1912, - }, - { - y: 1, - x: 1917, - }, - { - y: 4, - x: 1918, - }, - { - y: 1, - x: 1919, - }, - { - y: 2, - x: 1921, - }, - { - y: 1, - x: 1922, - }, - { - y: 1, - x: 1923, - }, - { - y: 1, - x: 1929, - }, - { - y: 1, - x: 1932, - }, - { - y: 1, - x: 1941, - }, - { - y: 1, - x: 1944, - }, - { - y: 2, - x: 1945, - }, - { - y: 2, - x: 1946, - }, - { - y: 1, - x: 1947, - }, - { - y: 4, - x: 1948, - }, - { - y: 2, - x: 1951, - }, - { - y: 1, - x: 1953, - }, - { - y: 1, - x: 1955, - }, - { - y: 1, - x: 1956, - }, - { - y: 2, - x: 1957, - }, - { - y: 1, - x: 1958, - }, - { - y: 4, - x: 1960, - }, - { - y: 3, - x: 1961, - }, - { - y: 4, - x: 1962, - }, - { - y: 1, - x: 1963, - }, - { - y: 2, - x: 1964, - }, - { - y: 3, - x: 1965, - }, - { - y: 3, - x: 1966, - }, - { - y: 4, - x: 1968, - }, - { - y: 2, - x: 1970, - }, - { - y: 2, - x: 1971, - }, - { - y: 1, - x: 1973, - }, - { - y: 2, - x: 1974, - }, - { - y: 5, - x: 1975, - }, - { - y: 1, - x: 1976, - }, - { - y: 1, - x: 1977, - }, - { - y: 3, - x: 1978, - }, - { - y: 2, - x: 1979, - }, - { - y: 2, - x: 1980, - }, - { - y: 2, - x: 1981, - }, - { - y: 1, - x: 1983, - }, - { - y: 1, - x: 1984, - }, - { - y: 2, - x: 1990, - }, - { - y: 5, - x: 1991, - }, - { - y: 1, - x: 1992, - }, - { - y: 2, - x: 1993, + x: 12, + y: 80, }, + ], + name: 'AB', + }, + { + type: 'bar-x', + visible: true, + data: [ { - y: 1, - x: 1994, + x: 95.5, + y: 120, }, ], - name: 'AB', + name: 'C', }, ], },