Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
20cb47a
[d3-chart] init reference area
ilyabrower Oct 1, 2026
aef25d7
[d3-chart] update gradients for Reference components
j-mnizhek Oct 19, 2026
0455b2b
[d3-chart] fixed colors in gradients
ilyabrower Oct 20, 2026
2a643ad
[d3-chart] fixed colors in gradients
ilyabrower Oct 20, 2026
d236078
Merge branch 'release/v18' into UIK-6018/reference-area
Valeria-Zimnitskaya Oct 20, 2026
e39b89c
[d3-chart] marked all Reference* components as deprecated
ilyabrower Oct 20, 2026
dbeb95f
[d3-chart] added uniqueIDEnhancement to the ReferenceLine
ilyabrower Oct 20, 2026
7892570
[d3-chart] added resolveColor to gradient
ilyabrower Oct 20, 2026
61e798e
[d3-chart] fixed path in referenceLine
ilyabrower Oct 20, 2026
b0a7cdb
[stories] update reference-line example
j-mnizhek Oct 20, 2026
8242dab
[d3-chart] update tests
Valeria-Zimnitskaya Oct 21, 2026
78b719f
[d3-chart] fixed reference line/area example, fixed type for referenc…
ilyabrower Oct 21, 2026
03b7208
[chore] Merge branch 'UIK-6018/reference-area' of https://github.com/…
Valeria-Zimnitskaya Oct 22, 2026
69646cf
[d3-chart] added aria-hidden to areas in reference
ilyabrower Oct 22, 2026
836603a
[chore] Merge branch 'UIK-6018/reference-area' of https://github.com/…
Valeria-Zimnitskaya Oct 22, 2026
fbe68c3
[chore] update tests
Valeria-Zimnitskaya Oct 22, 2026
f453ce8
Apply suggestion from @Valeria-Zimnitskaya
ilyabrower Oct 22, 2026
2528b81
[d3-chart] fixed deprecate message in Reference* types
ilyabrower Oct 22, 2026
8cbfca7
[chore] Merge branch 'UIK-6018/reference-area' of github.com:semrush/…
ilyabrower Oct 22, 2026
cb3e9b2
[chore] update story
Valeria-Zimnitskaya Oct 22, 2026
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
6 changes: 2 additions & 4 deletions semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,10 @@ test.describe(`${TAG.VISUAL}`, () => {

test.describe('Reference Lines', () => {
test('Verify attributes reference lines ', {
tag: [TAG.PRIORITY_MEDIUM, '@d3-chart'],
tag: [TAG.PRIORITY_LOW, '@d3-chart'],
}, async ({ page }) => {
await loadPage(page, 'stories/components/d3-chart/tests/examples/d3-chart/reference-line-props.tsx', 'en');

const svgs = page.locator('svg');

const referenceLine = locators.referenceLine(page, 0);
await expect(referenceLine).toHaveAttribute('aria-hidden', 'true');
await expect(referenceLine).toHaveAttribute('title', 'Left data');
Expand All @@ -232,7 +230,7 @@ test.describe(`${TAG.VISUAL}`, () => {
await expect(referenceTitle).toHaveAttribute('aria-hidden', 'true');
await expect(referenceTitle).toHaveAttribute('value', 'Category 0');

const background = await svgs.first().locator('rect').nth(1);
const background = locators.plot(page, 0).locator('path[value="Category 3"]');
await expect(background).toHaveAttribute('aria-hidden', 'true');
await expect(background).toHaveAttribute('value', 'Category 3');

Expand Down
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.
507 changes: 507 additions & 0 deletions semcore/d3-chart/__tests__/line-chart-reference.browser-test.tsx

Large diffs are not rendered by default.

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.
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.
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.
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.
138 changes: 110 additions & 28 deletions semcore/d3-chart/src/Reference.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, sstyled } from '@semcore/core';
import uniqueIDEnhancement from '@semcore/core/lib/utils/uniqueID';
import React from 'react';

import { SvgElement } from './component/SvgElement';
import createElement from './createElement';
import style from './style/reference.shadow.css';
import { scaleOfBandwidth } from './utils';
Expand Down Expand Up @@ -132,6 +133,8 @@ class ReferenceLineRoot extends Component {
position: 'left',
};

static enhance = [uniqueIDEnhancement()];

getTitleProps() {
const { position, value } = this.asProps;
return { position, value };
Expand All @@ -144,20 +147,37 @@ class ReferenceLineRoot extends Component {

render() {
const SReferenceLine = this.Element;
const { title, scale, position, value, color, resolveColor, styles } = this.asProps;
const SArea = SvgElement;
const { title, scale, position, value, color, resolveColor, styles, uid, area } = this.asProps;
const positionProps = lineDirection2props[side2direction[position]];
const { notchFirst, notchLast, ...line } = positionProps(scale, value);
const patternId = `${uid}-pattern`;

return sstyled(styles)(
<>
{area && (
<g aria-hidden={true}>
<defs>
<linearGradient id={patternId} x1='0%' y1='0%' x2='0%' y2='100%'>
{getColorByUse('neutral', resolveColor)}
</linearGradient>
</defs>

<SArea
tag='path'
d={getReferenceAreaPath({ x: line.x1 - area / 2, y: line.y2, width: area, height: line.y1 - line.y2 })}
fill={`url(#${patternId})`}
/>
</g>
)}
<SReferenceLine
render='g'
__excludeProps={['data', 'scale', 'format', 'value', 'color']}
stroke={resolveColor(color)}
>
<line {...notchFirst} />
<line {...line} />
<line {...notchLast} />
{!area && (<line {...notchLast} />)}
</SReferenceLine>
{title && <ReferenceLine.Title>{title}</ReferenceLine.Title>}
</>,
Expand Down Expand Up @@ -186,42 +206,104 @@ function Title(props) {
/>,
);
}
function Background(props) {
const { Element: SBackground, styles, scale, position = 'left', value, endValue } = props;
const positionProps = rectDirection2props[side2direction[position]];

return sstyled(styles)(
<SBackground
render='rect'
childrenPosition='inside'
{...positionProps(scale, value, endValue)}
/>,
);
function getColorByUse(use, resolveColor) {
switch (use) {
case 'bad': {
return (
<>
<stop offset='0%' stopColor={resolveColor('red-50')} stopOpacity='0.8' />
<stop offset='12.68%' stopColor={resolveColor('red-50')} stopOpacity='0.5' />
<stop offset='87.98%' stopColor={resolveColor('red-50')} stopOpacity='0.0' />
</>
);
}
case 'good': {
return (
<>
<stop offset='0%' stopColor={resolveColor('green-50')} stopOpacity='0.8' />
<stop offset='12.68%' stopColor={resolveColor('green-50')} stopOpacity='0.4' />
<stop offset='87.98%' stopColor={resolveColor('green-50')} stopOpacity='0.0' />
</>
);
}
case 'insight': {
return (
<>
<stop offset='0%' stopColor={resolveColor('violet-50')} stopOpacity='0.9' />
<stop offset='12.68%' stopColor={resolveColor('violet-50')} stopOpacity='0.5' />
<stop offset='87.98%' stopColor={resolveColor('violet-50')} stopOpacity='0.0' />
</>
);
}
default: {
return (
<>
<stop offset='0%' stopColor={resolveColor('gray-50')} stopOpacity='0.9' />
<stop offset='41.83%' stopColor={resolveColor('gray-50')} stopOpacity='0.5' />
<stop offset='87.98%' stopColor={resolveColor('gray-50')} stopOpacity='0.0' />
</>
);
}
}
}

function getReferenceAreaPath({ x, y, width, height }) {
const r = 12;

const path = `M ${x},${y + r} Q ${x},${y} ${x + r},${y} H ${x + width - r} Q ${x + width},${y} ${x + width},${y + r} V ${y + height} H ${x} Z`;

return path;
}
Background.style = style;

const diagonalGap = 8;
const gap = Math.sqrt(diagonalGap ** 2 + diagonalGap ** 2);
const gap12 = gap * (1 / 2);
const gap32 = gap * (3 / 2);
const path = `M-${gap},-${gap12} L${gap},${gap32} M-${gap12},-${gap} L${gap32},${gap}`;
function Stripes(props) {
const { Element: SStripes, styles, scale, position = 'left', value, endValue, uid } = props;
const SStripesPatternPath = 'path';
const positionProps = rectDirection2props[side2direction[position]];
const { Element: SStripes, styles, scale, position = 'left', value, endValue, uid, use = 'neutral', title, subTitle, resolveColor } = props;
const positionProps = rectDirection2props[side2direction[position]](scale, value, endValue);
const patternId = `${uid}-pattern`;
const STitle = SvgElement;
const SSubTitle = SvgElement;

const textX = positionProps.x + 6;
const textY = positionProps.y + 6;

return sstyled(styles)(
<g>
<defs>
<linearGradient id={patternId} x1='0%' y1='0%' x2='0%' y2='100%'>
{getColorByUse(use, resolveColor)}
</linearGradient>
</defs>

<SStripes
render='rect'
render='path'
d={getReferenceAreaPath(positionProps)}
childrenPosition='inside'
fill={`url(#${patternId})`}
{...positionProps(scale, value, endValue)}
/>
<pattern id={patternId} patternUnits='userSpaceOnUse' width={gap} height={gap}>
<SStripesPatternPath d={path} />
</pattern>

{title && (
<STitle
tag='text'
childrenPosition='inside'
position='top'
x={textX}
y={textY}
>
{title}
</STitle>
)}

{subTitle && (
<SSubTitle
tag='text'
childrenPosition='inside'
position='top'
x={textX}
y={textY + 26}
>
{subTitle}
</SSubTitle>
)}
</g>,
);
}
Expand All @@ -230,9 +312,9 @@ Stripes.enhance = [uniqueIDEnhancement()];

export const ReferenceLine = createElement(ReferenceLineRoot, {
Title,
Background,
Background: Stripes,
Stripes,
});

export const ReferenceBackground = createElement(Background);
export const ReferenceBackground = createElement(Stripes);
export const ReferenceStripes = createElement(Stripes);
5 changes: 5 additions & 0 deletions semcore/d3-chart/src/component/Chart/AbstractChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,10 @@ export abstract class AbstractChart<
);
}

protected renderReference(): React.ReactNode {
return null;
}

public render() {
const SChart = Root;
const { styles, data, patterns, a11yAltTextConfig, duration, eventEmitter, showTooltip, locale } =
Expand All @@ -684,6 +688,7 @@ export abstract class AbstractChart<
{...extractedAriaProps}
>
{this.renderAxis()}
{this.renderReference()}
{!showTooltip ? null : this.renderTooltip()}
{this.renderChart()}
</Plot>
Expand Down
32 changes: 31 additions & 1 deletion semcore/d3-chart/src/component/Chart/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { ObjectData } from './AbstractChart.type';
import { HIGHLIGHT_DOT } from './AbstractChart.type';
import type { LineChartData, LineChartDefaultProps, LineChartProps, LineChartType } from './LineChart.type';
// @ts-ignore
import { Line, minMax, HoverLine } from '../..';
import { Line, minMax, HoverLine, ReferenceLine, ReferenceStripes } from '../..';
import type { ChartState } from './AbstractChart';
import { AbstractChart } from './AbstractChart';
import { localizedMessages } from '../../translations/__intergalactic-dynamic-locales';
Expand Down Expand Up @@ -116,6 +116,36 @@ class LineChartComponent extends AbstractChart<
);
}

protected renderReference() {
const { referenceLine, referenceArea } = this.asProps;

if (!referenceLine && !referenceArea) {
return null;
}

return (
<>
{referenceLine && (
Array.isArray(referenceLine)
? referenceLine.map((refLineItem) => {
return (<ReferenceLine key={refLineItem.value.toString()} value={refLineItem.value} area={refLineItem.area} />);
})
: (<ReferenceLine value={referenceLine.value} area={referenceLine.area} />)
)}

{referenceArea && (
<ReferenceStripes
value={referenceArea.value[0]}
endValue={referenceArea.value[1]}
use={referenceArea.use}
title={referenceArea.title}
subTitle={referenceArea.subTitle}
/>
)}
</>
);
}

protected getLegendAriaLabel(): string {
return this.asProps.getI18nText('legendForChart', { chartType: 'Line' });
}
Expand Down
39 changes: 39 additions & 0 deletions semcore/d3-chart/src/component/Chart/LineChart.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,37 @@ type AreaItem = {

export type LineChartData = Array<Record<string, string | number | typeof interpolateValue | Date>>;

type ReferenceLine = {
/**
* Simple reference line.
*/
value: string | number | Date;

/**
* Size of area near line
*/
area?: number;
};

type ReferenceArea = {
/**
* Reference line with area.
*/
value: [string | number | Date, string | number | Date];
/**
* @default neutral.
*/
use?: 'neutral' | 'insight' | 'good' | 'bad';
/**
* Title of the area.
*/
title?: string;
/**
* Custom subtitle with data.
*/
subTitle?: string;
};

export type LineChartProps = BaseChartProps<LineChartData> & {
/** Field name that groups the data points */
groupKey: string;
Expand All @@ -32,6 +63,14 @@ export type LineChartProps = BaseChartProps<LineChartData> & {
areaCurve?: CurveFactory;
/** Callback triggered when a user clicks on a line */
onClickLine?: (index: number, event: React.SyntheticEvent) => void;
/**
* Settings for render reference line.
*/
referenceLine?: ReferenceLine | ReferenceLine[];
/**
* Settings for render reference area.
*/
referenceArea?: ReferenceArea;
};

export type LineChartDefaultProps = {
Expand Down
17 changes: 7 additions & 10 deletions semcore/d3-chart/src/style/reference.shadow.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ SReferenceLine {
STitle {
font-size: var(--intergalactic-fs-100, 12px);
fill: var(--intergalactic-chart-grid-text-label, oklch(from oklch(0.182 0.045 244.8 / 0.593) l c h / 0.4));
transform-origin: var(--transform-origin);
}

STitle[position='top'] {
text-anchor: middle;
dominant-baseline: hanging;
}

STitle[position='bottom'] {
Expand All @@ -30,14 +29,12 @@ STitle[position='left'] {
dominant-baseline: middle;
}

SBackground {
fill: var(--intergalactic-chart-grid-period-bg, oklch(0.96 0.005 232.5));
}
SStripes {
fill: var(--fill);
SSubTitle {
font-size: var(--intergalactic-fs-100, 12px);
fill: var(--intergalactic-chart-grid-text-label-accent, oklch(0.102 0.021 230.9 / 0.9));
font-weight: bold;
}

SStripesPatternPath {
stroke: var(--intergalactic-chart-grid-period-pattern, oklch(0.95 0.006 232.7));
stroke-width: 1px;
SBackground {
fill: var(--intergalactic-chart-grid-period-bg, oklch(0.96 0.005 232.5));
}
Loading
Loading