Skip to content
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
10 changes: 10 additions & 0 deletions semcore/d3-chart/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

CHANGELOG.md standards are inspired by [keepachangelog.com](https://keepachangelog.com/en/1.0.0/).

## [16.3.0] - 2025-10-17

### Added

- New `multiline` property for `XAxis.Ticks/YAxis.Ticks` and `multilineXTicks/multilineYTicks` for `Chart`.

### Fixed

- Chart content remains visible after unchecking single legend item in Bar, Horizontal Bar, Histogram, and Stacked Horizontal Bar charts.

## [16.2.1] - 2025-10-17

### Fixed
Expand Down
19 changes: 19 additions & 0 deletions semcore/d3-chart/__tests__/bar-chart.browser-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,23 @@ test.describe('Bar chart', () => {
await page.waitForTimeout(500);
await expect(page).toHaveScreenshot();
});

test('Verify multiline tick labels', async ({ page }) => {
const standPath = 'stories/components/d3-chart/tests/examples/bar-chart/basic-usage.tsx';
const htmlContent = await e2eStandToHtml(standPath, 'en', {
data: [
{ category: 'Google AI Mode 0 Top', bar: 2 },
{ category: 'Google AI Mode 1 Top', bar: 5 },
{ category: 'Google AI Mode 2 Top', bar: 7 },
{ category: 'Google AI Mode 3 Top', bar: 4 },
{ category: 'Google AI Mode 4 Top', bar: 8 },
],
multilineXTicks: true,
marginX: 60,
});

await page.setContent(htmlContent);

await expect(page).toHaveScreenshot();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 79 additions & 3 deletions semcore/d3-chart/src/Axis.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, sstyled } from '@semcore/core';
import React from 'react';
import React, { useState, useEffect } from 'react';

import createElement from './createElement';
import style from './style/axis.shadow.css';
Expand Down Expand Up @@ -186,6 +186,54 @@ function renderValue(value) {
return value;
}

function splitTextByWidth(root, text, maxWidth) {
if (!text || !maxWidth || maxWidth <= 0) return [];

const words = text.split(/\s+/).filter((word) => word.length > 0);
if (words.length === 0) return [];

const lines = [];
let currentLine = words[0];

for (let i = 1; i < words.length; i++) {
const testLine = `${currentLine} ${words[i]}`.trim();
const testWidth = measureTextWidth(root, testLine);

if (testWidth <= maxWidth) {
currentLine = testLine;
} else {
if (currentLine) {
lines.push(currentLine);
}

currentLine = words[i];

if (measureTextWidth(root, currentLine) > maxWidth) {
lines.push(currentLine);
currentLine = '';
}
}
}

if (currentLine) {
lines.push(currentLine);
}

return lines;
}

function measureTextWidth(rootRef, text, fontSize = 12) {
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
textEl.setAttribute('font-size', fontSize);
textEl.setAttribute('visibility', 'hidden');
textEl.textContent = text;

rootRef.appendChild(textEl);
const width = textEl.getComputedTextLength();
rootRef.removeChild(textEl);
return width;
}

class AxisRoot extends Component {
static displayName = 'Axis';

Expand Down Expand Up @@ -246,8 +294,31 @@ function Ticks(props) {
dataHintsHandler,
children,
childrenPosition = 'inside',
rootRef,
multiline,
} = props;

const [ticksState, setTicksState] = useState([]);

useEffect(() => {
const tickBandwidth = scale[indexScale]?.bandwidth?.();

const ticksWithLines = ticks.map((tick) => {
let lines = [];

if (typeof tick === 'string' && multiline) {
lines = splitTextByWidth(rootRef.current, tick, tickBandwidth);
}

return {
tick,
lines,
};
});

setTicksState(ticksWithLines);
}, [ticks, multiline]);

const pos = MAP_POSITION_TICK[position] ?? MAP_POSITION_TICK[MAP_INDEX_SCALE_SYMBOL[indexScale]];
const positionClass = MAP_POSITION_TICK[position] ? position : `custom_${indexScale}`;

Expand All @@ -263,7 +334,7 @@ function Ticks(props) {
}
}

return ticks.map((value, i) => {
return ticksState.map(({ tick: value, lines }, i) => {
const displayValue = typeof children === 'function' ? undefined : renderValue(value);

return sstyled(styles)(
Expand All @@ -277,9 +348,14 @@ function Ticks(props) {
index={i}
position={positionClass}
hide={hide}
multiline={multiline}
{...pos(scale, value, position)}
>
{displayValue}
{ lines.length > 1
? lines.map((line, lineIndex) => (
<tspan key={line} {...pos(scale, value, position)} dy={lineIndex * 15}>{line}</tspan>
))
: displayValue}
</STick>,
);
});
Expand Down
20 changes: 14 additions & 6 deletions semcore/d3-chart/src/component/Chart/AbstractChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,16 @@ export abstract class AbstractChart<
}

protected renderAxis(): React.ReactNode {
const { invertAxis, showXAxis, showYAxis, data, axisXValueFormatter, axisYValueFormatter } =
this.asProps;
const {
invertAxis,
showXAxis,
showYAxis,
data,
axisXValueFormatter,
axisYValueFormatter,
multilineXTicks,
multilineYTicks,
} = this.asProps;

if (!Array.isArray(data)) {
return null;
Expand All @@ -424,10 +432,10 @@ export abstract class AbstractChart<
<YAxis>
{yTicks
? (
<YAxis.Ticks ticks={yTicks}>{childrenY}</YAxis.Ticks>
<YAxis.Ticks multiline={multilineYTicks} ticks={yTicks}>{childrenY}</YAxis.Ticks>
)
: (
<YAxis.Ticks>{childrenY}</YAxis.Ticks>
<YAxis.Ticks multiline={multilineYTicks}>{childrenY}</YAxis.Ticks>
)}
{invertAxis !== true && (yTicks ? <YAxis.Grid ticks={yTicks} /> : <YAxis.Grid />)}
</YAxis>
Expand All @@ -437,10 +445,10 @@ export abstract class AbstractChart<
<XAxis>
{xTicks
? (
<XAxis.Ticks ticks={xTicks}>{childrenX}</XAxis.Ticks>
<XAxis.Ticks multiline={multilineXTicks} ticks={xTicks}>{childrenX}</XAxis.Ticks>
)
: (
<XAxis.Ticks>{childrenX}</XAxis.Ticks>
<XAxis.Ticks multiline={multilineXTicks}>{childrenX}</XAxis.Ticks>
)}
{invertAxis === true && (xTicks ? <XAxis.Grid ticks={xTicks} /> : <XAxis.Grid />)}
</XAxis>
Expand Down
4 changes: 4 additions & 0 deletions semcore/d3-chart/src/component/Chart/AbstractChart.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ export type BaseChartProps<T extends ListData | ObjectData> = FlexProps &
* Count of ticks for Y axis
*/
yTicksCount?: number;
/** Enables multiline tick labels for X axis, applicable only for band scales */
multilineXTicks?: boolean;
/** Enables multiline tick labels for Y axis, applicable only for band scales */
multilineYTicks?: boolean;
/**
* Group key for all array-based charts (for get keys of items for legend except that group key)
*/
Expand Down
36 changes: 36 additions & 0 deletions semcore/d3-chart/src/style/axis.shadow.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,64 @@ STick[position='bottom'] {
transform: translateY(12px);
text-anchor: middle;
dominant-baseline: hanging;

tspan {
dominant-baseline: hanging;
}
}

STick[position='right'] {
transform: translateX(16px);
text-anchor: start;
dominant-baseline: middle;

tspan {
dominant-baseline: middle;
}

&[multiline] {
dominant-baseline: auto;

tspan {
dominant-baseline: auto;
}
}
}

STick[position='left'] {
transform: translateX(-16px);
text-anchor: end;
dominant-baseline: middle;

tspan {
dominant-baseline: middle;
}

&[multiline] {
dominant-baseline: auto;

tspan {
dominant-baseline: auto;
}
}
}

STick[position='custom_0'] {
transform: translateY(12px);
text-anchor: middle;
dominant-baseline: hanging;

tspan {
dominant-baseline: hanging;
}
}

STick[position='custom_1'] {
transform: translateX(-16px);
text-anchor: end;
dominant-baseline: middle;

tspan {
dominant-baseline: middle;
}
}
2 changes: 2 additions & 0 deletions semcore/d3-chart/src/types/Axis.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export type AxisTicksProps = Context & {
hide?: boolean;
/** Values for axis ticks */
ticks?: any[];
/** Enables multiline tick labels, applicable only for band scales */
multiline?: boolean;
};

/** @deprecated */
Expand Down
14 changes: 14 additions & 0 deletions stories/components/d3-chart/tests/bar-chart.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ export const BasicUsage: StoryObj<typeof BasicUsageProps> = {
control: 'select',
options: [true, false, undefined],
},
multilineXTicks: {
control: 'select',
options: [true, false, undefined],
},
marginX: {
control: 'number',
},
multilineYTicks: {
control: 'select',
options: [true, false, undefined],
},
marginY: {
control: 'number',
},
},
args: BasicUsageProps,
};
Expand Down
11 changes: 9 additions & 2 deletions stories/components/d3-chart/tests/d3-chart-base.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite';

import AdaptivePropsExample from './examples/d3-chart/adaptive-props';
import GridAxisPropsExample from './examples/d3-chart/grid-axis-props';
import GridAxisPropsExample, { defaultProps as BasicUsageProps } from './examples/d3-chart/grid-axis-props';
import PlotAndA11yPropsExample from './examples/d3-chart/plot-props';
import ReferenceLinePropsExample from './examples/d3-chart/reference-line-props';
import TooltipHoverExample from './examples/d3-chart/tooltip-and-hover-line';
Expand All @@ -16,8 +16,15 @@ export const PlotAndA11yProps: StoryObj = {
render: PlotAndA11yPropsExample,
};

export const GridAxisProps: StoryObj = {
export const GridAxisProps: StoryObj<typeof BasicUsageProps> = {
render: GridAxisPropsExample,
argTypes: {
multiline: {
control: 'select',
options: [true, false, undefined],
},
},
args: BasicUsageProps,
};

export const ReferenceLineProps: StoryObj = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
import { Chart } from '@semcore/ui/d3-chart';
import React from 'react';

type BaseExampleProps = {
showLegend?: boolean;
multilineXTicks?: boolean;
marginX?: number;
multilineYTicks?: boolean;
marginY?: number;
data?: Array<{
category: string;
bar: number;
}>;
};

const Demo = (props: BaseExampleProps) => {
const { showLegend } = props;
const { showLegend, multilineXTicks, data, marginX, multilineYTicks, marginY } = props;
return (
<>
{ /* @ts-ignore: the value is not statically known, but it's valid at runtime */}

<Chart.Bar
groupKey='category'
data={data}
data={data ?? defaultData}
plotWidth={500}
plotHeight={300}
aria-label='Bar chart'
showLegend={showLegend}
multilineXTicks={multilineXTicks}
marginX={marginX}
multilineYTicks={multilineYTicks}
marginY={marginY}
/>
</>
);
};

const data = [
const defaultData = [
{ category: 'Category 0', bar: 2 },
{ category: 'Category 1', bar: 5 },
{ category: 'Category 2', bar: 7 },
Expand All @@ -31,7 +44,10 @@ const data = [

export const defaultProps: BaseExampleProps = {
showLegend: undefined,

multilineXTicks: undefined,
marginX: undefined,
multilineYTicks: undefined,
marginY: undefined,
};

export default Demo;
Loading
Loading