Skip to content

Commit bc1db9c

Browse files
slizhevskyv-semrushValeria-Zimnitskayaj-mnizhek
authored
[UIK-4320][d3-chart] added support for multiline tick labels (#2525)
<!--- Provide a general summary of your changes in the Title above --> ## Motivation and Context Added support for multiline ticks for chart axises. ## How has this been tested? I've added a browser test. ## Screenshots (if appropriate): ## Types of changes <!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue). - [x] New feature (non-breaking change which adds functionality). - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected). - [ ] Nice improve. ## Checklist: <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> <!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> - [ ] I have updated the documentation accordingly. - [x] I have added changelog note to corresponding `CHANGELOG.md` file with planned publish date. - [x] I have added new tests on added of fixed functionality. --------- Co-authored-by: Valeryia Zimnitskaya <[email protected]> Co-authored-by: Julia Mnizhek <[email protected]>
1 parent ed50f48 commit bc1db9c

18 files changed

+307
-34
lines changed

semcore/d3-chart/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

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

5+
## [16.3.0] - 2025-10-17
6+
7+
### Added
8+
9+
- New `multiline` property for `XAxis.Ticks/YAxis.Ticks` and `multilineXTicks/multilineYTicks` for `Chart`.
10+
11+
### Fixed
12+
13+
- Chart content remains visible after unchecking single legend item in Bar, Horizontal Bar, Histogram, and Stacked Horizontal Bar charts.
14+
515
## [16.2.1] - 2025-10-17
616

717
### Fixed

semcore/d3-chart/__tests__/bar-chart.browser-test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,23 @@ test.describe('Bar chart', () => {
297297
await page.waitForTimeout(500);
298298
await expect(page).toHaveScreenshot();
299299
});
300+
301+
test('Verify multiline tick labels', async ({ page }) => {
302+
const standPath = 'stories/components/d3-chart/tests/examples/bar-chart/basic-usage.tsx';
303+
const htmlContent = await e2eStandToHtml(standPath, 'en', {
304+
data: [
305+
{ category: 'Google AI Mode 0 Top', bar: 2 },
306+
{ category: 'Google AI Mode 1 Top', bar: 5 },
307+
{ category: 'Google AI Mode 2 Top', bar: 7 },
308+
{ category: 'Google AI Mode 3 Top', bar: 4 },
309+
{ category: 'Google AI Mode 4 Top', bar: 8 },
310+
],
311+
multilineXTicks: true,
312+
marginX: 60,
313+
});
314+
315+
await page.setContent(htmlContent);
316+
317+
await expect(page).toHaveScreenshot();
318+
});
300319
});
14.9 KB
Loading
34.6 KB
Loading
11.4 KB
Loading
Loading
Loading
Loading

semcore/d3-chart/src/Axis.jsx

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component, sstyled } from '@semcore/core';
2-
import React from 'react';
2+
import React, { useState, useEffect } from 'react';
33

44
import createElement from './createElement';
55
import style from './style/axis.shadow.css';
@@ -186,6 +186,54 @@ function renderValue(value) {
186186
return value;
187187
}
188188

