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;