Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(D3 plugin): bar-x min column width #294

Merged
merged 2 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 7 additions & 22 deletions src/plugins/d3/renderer/components/AxisX.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ type Props = {
width: number;
height: number;
scale: ChartScale;
chartWidth: number;
};

function getLabelFormatter({axis, scale}: {axis: PreparedAxis; scale: ChartScale}) {
Expand All @@ -42,17 +41,14 @@ 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<SVGGElement>(null);

React.useEffect(() => {
if (!ref.current) {
return;
}

const svgElement = select(ref.current);
svgElement.selectAll('*').remove();

const xAxisGenerator = axisBottom({
scale: scale as AxisScale<AxisDomain>,
ticks: {
Expand All @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion src/plugins/d3/renderer/components/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ export const Chart = (props: Props) => {
width={boundsWidth}
height={boundsHeight}
scale={xScale}
chartWidth={width}
/>
</g>
</React.Fragment>
Expand Down
6 changes: 5 additions & 1 deletion src/plugins/d3/renderer/hooks/useShapes/bar-x.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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[] = [];

Expand Down
101 changes: 72 additions & 29 deletions src/plugins/d3/renderer/utils/axis-generators/bottom.ts
Original file line number Diff line number Diff line change
@@ -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<AxisDomain>;
Expand Down Expand Up @@ -61,6 +62,9 @@ export function axisBottom(args: AxisBottomArgs) {
const values = getXAxisItems({scale, count: ticksCount, maxCount: maxTickCount});

return function (selection: Selection<SVGGElement, unknown, null, undefined>) {
const x = selection.node()?.getBoundingClientRect()?.x || 0;
const right = x + domainSize;

selection
.selectAll('.tick')
.data(values)
Expand All @@ -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[];

Expand All @@ -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 || '');
};
}
7 changes: 0 additions & 7 deletions src/plugins/d3/renderer/utils/axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/plugins/d3/renderer/utils/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading