From b139e0049974b9b883ffa092fa94b72b19b0222d Mon Sep 17 00:00:00 2001 From: Sheila Sheikh <166654065+sheila-semrush@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:39:18 +0200 Subject: [PATCH 1/3] [d3-chart] update grid line styles --- semcore/d3-chart/src/style/axis.shadow.css | 5 ++++- semcore/d3-chart/src/style/radar.shadow.css | 4 ++-- tools/theme/src/theme.ts | 2 +- website/docs/style/design-tokens/design-tokens.json | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/semcore/d3-chart/src/style/axis.shadow.css b/semcore/d3-chart/src/style/axis.shadow.css index 44ba9bc553..4723fe0022 100644 --- a/semcore/d3-chart/src/style/axis.shadow.css +++ b/semcore/d3-chart/src/style/axis.shadow.css @@ -9,6 +9,7 @@ SAxis[hide] { STick { font-size: var(--intergalactic-fs-100, 12px); fill: var(--intergalactic-chart-grid-text-label, oklch(0.097 0.023 156.5 / 0.468)); + font-variant-numeric: tabular-nums; } STick[primaryText] { fill: var(--intergalactic-text-primary, oklch(0.1 0.03 137 / 0.899)); @@ -20,7 +21,9 @@ STick[hide] { SGrid { fill: transparent; - stroke: var(--intergalactic-border-secondary, oklch(0.176 0.033 175.7 / 0.07)); + stroke: var(--intergalactic-border-primary, oklch(0.137 0.026 175.7 / 0.161)); + stroke-dasharray: 0 4; + stroke-linecap: round; } STitle { diff --git a/semcore/d3-chart/src/style/radar.shadow.css b/semcore/d3-chart/src/style/radar.shadow.css index 0f9c593b09..1f94076777 100644 --- a/semcore/d3-chart/src/style/radar.shadow.css +++ b/semcore/d3-chart/src/style/radar.shadow.css @@ -1,12 +1,12 @@ SAxis { - stroke: var(--intergalactic-chart-grid-line, oklch(0.95 0.002 180)); + stroke: var(--intergalactic-chart-grid-line, oklch(0.88 0.002 175.6)); stroke-width: 1; fill: transparent; } SAxisLine, SAxisTick { - stroke: var(--intergalactic-chart-grid-line, oklch(0.95 0.002 180)); + stroke: var(--intergalactic-chart-grid-line, oklch(0.88 0.002 175.6)); stroke-width: 1; stroke-dasharray: 2; fill: transparent; diff --git a/tools/theme/src/theme.ts b/tools/theme/src/theme.ts index 72da812a77..a8feb96157 100644 --- a/tools/theme/src/theme.ts +++ b/tools/theme/src/theme.ts @@ -677,7 +677,7 @@ export const theme: Theme = { description: 'Border for distinguishing data sets and chart dots on the chart grid.', }, chart_grid_line: { - value: neutral.at(L_BORDER_SECONDARY), + value: neutral.at(L_BORDER_PRIMARY), description: 'Grid and axis guide lines for charts.', }, chart_grid_period_bg: { diff --git a/website/docs/style/design-tokens/design-tokens.json b/website/docs/style/design-tokens/design-tokens.json index df1e7d1053..73329bc40c 100644 --- a/website/docs/style/design-tokens/design-tokens.json +++ b/website/docs/style/design-tokens/design-tokens.json @@ -692,7 +692,7 @@ }, { "name": "--intergalactic-chart-grid-line", - "value": "oklch(0.95 0.002 180)", + "value": "oklch(0.88 0.002 175.6)", "description": "Grid and axis guide lines for charts.", "components": [ "d3-chart" From fb2588eb421ac813c8d13c3c38dc6335187c77ec Mon Sep 17 00:00:00 2001 From: Slizhevsky Vladislav Date: Wed, 9 Sep 2026 15:07:49 +0200 Subject: [PATCH 2/3] [UIK-5826][d3-chart] update default date formating for Axis --- semcore/d3-chart/src/Axis.jsx | 10 +++++++--- .../d3-chart/docs/examples/bar-chart/basic-usage.tsx | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/semcore/d3-chart/src/Axis.jsx b/semcore/d3-chart/src/Axis.jsx index 34d235ae12..e6582bfb04 100644 --- a/semcore/d3-chart/src/Axis.jsx +++ b/semcore/d3-chart/src/Axis.jsx @@ -179,9 +179,12 @@ const MAP_POSITION_TITlE = { }, }; -function renderValue(value) { +function renderValue(value, locale = 'en') { if (value instanceof Date) { - return value.toLocaleDateString(); + return new Intl.DateTimeFormat(locale, { + day: 'numeric', + month: 'short', + }).format(value); } return value; } @@ -296,6 +299,7 @@ function Ticks(props) { childrenPosition = 'inside', rootRef, multiline, + locale, } = props; const [rootRefElement, setRootRefElement] = useState(null); @@ -327,7 +331,7 @@ function Ticks(props) { } return ticksWithLines.map(({ tick: value, lines }, i) => { - const displayValue = typeof children === 'function' ? undefined : renderValue(value); + const displayValue = typeof children === 'function' ? undefined : renderValue(value, locale); return sstyled(styles)( { return ( { plotWidth={500} plotHeight={300} aria-label='Bar chart' + getLink={(index) => links[index]} /> ); }; From 76c708dd7802ac6262af99276bd7bc35ae7c059c Mon Sep 17 00:00:00 2001 From: Slizhevsky Vladislav Date: Mon, 14 Sep 2026 16:05:04 +0200 Subject: [PATCH 3/3] [UIK-5826][d3-chart] add ability to render links within ticks --- semcore/d3-chart/src/Axis.jsx | 137 +++++++----------- .../src/component/Chart/AbstractChart.type.ts | 6 +- semcore/d3-chart/src/style/axis.shadow.css | 86 ++++------- semcore/d3-chart/src/types/Axis.d.ts | 5 +- .../d3-chart/docs/bar-chart.docs.stories.tsx | 5 + .../docs/examples/bar-chart/basic-usage.tsx | 7 - .../docs/examples/bar-chart/links.tsx | 33 +++++ 7 files changed, 123 insertions(+), 156 deletions(-) create mode 100644 stories/components/d3-chart/docs/examples/bar-chart/links.tsx diff --git a/semcore/d3-chart/src/Axis.jsx b/semcore/d3-chart/src/Axis.jsx index e6582bfb04..249313143b 100644 --- a/semcore/d3-chart/src/Axis.jsx +++ b/semcore/d3-chart/src/Axis.jsx @@ -1,10 +1,13 @@ import { Component, sstyled } from '@semcore/core'; -import React, { useState, useEffect } from 'react'; +import React from 'react'; import createElement from './createElement'; import style from './style/axis.shadow.css'; import { scaleOfBandwidth } from './utils'; +const TICK_MARGIN_X = 16; +const TICK_MARGIN_Y = 8; + const CUSTOM_0 = Symbol('custom_0'); const CUSTOM_1 = Symbol('custom_1'); @@ -82,32 +85,40 @@ const MAP_POSITION_AXIS = { }; const MAP_POSITION_TICK = { - top: ([xScale, yScale], value) => { + top: ([xScale, yScale], value, _, { width, height }) => { const yRange = yScale.range(); + const [translateX, translateY] = [-width / 2, -height - TICK_MARGIN_Y]; + return { - x: scaleOfBandwidth(xScale, value), - y: yRange[1], + x: scaleOfBandwidth(xScale, value) + translateX, + y: yRange[1] + translateY, }; }, - bottom: ([xScale, yScale], value) => { + bottom: ([xScale, yScale], value, _, { width }) => { const yRange = yScale.range(); + const [translateX, translateY] = [-width / 2, TICK_MARGIN_Y]; + return { - x: scaleOfBandwidth(xScale, value), - y: yRange[0], + x: scaleOfBandwidth(xScale, value) + translateX, + y: yRange[0] + translateY, }; }, - right: ([xScale, yScale], value) => { + right: ([xScale, yScale], value, _, { height }) => { const xRange = xScale.range(); + const [translateX, translateY] = [TICK_MARGIN_X, -height / 2]; + return { - x: xRange[1], - y: scaleOfBandwidth(yScale, value), + x: xRange[1] + translateX, + y: scaleOfBandwidth(yScale, value) + translateY, }; }, - left: ([xScale, yScale], value) => { + left: ([xScale, yScale], value, _, { width, height }) => { const xRange = xScale.range(); + const [translateX, translateY] = [-width - TICK_MARGIN_X, -height / 2]; + return { - x: xRange[0], - y: scaleOfBandwidth(yScale, value), + x: xRange[0] + translateX, + y: scaleOfBandwidth(yScale, value) + translateY, }; }, [CUSTOM_0]: ([xScale, yScale], value, pos) => { @@ -189,54 +200,6 @@ function renderValue(value, locale = 'en') { 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'; @@ -297,23 +260,25 @@ function Ticks(props) { dataHintsHandler, children, childrenPosition = 'inside', - rootRef, - multiline, locale, + primaryText, + size, } = props; - const [rootRefElement, setRootRefElement] = useState(null); - useEffect(() => { - if (rootRef.current) setRootRefElement(rootRef.current); - }, []); + const isXScale = indexScale === 0; + + const [_, plotHeight] = size; + const currentScale = scale[indexScale]; + const secondaryScale = scale[isXScale ? 1 : 0]; + + const [startPointCurrentScale, endPointCurrentScale] = currentScale.range(); + const [startPointSecondaryScale, endPointSecondaryScale] = secondaryScale.range(); - const tickBandwidth = scale[indexScale]?.bandwidth?.(); - const ticksWithLines = ticks.map((tick) => ({ - tick, - lines: typeof tick === 'string' && multiline && rootRefElement - ? splitTextByWidth(rootRefElement, tick, tickBandwidth) - : [], - })); + const tickStepSize = currentScale?.step?.() ?? Math.abs(startPointCurrentScale - endPointCurrentScale) / ticks.length; + + const [tickWidth, tickHeight] = isXScale + ? [tickStepSize, Math.max(Math.abs(plotHeight - startPointSecondaryScale), endPointSecondaryScale)] + : [startPointSecondaryScale, tickStepSize]; const pos = MAP_POSITION_TICK[position] ?? MAP_POSITION_TICK[MAP_INDEX_SCALE_SYMBOL[indexScale]]; const positionClass = MAP_POSITION_TICK[position] ? position : `custom_${indexScale}`; @@ -330,29 +295,25 @@ function Ticks(props) { } } - return ticksWithLines.map(({ tick: value, lines }, i) => { + return ticks.map((value, i) => { const displayValue = typeof children === 'function' ? undefined : renderValue(value, locale); return sstyled(styles)( - { lines.length > 1 - ? lines.map((line, lineIndex) => ( - {line} - )) - : displayValue} + {displayValue} , ); }); diff --git a/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts b/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts index edcc2fef8e..824060c594 100644 --- a/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts +++ b/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts @@ -127,8 +127,10 @@ export type BaseChartProps = NSFlex.Props & { */ yTicksCount?: number; /** Enables multiline tick labels for X axis, applicable only for band scales */ + /** @deprecated has no effect since v.18 */ multilineXTicks?: boolean; /** Enables multiline tick labels for Y axis, applicable only for band scales */ + /** @deprecated has no effect since v.18 */ multilineYTicks?: boolean; /** * Group key for all array-based charts (for get keys of items for legend except that group key) @@ -137,8 +139,8 @@ export type BaseChartProps = NSFlex.Props & { /** * function for format axis item text */ - axisXValueFormatter?: (value: unknown) => string; - axisYValueFormatter?: (value: unknown) => string; + axisXValueFormatter?: (value: unknown) => React.ReactNode; + axisYValueFormatter?: (value: unknown) => React.ReactNode; /** * Function for format text for tooltip */ diff --git a/semcore/d3-chart/src/style/axis.shadow.css b/semcore/d3-chart/src/style/axis.shadow.css index 4723fe0022..8c88def800 100644 --- a/semcore/d3-chart/src/style/axis.shadow.css +++ b/semcore/d3-chart/src/style/axis.shadow.css @@ -8,15 +8,37 @@ SAxis[hide] { STick { font-size: var(--intergalactic-fs-100, 12px); - fill: var(--intergalactic-chart-grid-text-label, oklch(0.097 0.023 156.5 / 0.468)); font-variant-numeric: tabular-nums; -} -STick[primaryText] { - fill: var(--intergalactic-text-primary, oklch(0.1 0.03 137 / 0.899)); -} + line-height: var(--intergalactic-lh-100, 133%); + color: var(--intergalactic-text-secondary, oklch(0.097 0.023 156.5 / 0.468)); -STick[hide] { - display: none; + &[position='bottom'] { + text-align: center; + } + + &[position='left'] { + text-align: end; + align-content: center; + dominant-baseline: central; + } + + &[position='top'] { + text-align: center; + align-content: end; + } + + &[position='right'] { + align-content: center; + dominant-baseline: central; + } + + &[hide] { + display: none; + } + + &[primaryText] { + color: var(--intergalactic-text-primary, oklch(0.1 0.03 137 / 0.899)); + } } SGrid { @@ -58,56 +80,6 @@ STitle[position='left'][verticalWritingMode] { transform: none; } -STick[position='top'] { - transform: translateY(-12px); - text-anchor: middle; -} - -STick[position='bottom'] { - transform: translateY(12px); - text-anchor: middle; - dominant-baseline: hanging; - - tspan { - dominant-baseline: hanging; - } -} - -STick[position='right'] { - transform: translateX(16px); - 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; diff --git a/semcore/d3-chart/src/types/Axis.d.ts b/semcore/d3-chart/src/types/Axis.d.ts index 5fd52fd2b4..44906e02da 100644 --- a/semcore/d3-chart/src/types/Axis.d.ts +++ b/semcore/d3-chart/src/types/Axis.d.ts @@ -7,7 +7,7 @@ import type { IntergalacticD3Component } from './Plot'; export interface IXAxisProps extends XAxisProps, UnknownProperties {} export type XAxisProps = Context & { /** The position of the axis relative chart - * @default 'button' */ + * @default 'bottom' */ position?: 'top' | 'right' | 'bottom' | 'left' | number; /** Element hide property * @default false */ @@ -38,7 +38,8 @@ export type AxisTicksProps = Context & { hide?: boolean; /** Values for axis ticks */ ticks?: any[]; - /** Enables multiline tick labels, applicable only for band scales */ + /** Enables multiline tick labels, applicable only for band scales */ + /** @deprecated has no effect since v.18 */ multiline?: boolean; /** * Enable `--intergalactic-text-primary` color for ticks diff --git a/stories/components/d3-chart/docs/bar-chart.docs.stories.tsx b/stories/components/d3-chart/docs/bar-chart.docs.stories.tsx index 8ed49225e6..91c142ad5d 100644 --- a/stories/components/d3-chart/docs/bar-chart.docs.stories.tsx +++ b/stories/components/d3-chart/docs/bar-chart.docs.stories.tsx @@ -6,6 +6,7 @@ import BasicUsageExample from './examples/bar-chart/basic-usage'; import DateFormatExample from './examples/bar-chart/date-format'; import GroupedBarsExample from './examples/bar-chart/grouped-bars'; import LegendAndPatternFillExample from './examples/bar-chart/legend-and-pattern-fill'; +import LinksExample from './examples/bar-chart/links'; import NegativeValuesExample from './examples/bar-chart/negative-values'; import TooltipExample from './examples/bar-chart/tooltip'; import TrendLineExample from './examples/bar-chart/trend-line'; @@ -51,3 +52,7 @@ export const TrendLine: StoryObj = { export const LegendAndPatternFill: StoryObj = { render: LegendAndPatternFillExample, }; + +export const Links: StoryObj = { + render: LinksExample, +}; diff --git a/stories/components/d3-chart/docs/examples/bar-chart/basic-usage.tsx b/stories/components/d3-chart/docs/examples/bar-chart/basic-usage.tsx index 55be154189..4071fc8cf8 100644 --- a/stories/components/d3-chart/docs/examples/bar-chart/basic-usage.tsx +++ b/stories/components/d3-chart/docs/examples/bar-chart/basic-usage.tsx @@ -3,12 +3,6 @@ import React from 'react'; import BarMockData from '../../../__mocks__/bar'; -const links = [ - 'https://google.com', - 'https://semrush.com', - 'https://wroclaw.pl', -]; - const Demo = () => { return ( { plotWidth={500} plotHeight={300} aria-label='Bar chart' - getLink={(index) => links[index]} /> ); }; diff --git a/stories/components/d3-chart/docs/examples/bar-chart/links.tsx b/stories/components/d3-chart/docs/examples/bar-chart/links.tsx new file mode 100644 index 0000000000..6507f1c835 --- /dev/null +++ b/stories/components/d3-chart/docs/examples/bar-chart/links.tsx @@ -0,0 +1,33 @@ +import { Chart } from '@semcore/ui/d3-chart'; +import Link from '@semcore/ui/link'; +import React from 'react'; + +import BarMockData from '../../../__mocks__/bar'; + +const links: Record = { + 'Category 1': 'https://google.com', + 'Category 2': 'https://semrush.com', + 'Category 3': 'https://developer.semrush.com/intergalactic/', +}; + +const Demo = () => { + return ( + { + if (typeof value === 'string') { + const href = links[value]; + return href ? {value} : value; + } + }} + /> + ); +}; + +const data = BarMockData.Default; + +export default Demo;