189+
function splitTextByWidth(root, text, maxWidth) {
190+
if (!text || !maxWidth || maxWidth <= 0) return [];
191+
192+
const words = text.split(/\s+/).filter((word) => word.length > 0);
193+
if (words.length === 0) return [];
194+
195+
const lines = [];
196+
let currentLine = words[0];
197+
198+
for (let i = 1; i < words.length; i++) {
199+
const testLine = `${currentLine} ${words[i]}`.trim();
200+
const testWidth = measureTextWidth(root, testLine);
201+
202+
if (testWidth <= maxWidth) {
203+
currentLine = testLine;
204+
} else {
205+
if (currentLine) {
206+
lines.push(currentLine);
207+
}
208+
209+
currentLine = words[i];
210+
211+
if (measureTextWidth(root, currentLine) > maxWidth) {
212+
lines.push(currentLine);
213+
currentLine = '';
214+
}
215+
}
216+
}
217+
218+
if (currentLine) {
219+
lines.push(currentLine);
220+
}
221+
222+
return lines;
223+
}
224+
225+
function measureTextWidth(rootRef, text, fontSize = 12) {
226+
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
227+
textEl.setAttribute('font-size', fontSize);
228+
textEl.setAttribute('visibility', 'hidden');
229+
textEl.textContent = text;
230+
231+
rootRef.appendChild(textEl);
232+
const width = textEl.getComputedTextLength();
233+
rootRef.removeChild(textEl);
234+
return width;
235+
}
236+
189237
class AxisRoot extends Component {
190238
static displayName = 'Axis';
191239

@@ -246,8 +294,31 @@ function Ticks(props) {
246294
dataHintsHandler,
247295
children,
248296
childrenPosition = 'inside',
297+
rootRef,
298+
multiline,
249299
} = props;
250300

301+
const [ticksState, setTicksState] = useState([]);
302+
303+
useEffect(() => {
304+
const tickBandwidth = scale[indexScale]?.bandwidth?.();
305+
306+
const ticksWithLines = ticks.map((tick) => {
307+
let lines = [];
308+
309+
if (typeof tick === 'string' && multiline) {
310+
lines = splitTextByWidth(rootRef.current, tick, tickBandwidth);
311+
}
312+
313+
return {
314+
tick,
315+
lines,
316+
};
317+
});
318+
319+
setTicksState(ticksWithLines);
320+
}, [ticks, multiline]);
321+
251322
const pos = MAP_POSITION_TICK[position] ?? MAP_POSITION_TICK[MAP_INDEX_SCALE_SYMBOL[indexScale]];
252323
const positionClass = MAP_POSITION_TICK[position] ? position : `custom_${indexScale}`;
253324

@@ -263,7 +334,7 @@ function Ticks(props) {
263334
}
264335
}
265336

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

269340
return sstyled(styles)(
@@ -277,9 +348,14 @@ function Ticks(props) {
277348
index={i}
278349
position={positionClass}
279350
hide={hide}
351+
multiline={multiline}
280352
{...pos(scale, value, position)}
281353
>
282-
{displayValue}
354+
{ lines.length > 1
355+
? lines.map((line, lineIndex) => (
356+
<tspan key={line} {...pos(scale, value, position)} dy={lineIndex * 15}>{line}</tspan>
357+
))
358+
: displayValue}
283359
</STick>,
284360
);
285361
});

semcore/d3-chart/src/component/Chart/AbstractChart.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,16 @@ export abstract class AbstractChart<
401401
}
402402

403403
protected renderAxis(): React.ReactNode {
404-
const { invertAxis, showXAxis, showYAxis, data, axisXValueFormatter, axisYValueFormatter } =
405-
this.asProps;
404+
const {
405+
invertAxis,
406+
showXAxis,
407+
showYAxis,
408+
data,
409+
axisXValueFormatter,
410+
axisYValueFormatter,
411+
multilineXTicks,
412+
multilineYTicks,
413+
} = this.asProps;
406414

407415
if (!Array.isArray(data)) {
408416
return null;
@@ -424,10 +432,10 @@ export abstract class AbstractChart<
424432
<YAxis>
425433
{yTicks
426434
? (
427-
<YAxis.Ticks ticks={yTicks}>{childrenY}</YAxis.Ticks>
435+
<YAxis.Ticks multiline={multilineYTicks} ticks={yTicks}>{childrenY}</YAxis.Ticks>
428436
)
429437
: (
430-
<YAxis.Ticks>{childrenY}</YAxis.Ticks>
438+
<YAxis.Ticks multiline={multilineYTicks}>{childrenY}</YAxis.Ticks>
431439
)}
432440
{invertAxis !== true && (yTicks ? <YAxis.Grid ticks={yTicks} /> : <YAxis.Grid />)}
433441
</YAxis>
@@ -437,10 +445,10 @@ export abstract class AbstractChart<
437445
<XAxis>
438446
{xTicks
439447
? (
440-
<XAxis.Ticks ticks={xTicks}>{childrenX}</XAxis.Ticks>
448+
<XAxis.Ticks multiline={multilineXTicks} ticks={xTicks}>{childrenX}</XAxis.Ticks>
441449
)
442450
: (
443-
<XAxis.Ticks>{childrenX}</XAxis.Ticks>
451+
<XAxis.Ticks multiline={multilineXTicks}>{childrenX}</XAxis.Ticks>
444452
)}
445453
{invertAxis === true && (xTicks ? <XAxis.Grid ticks={xTicks} /> : <XAxis.Grid />)}
446454
</XAxis>

0 commit comments

Comments
 (0)