diff --git a/semcore/d3-chart/CHANGELOG.md b/semcore/d3-chart/CHANGELOG.md
index 4e71318513..eb8926c85a 100644
--- a/semcore/d3-chart/CHANGELOG.md
+++ b/semcore/d3-chart/CHANGELOG.md
@@ -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
diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx
index ff5df0665d..0324ba1ea1 100644
--- a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx
+++ b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx
@@ -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();
+ });
});
diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/Bar-chart-Verify-multiline-tick-labels-1-chromium-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/Bar-chart-Verify-multiline-tick-labels-1-chromium-linux.png
new file mode 100644
index 0000000000..2856ef9494
Binary files /dev/null and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/Bar-chart-Verify-multiline-tick-labels-1-chromium-linux.png differ
diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/Bar-chart-Verify-multiline-tick-labels-1-firefox-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/Bar-chart-Verify-multiline-tick-labels-1-firefox-linux.png
new file mode 100644
index 0000000000..07b0c3c1e1
Binary files /dev/null and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/Bar-chart-Verify-multiline-tick-labels-1-firefox-linux.png differ
diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/Bar-chart-Verify-multiline-tick-labels-1-webkit-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/Bar-chart-Verify-multiline-tick-labels-1-webkit-linux.png
new file mode 100644
index 0000000000..ee65a5f6bb
Binary files /dev/null and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/Bar-chart-Verify-multiline-tick-labels-1-webkit-linux.png differ
diff --git a/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-chromium-linux.png b/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-chromium-linux.png
index af84d4dd89..b5e48fe2d6 100644
Binary files a/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-chromium-linux.png and b/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-chromium-linux.png differ
diff --git a/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-firefox-linux.png b/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-firefox-linux.png
index 3ebb905d80..08841e7334 100644
Binary files a/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-firefox-linux.png and b/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-firefox-linux.png differ
diff --git a/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-webkit-linux.png b/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-webkit-linux.png
index 03f211c3c4..2615abc9ad 100644
Binary files a/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-webkit-linux.png and b/semcore/d3-chart/__tests__/bar-horizontal-chart.browser-test.tsx-snapshots/Horizontal-Bar-chart-Verify-Chart-Bar-renders-and-tooltip-shown-on-hover-1-webkit-linux.png differ
diff --git a/semcore/d3-chart/src/Axis.jsx b/semcore/d3-chart/src/Axis.jsx
index d829883822..192b8995f2 100644
--- a/semcore/d3-chart/src/Axis.jsx
+++ b/semcore/d3-chart/src/Axis.jsx
@@ -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';
@@ -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';
@@ -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}`;
@@ -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)(
@@ -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) => (
+ {line}
+ ))
+ : displayValue}
,
);
});
diff --git a/semcore/d3-chart/src/component/Chart/AbstractChart.tsx b/semcore/d3-chart/src/component/Chart/AbstractChart.tsx
index 3813c0beea..bace577f42 100644
--- a/semcore/d3-chart/src/component/Chart/AbstractChart.tsx
+++ b/semcore/d3-chart/src/component/Chart/AbstractChart.tsx
@@ -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;
@@ -424,10 +432,10 @@ export abstract class AbstractChart<
{yTicks
? (
- {childrenY}
+ {childrenY}
)
: (
- {childrenY}
+ {childrenY}
)}
{invertAxis !== true && (yTicks ? : )}
@@ -437,10 +445,10 @@ export abstract class AbstractChart<
{xTicks
? (
- {childrenX}
+ {childrenX}
)
: (
- {childrenX}
+ {childrenX}
)}
{invertAxis === true && (xTicks ? : )}
diff --git a/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts b/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts
index a4971ac17a..dc4606fdbb 100644
--- a/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts
+++ b/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts
@@ -124,6 +124,10 @@ export type BaseChartProps = 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)
*/
diff --git a/semcore/d3-chart/src/style/axis.shadow.css b/semcore/d3-chart/src/style/axis.shadow.css
index 83269a9452..8b6729dc9c 100644
--- a/semcore/d3-chart/src/style/axis.shadow.css
+++ b/semcore/d3-chart/src/style/axis.shadow.css
@@ -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;
+ }
}
diff --git a/semcore/d3-chart/src/types/Axis.d.ts b/semcore/d3-chart/src/types/Axis.d.ts
index 2be5914ef0..f25bf81597 100644
--- a/semcore/d3-chart/src/types/Axis.d.ts
+++ b/semcore/d3-chart/src/types/Axis.d.ts
@@ -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 */
diff --git a/stories/components/d3-chart/tests/bar-chart.stories.tsx b/stories/components/d3-chart/tests/bar-chart.stories.tsx
index e25ab7eb46..0253cf9f44 100644
--- a/stories/components/d3-chart/tests/bar-chart.stories.tsx
+++ b/stories/components/d3-chart/tests/bar-chart.stories.tsx
@@ -36,6 +36,20 @@ export const BasicUsage: StoryObj = {
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,
};
diff --git a/stories/components/d3-chart/tests/d3-chart-base.stories.tsx b/stories/components/d3-chart/tests/d3-chart-base.stories.tsx
index 6f204af4b2..76934e4b6c 100644
--- a/stories/components/d3-chart/tests/d3-chart-base.stories.tsx
+++ b/stories/components/d3-chart/tests/d3-chart-base.stories.tsx
@@ -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';
@@ -16,8 +16,15 @@ export const PlotAndA11yProps: StoryObj = {
render: PlotAndA11yPropsExample,
};
-export const GridAxisProps: StoryObj = {
+export const GridAxisProps: StoryObj = {
render: GridAxisPropsExample,
+ argTypes: {
+ multiline: {
+ control: 'select',
+ options: [true, false, undefined],
+ },
+ },
+ args: BasicUsageProps,
};
export const ReferenceLineProps: StoryObj = {
diff --git a/stories/components/d3-chart/tests/examples/bar-chart/basic-usage.tsx b/stories/components/d3-chart/tests/examples/bar-chart/basic-usage.tsx
index 805aaa027a..c0b27ef635 100644
--- a/stories/components/d3-chart/tests/examples/bar-chart/basic-usage.tsx
+++ b/stories/components/d3-chart/tests/examples/bar-chart/basic-usage.tsx
@@ -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 */}
-
>
);
};
-const data = [
+const defaultData = [
{ category: 'Category 0', bar: 2 },
{ category: 'Category 1', bar: 5 },
{ category: 'Category 2', bar: 7 },
@@ -31,7 +44,10 @@ const data = [
export const defaultProps: BaseExampleProps = {
showLegend: undefined,
-
+ multilineXTicks: undefined,
+ marginX: undefined,
+ multilineYTicks: undefined,
+ marginY: undefined,
};
export default Demo;
diff --git a/stories/components/d3-chart/tests/examples/bar-horizontal/basic-usage.tsx b/stories/components/d3-chart/tests/examples/bar-horizontal/basic-usage.tsx
index df23ab0e33..9932c166cc 100644
--- a/stories/components/d3-chart/tests/examples/bar-horizontal/basic-usage.tsx
+++ b/stories/components/d3-chart/tests/examples/bar-horizontal/basic-usage.tsx
@@ -17,6 +17,8 @@ const Demo = (props: BaseExampleProps) => {
invertAxis={true}
aria-label='CompactHorizontalBar chart'
showLegend={showLegend}
+ multilineYTicks={true}
+ marginY={100}
/>
>
);
diff --git a/stories/components/d3-chart/tests/examples/d3-chart/grid-axis-props.tsx b/stories/components/d3-chart/tests/examples/d3-chart/grid-axis-props.tsx
index ce018b9082..29b5d2b7c3 100644
--- a/stories/components/d3-chart/tests/examples/d3-chart/grid-axis-props.tsx
+++ b/stories/components/d3-chart/tests/examples/d3-chart/grid-axis-props.tsx
@@ -3,7 +3,11 @@ import { Flex } from '@semcore/ui/flex-box';
import { scaleLinear, scaleBand } from 'd3-scale';
import React from 'react';
-const Demo = () => {
+type BaseExampleProps = {
+ multiline?: boolean;
+};
+
+const Demo = (props: BaseExampleProps) => {
const MARGIN = 40;
const width = 250;
const height = 200;
@@ -37,17 +41,29 @@ const Demo = () => {
return (
-
+
-
+
-
+
@@ -57,7 +73,12 @@ const Demo = () => {
-
+
@@ -71,7 +92,12 @@ const Demo = () => {
-
+
@@ -88,40 +114,73 @@ const Demo = () => {
-
+
-
+
YAxis title
-
+
XAxis title
-
+
-
+
YAxis title
-
+
XAxis title
-
+
YAxis title
-
+
XAxis title
@@ -129,7 +188,12 @@ const Demo = () => {
-
+
@@ -142,7 +206,12 @@ const Demo = () => {
-
+
@@ -154,7 +223,12 @@ const Demo = () => {
-
+
@@ -179,8 +253,13 @@ const data = Array(20)
const data2 = Array(5)
.fill({})
.map((d, i) => ({
- category: `Cat ${i}`,
+ category: `Cat Cat Cat ${i}`,
bar: Math.sin(i / 2) * 5 + 5,
}));
+export const defaultProps: BaseExampleProps = {
+
+ multiline: undefined,
+};
+
export default Demo;