From 1c1577afe0be88ee44de21ca8c252fb20316b19d Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Wed, 21 Oct 2026 12:33:07 +0200 Subject: [PATCH 01/21] [d3-chart] added example for other value in donut chart --- .../components/d3-chart/__mocks__/donut.ts | 6 +++ .../docs/donut-chart.docs.stories.tsx | 2 +- .../docs/examples/donut-chart/donut.tsx | 42 +++++-------------- .../donut-chart/donut-chart-code.md | 6 +-- 4 files changed, 20 insertions(+), 36 deletions(-) diff --git a/stories/components/d3-chart/__mocks__/donut.ts b/stories/components/d3-chart/__mocks__/donut.ts index baf6b99ffe..e44fc9bf2b 100644 --- a/stories/components/d3-chart/__mocks__/donut.ts +++ b/stories/components/d3-chart/__mocks__/donut.ts @@ -9,4 +9,10 @@ export default { b: 0, c: 0, }, + WithOther: { + a: 5, + b: 8, + c: 4, + other: 2, + }, }; diff --git a/stories/components/d3-chart/docs/donut-chart.docs.stories.tsx b/stories/components/d3-chart/docs/donut-chart.docs.stories.tsx index 63b861e10c..7a8ba6f158 100644 --- a/stories/components/d3-chart/docs/donut-chart.docs.stories.tsx +++ b/stories/components/d3-chart/docs/donut-chart.docs.stories.tsx @@ -13,7 +13,7 @@ const meta: Meta = { export default meta; -export const Donut: StoryObj = { +export const DonutWithOther: StoryObj = { render: DonutExample, }; diff --git a/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx b/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx index 5c93a795c4..e8423b6abe 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx @@ -1,42 +1,22 @@ -import { Flex } from '@semcore/ui/base-components'; -import { Donut, Plot } from '@semcore/ui/d3-chart'; -import { Text } from '@semcore/ui/typography'; +import { Chart } from '@semcore/ui/d3-chart'; import React from 'react'; import DonutMockData from '../../../__mocks__/donut'; const Demo = () => { return ( - - - - - - - - Example - - - - - {({ dataKey, name }) => { - return { - children: ( - <> - {name} - - {/* @ts-ignore */} - {data[dataKey]} - - - ), - }; - }} - - +
+ +
); }; -const data = DonutMockData.Default; +const data = DonutMockData.WithOther; export default Demo; diff --git a/website/docs/data-display/donut-chart/donut-chart-code.md b/website/docs/data-display/donut-chart/donut-chart-code.md index 8ab4406565..94d7fbb21a 100644 --- a/website/docs/data-display/donut-chart/donut-chart-code.md +++ b/website/docs/data-display/donut-chart/donut-chart-code.md @@ -18,11 +18,9 @@ For core principles, concept description, API and changelog, refer to the [D3 ch ::: -## Advanced usage +## Donut with "other value" -- You can draw donut and pie charts with the `Donut` component. -- `Pie` is a separate sector. -- `Label` is a text label inside the chart. +- You should add custom color (use `chart-palette-order-other-data` token) by key in your data for "other value". ::: sandbox From 190c28bcae507697445fe80e0e3af4befc88ed45 Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Wed, 21 Oct 2026 20:29:11 +0200 Subject: [PATCH 02/21] [d3-chart] added new Score Donut chart --- playground/entries/Chart/ScoreDonut.tsx | 85 +++++++ playground/registry.ts | 2 + semcore/d3-chart/src/Pattern.tsx | 25 +- .../src/component/Chart/ScoreDonutChart.tsx | 216 ++++++++++++++++++ .../component/Chart/ScoreDonutChart.type.ts | 45 ++++ semcore/d3-chart/src/component/Chart/index.ts | 2 + semcore/d3-chart/src/types/index.d.ts | 1 + .../score-donut-chart/basic-usage.tsx | 12 + .../examples/score-donut-chart/semi-donut.tsx | 12 + .../docs/score-donut-chart.docs.stories.tsx | 18 ++ website/docs/.vitepress/sidebarConfig.ts | 5 + .../donut-chart/donut-chart-code.md | 2 +- .../score-donut-chart-a11y.md | 5 + .../score-donut-chart-api.md | 18 ++ .../score-donut-chart-changelog.md | 8 + .../score-donut-chart-code.md | 28 +++ .../score-donut-chart/score-donut-chart.md | 15 ++ 17 files changed, 482 insertions(+), 17 deletions(-) create mode 100644 playground/entries/Chart/ScoreDonut.tsx create mode 100644 semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx create mode 100644 semcore/d3-chart/src/component/Chart/ScoreDonutChart.type.ts create mode 100644 stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx create mode 100644 stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx create mode 100644 stories/components/d3-chart/docs/score-donut-chart.docs.stories.tsx create mode 100644 website/docs/data-display/score-donut-chart/score-donut-chart-a11y.md create mode 100644 website/docs/data-display/score-donut-chart/score-donut-chart-api.md create mode 100644 website/docs/data-display/score-donut-chart/score-donut-chart-changelog.md create mode 100644 website/docs/data-display/score-donut-chart/score-donut-chart-code.md create mode 100644 website/docs/data-display/score-donut-chart/score-donut-chart.md diff --git a/playground/entries/Chart/ScoreDonut.tsx b/playground/entries/Chart/ScoreDonut.tsx new file mode 100644 index 0000000000..8340114d56 --- /dev/null +++ b/playground/entries/Chart/ScoreDonut.tsx @@ -0,0 +1,85 @@ +import { Chart } from '@semcore/ui/d3-chart'; +import { Text } from '@semcore/ui/typography'; +import React from 'react'; + +import { getDefaultChartControls } from './common/controls'; +import type { CommonChartProps, LegendChartProps } from './common/controls'; +import type { JSXProps } from '../../types/JSXProps'; +import type { PlaygroundEntry } from '../../types/Playground'; +import createGithubLink from '../../utils/createGHLink'; + +type DonutChartProps = { + commonChartProps: Omit; + legendProps: Omit; + donutProps: { + halfsize: boolean; + innerLabel: string; + }; +}; +export type DonutChartJSXProps = JSXProps; + +const data = { + value: 33, +}; + +function getJSX(props: DonutChartJSXProps) { + const legendProps = { + ...props.legendProps, + }; + return ( + + {props.donutProps.innerLabel} + + ) + : props.donutProps.innerLabel + )} + {...props.commonChartProps} + {...(props.legendProps && { + legendProps, + showLegend: props.commonChartProps.showLegend as true, + })} + {...(props.legendProps?.patterns && { patterns: props.legendProps.patterns })} + /> + ); +} + +const entry: PlaygroundEntry = { + JSX: (props) => getJSX(props), + controls: { + donutProps: { + type: 'group', + groupName: 'Donut props', + controls: { + halfsize: { + type: 'boolean', + value: false, + displayName: 'Half size', + }, + innerLabel: { + type: 'text', + value: 'Example', + displayName: 'Inner Label', + }, + }, + }, + ...getDefaultChartControls({ + skip: { + commonChartProps: ['showTotalInTooltip'], + legendProps: ['direction'], + }, + }), + }, + link: createGithubLink('d3-chart'), + filterProps: ['data'], +}; + +export default entry; diff --git a/playground/registry.ts b/playground/registry.ts index 502edfcc70..e52784d6b0 100644 --- a/playground/registry.ts +++ b/playground/registry.ts @@ -13,6 +13,7 @@ import ChartLine from './entries/Chart/Line'; import MiniChart from './entries/Chart/MiniChart'; import ChartRadar from './entries/Chart/Radar'; import ChartScatterPlot from './entries/Chart/ScatterPlot'; +import ChartScoreDonut from './entries/Chart/ScoreDonut'; import ChartStackedArea from './entries/Chart/StackedArea'; import ChartStackedBar from './entries/Chart/StackedBar'; import ChartStackedHorizontalBar from './entries/Chart/StackedHorizontalBar'; @@ -89,6 +90,7 @@ const registry = { 'Chart.Cigarette': ChartCigarette, 'ChartLegend': ChartLegend, 'Chart.Donut': ChartDonut, + 'Chart.ScoreDonut': ChartScoreDonut, 'Chart.Histogram': ChartHistogram, 'Chart.Line': ChartLine, 'MiniChart': MiniChart, diff --git a/semcore/d3-chart/src/Pattern.tsx b/semcore/d3-chart/src/Pattern.tsx index 4f12eddcd6..e073fc44d4 100644 --- a/semcore/d3-chart/src/Pattern.tsx +++ b/semcore/d3-chart/src/Pattern.tsx @@ -632,26 +632,19 @@ export const DefaultStrokePattern = (props: { id: string }) => { return ( - - + + + + + + ); }; diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx new file mode 100644 index 0000000000..914d2fa401 --- /dev/null +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -0,0 +1,216 @@ +import { Flex } from '@semcore/base-components'; +import { createComponent, Root, sstyled } from '@semcore/core'; +import { extractAriaProps } from '@semcore/core/lib/utils/ariaProps'; +import i18nEnhance from '@semcore/core/lib/utils/enhances/i18nEnhance'; +import uniqueIDEnhancement from '@semcore/core/lib/utils/uniqueID'; +import { Text } from '@semcore/typography'; +import { scaleLinear } from 'd3-scale'; +import React from 'react'; + +// @ts-ignore +import { Donut, Plot } from '../..'; +import type { ChartState } from './AbstractChart'; +import { NOT_A_VALUE, AbstractChart } from './AbstractChart'; +import type { ScoreDonutChartData, ScoreDonutChartDefaultProps, ScoreDonutChartProps, ScoreDonutChartType } from './ScoreDonutChart.type'; +import { DefaultStrokePattern } from '../../Pattern'; +import { localizedMessages } from '../../translations/__intergalactic-dynamic-locales'; +import { interpolateValue } from '../../utils'; +import type { LegendItem } from '../ChartLegend/LegendItem/LegendItem.type'; + +class ScoreDonutChartComponent extends AbstractChart< + ScoreDonutChartData, + ScoreDonutChartProps, + typeof ScoreDonutChartComponent.enhance, + {}, + ChartState, + ScoreDonutChartDefaultProps +> { + static displayName = 'Chart.ScoreDonut'; + static defaultProps = { + direction: 'row-reverse', + alignItems: 'flex-start', + marginX: 0, + marginY: 0, + locale: 'en', + } as const; + + static enhance = [i18nEnhance(localizedMessages), uniqueIDEnhancement()] as const; + + protected override getDefaultDataDefinitions(): Array { + const { data, legendProps } = this.props; + + const legendData = legendProps?.legendMap?.value; + + // @ts-expect-error will fill columns or rows later + const dataDefinition: LegendItem & ({ columns: Array } | { rows: Array }) = { + id: 'value', + label: legendData?.label ?? 'value', + icon: legendData?.icon ?? undefined, + checked: legendData?.defaultChecked ?? true, + color: this.resolveColor('value', 0), + }; + + if (legendData?.additionalInfo || legendData?.count) { + dataDefinition.additionalInfo = legendData.additionalInfo + ? { label: legendData.additionalInfo } + : legendData.count + ? { count: legendData.count } + : undefined; + } + + if (legendData && 'rows' in legendData && !('columns' in dataDefinition)) { + dataDefinition.rows = legendData.rows || []; + } else if (legendData && 'columns' in legendData && !('rows' in dataDefinition)) { + dataDefinition.columns = legendData.columns || []; + } else if (!('rows' in dataDefinition)) { + const value = data.value; + const percent = value.toFixed(2); + + dataDefinition.columns = [ + + {percent !== undefined ? `${percent}%` : ''} + , + + {value} + , + ]; + } + + return [dataDefinition]; + } + + get xScale() { + const { xScale } = this.asProps; + + if (xScale) { + return xScale; + } + + return scaleLinear(); + } + + get yScale() { + const { yScale } = this.asProps; + + if (yScale) { + return yScale; + } + + return scaleLinear(); + } + + defaultLegendProps() { + return { + legendType: 'Table' as const, + }; + } + + renderChart() { + const { innerRadius, halfsize, innerLabel, onClickPie, data, uid } = this.asProps; + const { dataDefinitions, highlightedItem } = this.state; + + const checkedLegendItems = dataDefinitions.filter((item) => item.checked); + + return ( + + {checkedLegendItems.length === 0 && } + {checkedLegendItems.map((item, index) => { + return ( + + ); + })} + + + {innerLabel && {innerLabel}} + + ); + } + + renderTooltip() { + const { data } = this.asProps; + const { dataDefinitions } = this.state; + + return ( + + {({ dataKey }: any) => { + const title = dataDefinitions.find((item) => item.id === dataKey); + + return { + children: ( + <> + {this.tooltipTitleFormatter(title?.label || dataKey)} + + {this.tooltipValueFormatter(data.value)} + + + ), + }; + }} + + ); + } + + public override render() { + const SChart = Root; + const { styles, data, patterns, a11yAltTextConfig, duration, eventEmitter, showTooltip, locale } = + this.asProps; + const { plotWidth, plotHeight } = this; + + const { extractedAriaProps } = extractAriaProps(this.asProps); + + return sstyled(styles)( + + {this.renderLegend()} + + {this.renderAxis()} + {!showTooltip ? null : this.renderTooltip()} + {this.renderChart()} + + , + ); + } + + private get data() { + return { + value: this.asProps.data.value, + other: 100 - this.asProps.data.value, + }; + } + + protected getLegendAriaLabel(): string { + return this.asProps.getI18nText('legendForChart', { chartType: 'ScoreDonut' }); + } +} + +/** + * ScoreDonutChart + * + * {@link https://developer.semrush.com/intergalactic/data-display/score-donut-chart/score-donut-chart-api/|API} | {@link https://developer.semrush.com/intergalactic/data-display/score-donut-chart/score-donut-chart-code/|Examples} + */ +export const ScoreDonutChart = createComponent< + ScoreDonutChartType, + typeof ScoreDonutChartComponent +>(ScoreDonutChartComponent); diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.type.ts b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.type.ts new file mode 100644 index 0000000000..22abf9da8e --- /dev/null +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.type.ts @@ -0,0 +1,45 @@ +import type { Flex } from '@semcore/base-components'; +import type { Intergalactic } from '@semcore/core'; +import type { ScaleLinear } from 'd3-scale'; +import type React from 'react'; + +import type { BaseChartProps } from './AbstractChart.type'; + +export type DataKey = string; + +export type ScoreDonutChartData = { + /** + * Number is a percent value from 0 to 100. + */ + value: number; +}; + +export type ScoreDonutChartProps = Intergalactic.InternalTypings.EfficientOmit< + BaseChartProps, + 'showTotalInTooltip' | 'showDeltaPercentInTooltip' | 'getPercentDelta' | 'deltaPercentGrowthColor' +> & { + /** Internal */ + groupKey?: never; + /** Custom x-axis scale */ + xScale?: ScaleLinear; + /** Custom y-axis scale */ + yScale?: ScaleLinear; + /** Controls the inner radius of the donut */ + innerRadius?: number; + /** Creates a semi-donut chart when enabled */ + halfsize?: boolean; + /** Content displayed in the center of the donut */ + innerLabel?: React.ReactNode; + /** Callback triggered when a user clicks on a pie */ + onClickPie?: (key: DataKey, e: React.SyntheticEvent) => void; +}; + +export type ScoreDonutChartDefaultProps = { + direction: 'row-reverse'; + alignItems: 'flex-start'; + marginX: 0; + marginY: 0; + locale: 'en'; +}; + +export type ScoreDonutChartType = Intergalactic.Component; diff --git a/semcore/d3-chart/src/component/Chart/index.ts b/semcore/d3-chart/src/component/Chart/index.ts index 0afd4d430c..701e21ba97 100644 --- a/semcore/d3-chart/src/component/Chart/index.ts +++ b/semcore/d3-chart/src/component/Chart/index.ts @@ -9,6 +9,7 @@ import { HistogramChart } from './HistogramChart'; import { LineChart } from './LineChart'; import { RadarChart } from './RadarChart'; import { ScatterPlotChart } from './ScatterPlotChart'; +import { ScoreDonutChart } from './ScoreDonutChart'; import { VennChart } from './VennChart'; export default { @@ -19,6 +20,7 @@ export default { Area: AreaChart, Bubble: BubbleChart, Donut: DonutChart, + ScoreDonut: ScoreDonutChart, Venn: VennChart, Radar: RadarChart, Cigarette: CigaretteChart, diff --git a/semcore/d3-chart/src/types/index.d.ts b/semcore/d3-chart/src/types/index.d.ts index c92b2c003b..ed60147b45 100644 --- a/semcore/d3-chart/src/types/index.d.ts +++ b/semcore/d3-chart/src/types/index.d.ts @@ -103,6 +103,7 @@ export * from './component/Chart/ScatterPlotChart.type'; export * from './component/Chart/AreaChart.type'; export * from './component/Chart/BubbleChart.type'; export * from './component/Chart/DonutChart.type'; +export * from './component/Chart/ScoreDonutChart.type'; export * from './component/Chart/VennChart.type'; export * from './component/Chart/RadarChart.type'; export * from './component/Chart/ScatterPlotChart.type'; diff --git a/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx b/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx new file mode 100644 index 0000000000..dd792ca69b --- /dev/null +++ b/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx @@ -0,0 +1,12 @@ +import { Chart } from '@semcore/ui/d3-chart'; +import React from 'react'; + +const Demo = () => { + return ( +
+ +
+ ); +}; + +export default Demo; diff --git a/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx b/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx new file mode 100644 index 0000000000..ebde6dc36a --- /dev/null +++ b/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx @@ -0,0 +1,12 @@ +import { Chart } from '@semcore/ui/d3-chart'; +import React from 'react'; + +const Demo = () => { + return ( +
+ +
+ ); +}; + +export default Demo; diff --git a/stories/components/d3-chart/docs/score-donut-chart.docs.stories.tsx b/stories/components/d3-chart/docs/score-donut-chart.docs.stories.tsx new file mode 100644 index 0000000000..31c91f1af2 --- /dev/null +++ b/stories/components/d3-chart/docs/score-donut-chart.docs.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import BasicUsageExample from './examples/score-donut-chart/basic-usage'; +import HalfSizeExample from './examples/score-donut-chart/semi-donut'; + +const meta: Meta = { + title: 'Components/d3Charts/Documentation/Score-Donut-Chart', +}; + +export default meta; + +export const BasicUsage: StoryObj = { + render: BasicUsageExample, +}; + +export const SemiDonut: StoryObj = { + render: HalfSizeExample, +}; diff --git a/website/docs/.vitepress/sidebarConfig.ts b/website/docs/.vitepress/sidebarConfig.ts index 758ac526c2..016eba17d9 100644 --- a/website/docs/.vitepress/sidebarConfig.ts +++ b/website/docs/.vitepress/sidebarConfig.ts @@ -609,6 +609,11 @@ export const sideBarConfig: SidebarConfig = [ activeMatch: '/data-display/donut-chart/', text: 'Donut chart', }, + { + link: '/data-display/score-donut-chart/score-donut-chart', + activeMatch: '/data-display/score-donut-chart/', + text: 'Score donut chart', + }, { link: '/data-display/histogram-chart/histogram-chart', activeMatch: '/data-display/histogram-chart/', diff --git a/website/docs/data-display/donut-chart/donut-chart-code.md b/website/docs/data-display/donut-chart/donut-chart-code.md index 94d7fbb21a..1f6a2866de 100644 --- a/website/docs/data-display/donut-chart/donut-chart-code.md +++ b/website/docs/data-display/donut-chart/donut-chart-code.md @@ -20,7 +20,7 @@ For core principles, concept description, API and changelog, refer to the [D3 ch ## Donut with "other value" -- You should add custom color (use `chart-palette-order-other-data` token) by key in your data for "other value". +- You should add custom color (use `chart-palette-order-other-data` token) by key in your data for "other value". ::: sandbox diff --git a/website/docs/data-display/score-donut-chart/score-donut-chart-a11y.md b/website/docs/data-display/score-donut-chart/score-donut-chart-a11y.md new file mode 100644 index 0000000000..57403835b3 --- /dev/null +++ b/website/docs/data-display/score-donut-chart/score-donut-chart-a11y.md @@ -0,0 +1,5 @@ +--- +title: Score donut chart +tabs: Design('score-donut-chart'), A11y('score-donut-chart-a11y'), API('score-donut-chart-api'), Examples('score-donut-chart-code'), Changelog('score-donut-chart-changelog') +--- +Score Donut chart is accessible for screen readers. Refer to [Accessibility section in D3 chart documentation](/data-display/d3-chart/d3-chart-a11y) for more information. diff --git a/website/docs/data-display/score-donut-chart/score-donut-chart-api.md b/website/docs/data-display/score-donut-chart/score-donut-chart-api.md new file mode 100644 index 0000000000..8e82c00c1d --- /dev/null +++ b/website/docs/data-display/score-donut-chart/score-donut-chart-api.md @@ -0,0 +1,18 @@ +--- +title: Score donut chart +tabs: Design('score-donut-chart'), A11y('score-donut-chart-a11y'), API('score-donut-chart-api'), Examples('score-donut-chart-code'), Changelog('score-donut-chart-changelog') +--- + +## D3 chart + +For all common D3 chart properties, refer to [D3 chart API](/data-display/d3-chart/d3-chart-api). + +## Chart.ScoreDonut + +```js +import { Chart } from '@semcore/ui/d3-chart'; +``` + + + + diff --git a/website/docs/data-display/score-donut-chart/score-donut-chart-changelog.md b/website/docs/data-display/score-donut-chart/score-donut-chart-changelog.md new file mode 100644 index 0000000000..4eb699e21d --- /dev/null +++ b/website/docs/data-display/score-donut-chart/score-donut-chart-changelog.md @@ -0,0 +1,8 @@ +--- +title: Score donut chart +tabs: Design('score-donut-chart'), A11y('score-donut-chart-a11y'), API('score-donut-chart-api'), Examples('score-donut-chart-code'), Changelog('score-donut-chart-changelog') +--- + +Common changelog of `d3-chart` package. + +::: changelog d3-chart ::: diff --git a/website/docs/data-display/score-donut-chart/score-donut-chart-code.md b/website/docs/data-display/score-donut-chart/score-donut-chart-code.md new file mode 100644 index 0000000000..2fe21f9abc --- /dev/null +++ b/website/docs/data-display/score-donut-chart/score-donut-chart-code.md @@ -0,0 +1,28 @@ +--- +title: Score donut chart +tabs: Design('score-donut-chart'), A11y('score-donut-chart-a11y'), API('score-donut-chart-api'), Examples('score-donut-chart-code'), Changelog('score-donut-chart-changelog') +--- + +::: tip +For core principles, concept description, API and changelog, refer to the [D3 chart](/data-display/d3-chart/d3-chart). +::: + +## Basic usage + +::: sandbox + + + +::: + +## Half-size score donut + +::: sandbox + + + +::: diff --git a/website/docs/data-display/score-donut-chart/score-donut-chart.md b/website/docs/data-display/score-donut-chart/score-donut-chart.md new file mode 100644 index 0000000000..90335bcf01 --- /dev/null +++ b/website/docs/data-display/score-donut-chart/score-donut-chart.md @@ -0,0 +1,15 @@ +--- +title: Score donut chart +fileSource: d3-chart +tabs: Design('score-donut-chart'), A11y('score-donut-chart-a11y'), API('score-donut-chart-api'), Examples('score-donut-chart-code'), Changelog('score-donut-chart-changelog') +--- + + + +::: info +Basic data visualization rules are described in the [D3 chart](/data-display/d3-chart/d3-chart). +::: + +## Description + +**Score donut chart** shows score value in a donut or semi-donut view. From 17f76fe5c5e444fae3337b288d43c39905d287fa Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Mon, 26 Oct 2026 11:13:47 +0100 Subject: [PATCH 03/21] [d3-chart] fixed hover state --- semcore/d3-chart/src/Donut.jsx | 140 ++---------------- .../src/component/Chart/DonutChart.tsx | 8 +- semcore/d3-chart/src/style/donut.shadow.css | 1 + 3 files changed, 23 insertions(+), 126 deletions(-) diff --git a/semcore/d3-chart/src/Donut.jsx b/semcore/d3-chart/src/Donut.jsx index 90b49bf092..a889a3b217 100644 --- a/semcore/d3-chart/src/Donut.jsx +++ b/semcore/d3-chart/src/Donut.jsx @@ -1,6 +1,5 @@ import { Component, Root, sstyled } from '@semcore/core'; import { callAllEventHandlers } from '@semcore/core/lib/utils/assignProps'; -import canUseDOM from '@semcore/core/lib/utils/canUseDOM'; import getOriginChildren from '@semcore/core/lib/utils/getOriginChildren'; import uniqueIDEnhancement from '@semcore/core/lib/utils/uniqueID'; import { interpolate } from 'd3-interpolate'; @@ -21,9 +20,7 @@ function transitionAnglePie({ duration, halfsize, d3Arc, - d3ArcOut, arcs, - activeIndexPie, }) { return transition() .selection() @@ -48,37 +45,7 @@ function transitionAnglePie({ d.startAngle = iStartAngle(t); d.endAngle = iEndAngle(t); self.arc = d; - return ind === activeIndexPie ? d3ArcOut(self.arc) : d3Arc(self.arc); - }; - }); -} - -function transitionRadiusPie({ - data, - selector, - element, - duration, - innerRadius, - outerRadiusStartEnd, - paddingAngle, -}) { - return transition() - .selection() - .select(selector) - .interrupt() - .transition() - .duration(duration) - .attrTween('d', function () { - const [start, end] = outerRadiusStartEnd; - const iOuterRadius = interpolate(start, end); - return function (t) { - const outerRadiusPX = iOuterRadius(t); - element.dataset['currentRadius'] = outerRadiusPX; - const d3ArcOut = arc() - .innerRadius(innerRadius) - .outerRadius(outerRadiusPX) - .padAngle(paddingAngle); - return d3ArcOut(data); + return d3Arc(self.arc); }; }); } @@ -109,11 +76,6 @@ class DonutRoot extends Component { const d3Arc = arc().outerRadius(radius).innerRadius(innerRadius).padAngle(paddingAngle); - const d3ArcOut = arc() - .outerRadius(radius + increaseFactor) - .innerRadius(innerRadius) - .padAngle(paddingAngle); - let d3Pie = pie() .sort(null) .value(([, value]) => value); @@ -125,26 +87,23 @@ class DonutRoot extends Component { return { d3Pie, d3Arc, - d3ArcOut, duration: 500, paddingAngle, innerRadius, }; }; + state = { + active: null, + }; + get id() { const { uid, id } = this.asProps; return id || uid; } - virtualElement = canUseDOM() ? document.createElement('div') : {}; - activeIndexPie = undefined; canAnimatedHover = false; - generateGetBoundingClientRect(x = 0, y = 0) { - return () => ({ width: 0, height: 0, top: y, right: x, bottom: y, left: x }); - } - getArcs() { const { Children, data, d3Pie } = this.asProps; let pieData = Object.entries(data); @@ -180,26 +139,6 @@ class DonutRoot extends Component { eventEmitter.emit(`setTooltipVisible_${plotId}`, visible); }; - animationActivePie = ({ data, active, selector, element }) => { - const { duration, d3Arc, paddingAngle } = this.asProps; - const outerRadius = d3Arc.outerRadius()(); - const innerRadius = d3Arc.innerRadius()(); - const outerRadiusStartEnd = active - ? [+element.dataset['currentRadius'] || outerRadius, outerRadius + increaseFactor] - : [+element.dataset['currentRadius'] || outerRadius, outerRadius]; - if (this.canAnimatedHover && duration > 0) { - transitionRadiusPie({ - data, - selector: `#${this.id} ${selector}`, - element, - duration: duration === 0 ? 0 : 300, - innerRadius, - outerRadiusStartEnd, - paddingAngle, - }); - } - }; - animationTimeout = null; animationUpdatePie = () => { clearTimeout(this.animationTimeout); @@ -213,8 +152,6 @@ class DonutRoot extends Component { arcs: this.arcs, halfsize, d3Arc, - d3ArcOut, - activeIndexPie: this.activeIndexPie, }).on('end', () => { this.canAnimatedHover = true; }); @@ -237,56 +174,40 @@ class DonutRoot extends Component { } getPieProps(props, index) { - const { d3Arc, d3ArcOut, innerRadius, outerRadius, halfsize, resolveColor, uid, patterns } = + const { d3Arc, innerRadius, outerRadius, halfsize, resolveColor, patterns } = this.asProps; - const { active } = props; + const { transparent } = props; const data = this.arcs.find((arc) => arc.data[0] === props.dataKey); - if (active) { - this.activeIndexPie = index; - } + const tooltipProps = { dataKey: props.dataKey, name: props.name, resolveColor, color: props.color || getChartDefaultColorName(index), - active: props.active, transparent: props.transparent, patterns, }; + const pieId = `${this.id}-${index}`; + return { data, d3Arc, - d3ArcOut, innerRadius, outerRadius, - uid: `${uid}-${index}`, + uid: pieId, patterns, halfsize, color: props.color || getChartDefaultColorName(index), resolveColor, - $animationActivePie: this.animationActivePie, + transparent: transparent ?? (this.state.active !== null && this.state.active !== pieId), onMouseMove: this.bindHandlerTooltip(true, props, tooltipProps), onMouseLeave: this.bindHandlerTooltip(false, props, tooltipProps), onMouseOver: (e) => { - if (!active) { - this.animationActivePie({ - active: true, - data, - selector: `[d="${e.target.getAttribute('d')}"]`, - element: e.target, - }); - } + this.setState({ active: pieId }); }, onMouseOut: (e) => { - if (!active) { - this.animationActivePie({ - active: false, - data, - selector: `[d="${e.target.getAttribute('d')}"]`, - element: e.target, - }); - } + this.setState({ active: null }); }, onClickPieRoot: this.handlerOnClick(props.dataKey), }; @@ -335,49 +256,18 @@ function Pie({ d3Arc, data, color, - $animationActivePie, - active, - d3ArcOut, name, dataKey, dataHintsHandler, transparent, - innerRadius, - outerRadius, resolveColor, - halfsize, uid, patterns, onClick, onClickPieRoot, - ...other }) { - const [isMount, setIsMount] = React.useState(false); const pieRef = React.useRef(null); - React.useEffect(() => { - if (!pieRef.current) return; - pieRef.current.dataset['currentRadius'] = (active ? d3ArcOut : d3Arc).outerRadius()(); - - // do not run animation on first render - if (!isMount) { - setIsMount(true); - return; - } - if (active !== undefined && active !== null) { - $animationActivePie({ - active, - data, - selector: `[name="${other.name}"]`, - element: pieRef.current, - }); - } - }, [active, Boolean(data)]); - React.useEffect(() => { - if (!pieRef.current) return; - pieRef.current.dataset['currentRadius'] = (active ? d3ArcOut : d3Arc).outerRadius()(); - }, [active, innerRadius, outerRadius, halfsize, Boolean(data)]); - if (!data) return null; dataHintsHandler.establishDataType('values-set'); @@ -391,7 +281,7 @@ function Pie({ ref={pieRef} color={resolveColor(color)} pattern={patterns ? `url(#${uid}-pattern)` : undefined} - d={active ? d3ArcOut(data) : d3Arc(data)} + d={d3Arc(data)} transparent={transparent} use:onClick={callAllEventHandlers(onClickPieRoot, onClick)} />, diff --git a/semcore/d3-chart/src/component/Chart/DonutChart.tsx b/semcore/d3-chart/src/component/Chart/DonutChart.tsx index b2f079c966..9c1ddabece 100644 --- a/semcore/d3-chart/src/component/Chart/DonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/DonutChart.tsx @@ -73,7 +73,13 @@ class DonutChartComponent extends AbstractChart< dataKey={item.id} name={item.label} color={item.color} - active={highlightedItem === index} + transparent={highlightedItem !== -1 && highlightedItem !== index} + onMouseOver={() => { + this.setState({ highlightedItem: index }); + }} + onMouseOut={() => { + this.setState({ highlightedItem: -1 }); + }} /> ); })} diff --git a/semcore/d3-chart/src/style/donut.shadow.css b/semcore/d3-chart/src/style/donut.shadow.css index 854cb9fe31..2b5dc81cb0 100644 --- a/semcore/d3-chart/src/style/donut.shadow.css +++ b/semcore/d3-chart/src/style/donut.shadow.css @@ -1,6 +1,7 @@ SPie { stroke: var(--intergalactic-chart-grid-border, #ffffff); fill: var(--intergalactic-chart-palette-order-1, oklch(0.23 0.009 245)); + transition: opacity calc(var(--intergalactic-duration-medium, 300) * 1ms) ease-in-out; } SPie[color] { From 32f1f0af4f2ab1a9ccc2c5160c8a4264dfa698bb Mon Sep 17 00:00:00 2001 From: Valeryia Zimnitskaya Date: Mon, 26 Oct 2026 14:25:09 +0100 Subject: [PATCH 04/21] [stories] update test story --- .../docs/examples/donut-chart/donut.tsx | 2 +- .../score-donut-chart/basic-usage.tsx | 122 ++++++++++++++++++ .../tests/score-donut-chart.stories.tsx | 40 ++++++ 3 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 stories/components/d3-chart/tests/examples/score-donut-chart/basic-usage.tsx create mode 100644 stories/components/d3-chart/tests/score-donut-chart.stories.tsx diff --git a/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx b/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx index e8423b6abe..516fb52af6 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx @@ -7,7 +7,7 @@ const Demo = () => { return (
& { + patterns?: ScoreDonutChartProps['patterns'] | false; + /** + * `innerLabel` is a ReactNode and cannot travel through `loadPage` as JSON, so the story + * builds it. Turning it on is the only way to reach the `Donut.Label` branch. + */ + withInnerLabel?: boolean; + /** Set to true to replace the built-in tooltip value formatter with `formatValue`. */ + useCustomValueFormatter?: boolean; + /** + * Sets `legendProps.legendMap.value.label`. Empty string keeps the default `value` label. + * It is the only way to reach the `legendData.label` branch of `getDefaultDataDefinitions`. + */ + legendLabel?: string; +}; + +/** Storybook args: the story props plus the flattened legend type the helper un-flattens. */ +type ScoreDonutChartStoryArgs = ScoreDonutChartStoryProps & { + 'legendProps.legendType'?: 'Flex' | 'Table'; +}; + +type DemoComponent = ((props: ScoreDonutChartStoryArgs) => React.ReactElement) & { + defaultProps: ScoreDonutChartStoryArgs; +}; + +const Demo: DemoComponent = (props: ScoreDonutChartStoryArgs) => { + const { + plotWidth, + plotHeight, + withInnerLabel, + useCustomValueFormatter, + legendLabel, + ...chartProps + } = getPropsToChart(props); + + const typedChartProps = chartProps as ScoreDonutChartProps; + // A custom legend label is meaningful only with a visible legend, so the override also + // pins `showLegend: true` — the branch of the props union that accepts `legendProps`. + const resolvedChartProps: ScoreDonutChartProps = + legendLabel && typedChartProps.showLegend !== false + ? { + ...typedChartProps, + showLegend: true, + legendProps: { ...typedChartProps.legendProps, legendMap: { value: { label: legendLabel } } }, + } + : typedChartProps; + + const onClickPieHandler = (key: string, event: React.SyntheticEvent) => { + console.log('Clicked score donut pie:'); + console.log('→ Key:', key); + console.log('→ Event:', event); + }; + + return ( + + + {`${chartProps.data.value}%`} + + ), + } + : {})} + {...(useCustomValueFormatter ? { tooltipValueFormatter: formatValue } : {})} + onClickPie={onClickPieHandler} + /> + + ); +}; + +export const defaultProps: ScoreDonutChartStoryArgs = { + ...getChartProps({ + data: { value: 40 }, + // Mirrors the component defaults. `innerRadius` is left out: Donut derives it from the + // ring thickness when it is absent, and any fixed number here would change the look. + marginX: 0, + marginY: 0, + halfsize: false, + patterns: false, + withInnerLabel: false, + useCustomValueFormatter: false, + legendLabel: '', + }), + // The component defaults to a Table legend; the helper's baseline is Flex. + 'legendProps.legendType': 'Table', +}; + +Demo.defaultProps = defaultProps; + +export default Demo; diff --git a/stories/components/d3-chart/tests/score-donut-chart.stories.tsx b/stories/components/d3-chart/tests/score-donut-chart.stories.tsx new file mode 100644 index 0000000000..2da3bfb956 --- /dev/null +++ b/stories/components/d3-chart/tests/score-donut-chart.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta } from '@storybook/react-vite'; + +import BasicUsageExample, { defaultProps as basicUsageProps } from './examples/score-donut-chart/basic-usage'; +import { getChartArgTypes, legendTypeControl } from './examples/stories_props_helper'; + +const meta: Meta = { + title: 'Components/d3Charts/Tests/Score-Donut-Chart', +}; + +export default meta; + +const hidden = { table: { disable: true } } as const; + +export const BasicUsage = { + render: BasicUsageExample, + argTypes: getChartArgTypes({ + ...legendTypeControl, + data: { control: { type: 'object' } }, + innerRadius: { control: { type: 'number', min: 0, max: 150 } }, + halfsize: { control: { type: 'boolean' } }, + withInnerLabel: { control: { type: 'boolean' } }, + useCustomValueFormatter: { control: { type: 'boolean' } }, + legendLabel: { control: 'text' }, + // Omitted from ScoreDonutChartProps. + showTotalInTooltip: hidden, + showDeltaPercentInTooltip: hidden, + deltaPercentGrowthColor: hidden, + // Not used by a donut: there are no axes and one tooltip per slice. + invertAxis: hidden, + showXAxis: hidden, + showYAxis: hidden, + xTicksCount: hidden, + yTicksCount: hidden, + multilineXTicks: hidden, + multilineYTicks: hidden, + tooltipViewType: hidden, + showPercentValueInTooltip: hidden, + }), + args: basicUsageProps, +}; From 761fd0ba3b25561ad80e5e33e6b374558f0c888a Mon Sep 17 00:00:00 2001 From: Valeryia Zimnitskaya Date: Tue, 27 Oct 2026 10:28:57 +0100 Subject: [PATCH 05/21] [d3-chart] tests --- .../axe-tests/score-donut-chart.axe-test.tsx | 17 + semcore/d3-chart/__tests__/index.test.tsx | 315 ++++++++++++++ .../score-donut-chart.browser-test.tsx | 398 ++++++++++++++++++ .../score-donut-chart/basic-usage.tsx | 69 ++- .../tests/score-donut-chart.stories.tsx | 1 + 5 files changed, 781 insertions(+), 19 deletions(-) create mode 100644 semcore/d3-chart/__tests__/axe-tests/score-donut-chart.axe-test.tsx create mode 100644 semcore/d3-chart/__tests__/score-donut-chart.browser-test.tsx diff --git a/semcore/d3-chart/__tests__/axe-tests/score-donut-chart.axe-test.tsx b/semcore/d3-chart/__tests__/axe-tests/score-donut-chart.axe-test.tsx new file mode 100644 index 0000000000..7bf3d223fd --- /dev/null +++ b/semcore/d3-chart/__tests__/axe-tests/score-donut-chart.axe-test.tsx @@ -0,0 +1,17 @@ +import { expect, test, getAccessibilityViolations } from '@semcore/testing-utils/playwright'; +import { loadPage } from '@semcore/testing-utils/shared/helpers'; +import { TAG } from '@semcore/testing-utils/shared/tags'; + +test.describe(`@d3-chart @score-donut-chart ${TAG.ACCESSIBILITY}`, () => { + test('basic-usage', async ({ page }) => { + await loadPage(page, 'stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx', 'en'); + const violations = await getAccessibilityViolations({ page }); + expect(violations).toEqual([]); + }); + + test('semi-donut', async ({ page }) => { + await loadPage(page, 'stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx', 'en'); + const violations = await getAccessibilityViolations({ page }); + expect(violations).toEqual([]); + }); +}); diff --git a/semcore/d3-chart/__tests__/index.test.tsx b/semcore/d3-chart/__tests__/index.test.tsx index e13e971c52..4660301bb4 100644 --- a/semcore/d3-chart/__tests__/index.test.tsx +++ b/semcore/d3-chart/__tests__/index.test.tsx @@ -849,6 +849,321 @@ describe('Donut geometry', () => { }); }); +const SCORE_DONUT_PLOT = { plotWidth: 300, plotHeight: 300, duration: 0 } as const; + +const renderScoreDonut = (props: Record = {}) => + render( + , + ); + +const scoreDonutPies = (container: HTMLElement) => queryAllByAttribute('data-ui-name', container, 'Donut.Pie'); + +/** Angle (in degrees, clockwise from 12 o'clock) swept by the first arc command of a pie path. */ +const pieSweepDegrees = (pie: Element) => { + const d = pie.getAttribute('d') ?? ''; + const start = d.match(/^M([-\d.e]+),([-\d.e]+)/); + const end = d.match(/A[\d.]+,[\d.]+,0,(\d),1,([-\d.e]+),([-\d.e]+)/); + + if (!start || !end) return Number.NaN; + + const angle = (x: number, y: number) => ((Math.atan2(x, -y) * 180) / Math.PI + 360) % 360; + const startAngle = angle(Number(start[1]), Number(start[2])); + let sweep = angle(Number(end[2]), Number(end[3])) - startAngle; + + if (sweep <= 0) sweep += 360; + + return sweep; +}; + +describe('Chart.ScoreDonut', () => { + beforeEach(cleanup); + + describe('data.value equivalence classes', () => { + test('should split a valid value into value and other pies', async () => { + const { container } = renderScoreDonut({ data: { value: 40 } }); + const [valuePie, otherPie] = scoreDonutPies(container); + + expect(scoreDonutPies(container)).toHaveLength(2); + expect(pieSweepDegrees(valuePie)).toBeCloseTo(144, 0); + expect(pieSweepDegrees(otherPie)).toBeCloseTo(216, 0); + expect(otherPie.getAttribute('mask')).toMatch(/^url\(#.+-mask\)$/); + expect(valuePie.getAttribute('mask')).toBeNull(); + }); + + test('should keep a fractional value proportional', async () => { + const { container } = renderScoreDonut({ data: { value: 33.33 } }); + const [valuePie] = scoreDonutPies(container); + + expect(pieSweepDegrees(valuePie)).toBeCloseTo(120, 0); + }); + + test('should ignore extra keys in data', async () => { + const { container, getAllByText } = renderScoreDonut({ data: { value: 40, test: 60 }, showLegend: true }); + + expect(scoreDonutPies(container)).toHaveLength(2); + expect(getAllByText('value')).toHaveLength(1); + expect(container.textContent).not.toContain('test'); + }); + + // B6: getDefaultDataDefinitions calls value.toFixed() in the constructor. + test.fails.each([ + ['undefined', undefined], + ['null', null], + ])('should not crash when value is %s', async (_, value) => { + expect(() => renderScoreDonut({ data: { value }, showLegend: false })).not.toThrow(); + }); + + // B6: NaN does not crash, but reaches the legend as `NaN%` instead of `n/a`. + test.fails('should show n/a in the legend when value is NaN', async () => { + const { container, getAllByText } = renderScoreDonut({ data: { value: Number.NaN }, showLegend: true }); + + expect(getAllByText('n/a').length).toBeGreaterThan(0); + expect(container.textContent).not.toContain('NaN'); + }); + + // B7: the value is not clamped to the 0..100 contract. + test.fails.each([ + [120, '100.00%'], + [-10, '0.00%'], + ])('should clamp an out-of-range value %s in the legend', async (value, expected) => { + const { getByText } = renderScoreDonut({ data: { value }, showLegend: true }); + + expect(getByText(expected)).toBeTruthy(); + }); + }); + + describe('data.value boundary values', () => { + test('should render only the other pie for value 0', async () => { + const { container, getByText } = renderScoreDonut({ data: { value: 0 }, showLegend: true }); + const pies = scoreDonutPies(container); + + expect(pies).toHaveLength(1); + expect(pies[0].getAttribute('mask')).not.toBeNull(); + expect(getByText('0.00%')).toBeTruthy(); + }); + + test('should render only the value pie for value 100', async () => { + const { container } = renderScoreDonut({ data: { value: 100 } }); + const pies = scoreDonutPies(container); + + expect(pies).toHaveLength(1); + expect(pies[0].getAttribute('mask')).toBeNull(); + }); + + test('should split value 50 into two equal halves', async () => { + const { container } = renderScoreDonut({ data: { value: 50 } }); + const [valuePie, otherPie] = scoreDonutPies(container); + + expect(pieSweepDegrees(valuePie)).toBeCloseTo(180, 0); + expect(pieSweepDegrees(otherPie)).toBeCloseTo(180, 0); + }); + + test.each([0.01, 0.99, 1, 1.01])( + 'should keep a tiny value %s visible as about a 1%% slice', + async (value) => { + const { container } = renderScoreDonut({ data: { value } }); + const [valuePie] = scoreDonutPies(container); + + // Donut enlarges a slice below 1% of the total to 1% of it, i.e. ~0.99% of the new total. + expect(scoreDonutPies(container)).toHaveLength(2); + expect(pieSweepDegrees(valuePie)).toBeGreaterThan(3.5); + }, + ); + + test.each([99, 99.99])('should keep a tiny other slice visible for value %s', async (value) => { + const { container } = renderScoreDonut({ data: { value } }); + const [, otherPie] = scoreDonutPies(container); + + expect(scoreDonutPies(container)).toHaveLength(2); + expect(pieSweepDegrees(otherPie)).toBeGreaterThan(3.5); + }); + }); + + describe('innerRadius boundary values', () => { + test('should render a 16px ring by default', async () => { + const { container } = renderScoreDonut(); + const [pie] = scoreDonutPies(container); + + expect(getPieGeometry(pie).thickness).toBe(DONUT_THICKNESS); + }); + + test('should render a full pie for innerRadius 0', async () => { + const { container } = renderScoreDonut({ innerRadius: 0 }); + const [pie] = scoreDonutPies(container); + + expect(getPieGeometry(pie).innerRadius).toBe(0); + }); + + test('should render a 1px ring for innerRadius = outerRadius - 1', async () => { + const { container } = renderScoreDonut({ innerRadius: 145 }); + const [pie] = scoreDonutPies(container); + + expect(getPieGeometry(pie).thickness).toBe(1); + }); + + test('should not crash for innerRadius >= outerRadius', async () => { + expect(() => renderScoreDonut({ innerRadius: 200 })).not.toThrow(); + }); + }); + + describe('legend visibility (decision table)', () => { + test.each([ + ['undefined', undefined, false], + ['true', true, true], + ['false', false, false], + ])('showLegend %s', async (_, showLegend, expectedVisible) => { + const { queryByLabelText, container } = renderScoreDonut( + showLegend === undefined ? {} : { showLegend }, + ); + + expect(Boolean(queryByLabelText('Chart legend'))).toBe(expectedVisible); + expect(scoreDonutPies(container)).toHaveLength(2); + }); + }); + + describe('legend item content (decision table)', () => { + const renderWithLegend = (legendMapValue?: Record) => + renderScoreDonut({ + showLegend: true, + legendProps: { legendType: 'Table', ...(legendMapValue ? { legendMap: { value: legendMapValue } } : {}) }, + }); + + test('should compute percent and value columns without legendMap', async () => { + const { getByText } = renderWithLegend(); + + expect(getByText('value')).toBeTruthy(); + expect(getByText('40.00%')).toBeTruthy(); + expect(getByText('40')).toBeTruthy(); + }); + + test('should use a custom label and keep the computed columns', async () => { + const { getByText, queryByText } = renderWithLegend({ label: 'SEO score' }); + + expect(getByText('SEO score')).toBeTruthy(); + expect(queryByText('value')).toBeNull(); + expect(getByText('40.00%')).toBeTruthy(); + }); + + test('should render count after the label', async () => { + const { getByText } = renderWithLegend({ count: 12 }); + + expect(getByText('(12)')).toBeTruthy(); + }); + + test('should prefer additionalInfo over count', async () => { + const { getByText, queryByText } = renderWithLegend({ additionalInfo: 'of 100', count: 12 }); + + expect(getByText('of 100')).toBeTruthy(); + expect(queryByText('(12)')).toBeNull(); + }); + + test('should render given rows instead of computed columns', async () => { + const { getByText, queryByText } = renderWithLegend({ rows: [custom row] }); + + expect(getByText('custom row')).toBeTruthy(); + expect(queryByText('40.00%')).toBeNull(); + }); + + test('should render given columns instead of computed columns', async () => { + const { getByText, queryByText } = renderWithLegend({ columns: [custom column] }); + + expect(getByText('custom column')).toBeTruthy(); + expect(queryByText('40.00%')).toBeNull(); + }); + + test.each([['rows'], ['columns']])('should accept an empty %s array', async (key) => { + expect(() => renderWithLegend({ [key]: [] })).not.toThrow(); + }); + + test('should start unchecked with defaultChecked false and not draw the value pie', async () => { + const { container } = renderWithLegend({ defaultChecked: false }); + const masked = scoreDonutPies(container).filter((pie) => pie.getAttribute('mask')); + + expect(scoreDonutPies(container)).toHaveLength(masked.length); + }); + }); + + describe('state transitions', () => { + test('should update pies when value changes 40 -> 80 -> 0 -> 40', async () => { + const { container, rerender } = renderScoreDonut({ data: { value: 40 } }); + + const rerenderWith = (value: number) => + rerender(); + + rerenderWith(80); + expect(pieSweepDegrees(scoreDonutPies(container)[0])).toBeCloseTo(288, 0); + + rerenderWith(0); + expect(scoreDonutPies(container)).toHaveLength(1); + + rerenderWith(40); + expect(scoreDonutPies(container)).toHaveLength(2); + expect(pieSweepDegrees(scoreDonutPies(container)[0])).toBeCloseTo(144, 0); + }); + + // B3: the other pie is drawn even when the value item is unchecked. + test.fails('should draw only the empty state when the value item is unchecked', async () => { + const { container, getByRole } = renderScoreDonut({ showLegend: true }); + + await userEvent.click(getByRole('checkbox')); + + expect(queryByAttribute('data-ui-name', container, 'Donut.EmptyData')).not.toBeNull(); + expect(scoreDonutPies(container)).toHaveLength(0); + }); + }); + + describe('error guessing', () => { + test('should call onClickPie with value and other keys', async () => { + const onClickPie = vi.fn(); + const { container } = renderScoreDonut({ onClickPie }); + const [valuePie, otherPie] = scoreDonutPies(container); + + await userEvent.click(valuePie); + await userEvent.click(otherPie); + + expect(onClickPie.mock.calls.map(([key]) => key)).toEqual(['value', 'other']); + }); + + test('should give two charts on one page unique mask ids', async () => { + const { container } = render( + <> + + + , + ); + const maskIds = [...container.querySelectorAll('mask')].map((mask) => mask.id); + const references = scoreDonutPies(container) + .map((pie) => pie.getAttribute('mask')) + .filter(Boolean); + + expect(maskIds).toHaveLength(2); + expect(new Set(maskIds).size).toBe(2); + references.forEach((reference, index) => expect(reference).toBe(`url(#${maskIds[index]})`)); + }); + + test('should render innerLabel inside the donut', async () => { + const { container } = renderScoreDonut({ innerLabel: '40%' }); + + expect(queryByAttribute('data-ui-name', container, 'Donut.Label')?.textContent).toBe('40%'); + }); + }); + + describe('accessibility', () => { + test('should put role group on the root and aria-label on the plot', async () => { + const { container } = renderScoreDonut(); + + expect(container.querySelector('[role="group"]')).not.toBeNull(); + expect(container.querySelector('svg[aria-label="Score donut chart"]')).not.toBeNull(); + }); + + test('should hide pies from assistive technologies', async () => { + const { container } = renderScoreDonut(); + + scoreDonutPies(container).forEach((pie) => expect(pie.closest('[aria-hidden]')).not.toBeNull()); + }); + }); +}); + describe('Chart.Line', () => { beforeEach(cleanup); diff --git a/semcore/d3-chart/__tests__/score-donut-chart.browser-test.tsx b/semcore/d3-chart/__tests__/score-donut-chart.browser-test.tsx new file mode 100644 index 0000000000..56041c807b --- /dev/null +++ b/semcore/d3-chart/__tests__/score-donut-chart.browser-test.tsx @@ -0,0 +1,398 @@ +import type { Locator, Page } from '@semcore/testing-utils/playwright'; +import { expect, test } from '@semcore/testing-utils/playwright'; +import { loadPage } from '@semcore/testing-utils/shared/helpers'; +import { TAG } from '@semcore/testing-utils/shared/tags'; + +const STORY = 'stories/components/d3-chart/tests/examples/score-donut-chart/basic-usage.tsx'; + +const COMPONENT_TAGS = ['@score-donut-chart', '@d3-chart', '@base-components', '@flex-box', '@typography']; + +export const locators = { + plot: (page: Page) => page.locator('svg[data-ui-name="Plot"]'), + pie: (page: Page, index?: number) => { + const base = page.locator('[data-ui-name="Donut.Pie"]'); + return typeof index === 'number' ? base.nth(index) : base; + }, + otherPie: (page: Page) => page.locator('[data-ui-name="Donut.Pie"][mask]'), + valuePie: (page: Page) => page.locator('[data-ui-name="Donut.Pie"]:not([mask])'), + emptyData: (page: Page) => page.locator('[data-ui-name="Donut.EmptyData"]'), + tooltip: (page: Page) => page.locator('[data-ui-name="Donut.Tooltip"]'), + legend: (page: Page) => page.getByLabel('Chart legend'), + legendCheckbox: (page: Page, name = 'value') => page.getByLabel('Chart legend').getByRole('checkbox', { name }), + legendItem: (page: Page, text = 'value') => page.getByLabel('Chart legend').getByText(text, { exact: true }), +}; + +const load = async (page: Page, props: Record = {}) => { + await loadPage(page, STORY, 'en', { duration: 0, ...props }); + await locators.plot(page).waitFor({ state: 'visible' }); + // The chart measures its container first and re-renders with the final radius. + await page.waitForTimeout(300); +}; + +/** +Hovers the middle of the ring at a point of the outer arc. The midpoint between the outer and +inner arcs used by the Donut tests falls into the hole for wide arcs, so the point is taken in +the middle of the outer arc (a quarter of the path, which starts with the outer arc) and moved +halfway into the ring. The mouse is moved directly: +hovering starts the radius animation, and locator.hover() never sees the pie stable. +*/ +const hoverPie = async (pie: Locator) => { + const point = await pie.evaluate((path: SVGPathElement) => { + const onOuterArc = path.getPointAtLength(path.getTotalLength() * 0.25); + const radius = Math.hypot(onOuterArc.x, onOuterArc.y); + const scale = (radius - 8) / radius; + const svgPoint = path.ownerSVGElement!.createSVGPoint(); + + svgPoint.x = onOuterArc.x * scale; + svgPoint.y = onOuterArc.y * scale; + + const { x, y } = svgPoint.matrixTransform(path.getScreenCTM()!); + + return { x, y }; + }); + + await pie.page().mouse.move(point.x, point.y, { steps: 5 }); +}; + +/** Hovers a pie until its tooltip is shown: the first mouse events can land before the chart settles. */ +const hoverPieForTooltip = async (page: Page, pie: Locator) => { + await expect(async () => { + await page.mouse.move(0, 0); + await hoverPie(pie); + await expect(locators.tooltip(page)).toBeVisible({ timeout: 1000 }); + }).toPass(); +}; + +const outerRadius = (pie: Locator) => + pie.evaluate((path) => { + const radii = [...(path.getAttribute('d') ?? '').matchAll(/A([\d.]+),/g)].map((match) => Number(match[1])); + + return Math.max(...radii); + }); + +/** +Resolves the mask rectangle of the `other` pie into the pie's user space, where percentages +are taken from the svg viewport, and compares it with the pie's bounding box. +*/ +const maskCoversOtherPie = (page: Page) => + locators.otherPie(page).evaluate((path: SVGPathElement) => { + const svg = path.ownerSVGElement!; + const id = (path.getAttribute('mask') ?? '').replace(/^url\(#(.+)\)$/, '$1'); + const rect = svg.querySelector(`mask[id="${id}"] rect`)!; + const { width, height } = svg.getBoundingClientRect(); + const resolve = (value: string | null, base: number) => + value?.endsWith('%') ? (Number.parseFloat(value) / 100) * base : Number(value ?? 0); + + const maskX = resolve(rect.getAttribute('x'), width); + const maskY = resolve(rect.getAttribute('y'), height); + const maskWidth = resolve(rect.getAttribute('width'), width); + const maskHeight = resolve(rect.getAttribute('height'), height); + const box = path.getBBox(); + + return ( + box.x >= maskX && + box.y >= maskY && + box.x + box.width <= maskX + maskWidth && + box.y + box.height <= maskY + maskHeight + ); + }); + +/* ===================================================== +@visual +Visual states, hover styles and snapshots. +===================================================== */ +test.describe(`${TAG.VISUAL}`, () => { + const values = [ + { description: 'value 0: only the other pie', data: { value: 0 } }, + { description: 'value 50: two equal halves', data: { value: 50 } }, + { description: 'value 100: only the value pie', data: { value: 100 } }, + { description: 'innerRadius 0: full pie', data: { value: 40 }, innerRadius: 0 }, + ]; + + values.forEach((item) => { + test(`Verify score donut ${item.description}`, { + tag: [TAG.PRIORITY_HIGH, ...COMPONENT_TAGS], + }, async ({ page }) => { + const { description: _, ...props } = item; + await load(page, props); + + await test.step('Verify chart renders correctly', async () => { + await expect(page).toHaveScreenshot(); + }); + }); + }); + + // Pairwise coverage of halfsize × innerLabel × patterns × legendType × showTooltip × locale. + const pairwise = [ + { halfsize: false, withInnerLabel: false, patterns: false, legendType: 'Table', showTooltip: true, locale: 'en' }, + { halfsize: false, withInnerLabel: true, patterns: true, legendType: 'Flex', showTooltip: false, locale: 'ja' }, + { halfsize: true, withInnerLabel: false, patterns: true, legendType: 'Table', showTooltip: false, locale: 'ja' }, + { halfsize: true, withInnerLabel: true, patterns: false, legendType: 'Flex', showTooltip: true, locale: 'en' }, + { halfsize: false, withInnerLabel: false, patterns: true, legendType: 'Flex', showTooltip: true, locale: 'en' }, + { halfsize: true, withInnerLabel: true, patterns: false, legendType: 'Table', showTooltip: false, locale: 'ja' }, + { halfsize: false, withInnerLabel: true, patterns: false, legendType: 'Table', showTooltip: true, locale: 'ja' }, + { halfsize: true, withInnerLabel: false, patterns: true, legendType: 'Flex', showTooltip: true, locale: 'en' }, + ] as const; + + pairwise.forEach((item, index) => { + test(`Verify score donut pairwise combination ${index + 1}`, { + tag: [TAG.PRIORITY_MEDIUM, ...COMPONENT_TAGS, '@chart-legend'], + }, async ({ page }) => { + const { legendType, ...props } = item; + await load(page, { ...props, 'showLegend': true, 'legendProps.legendType': legendType }); + + await test.step('Verify chart renders correctly', async () => { + await expect(page).toHaveScreenshot(); + }); + + if (item.showTooltip) { + await test.step('Verify value pie highlights on hover', async () => { + await hoverPieForTooltip(page, locators.valuePie(page)); + await expect(page).toHaveScreenshot(); + }); + } + }); + }); +}); + +/* ===================================================== +@functional +Keyboard and mouse interactions - no snapshots here. +We verify states, visibility, and attributes. +===================================================== */ +test.describe(`${TAG.FUNCTIONAL}`, () => { + test.describe('Tooltip', () => { + test('Verify tooltip shows the value on the value pie', { + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, ...COMPONENT_TAGS], + }, async ({ page }) => { + await load(page, { data: { value: 40 } }); + + await test.step('Verify tooltip appears on hover', async () => { + await hoverPieForTooltip(page, locators.valuePie(page)); + await expect(locators.tooltip(page)).toContainText('value'); + await expect(locators.tooltip(page)).toContainText('40'); + }); + + await test.step('Verify tooltip disappears on leave', async () => { + await page.mouse.move(0, 0); + await expect(locators.tooltip(page)).toBeHidden(); + }); + }); + + test('Verify tooltip uses a custom value formatter', { + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, ...COMPONENT_TAGS], + }, async ({ page }) => { + await load(page, { data: { value: 40 }, useCustomValueFormatter: true }); + + await hoverPieForTooltip(page, locators.valuePie(page)); + await expect(locators.tooltip(page)).toContainText('40% score'); + }); + + test('Verify no tooltip when showTooltip is false', { + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, ...COMPONENT_TAGS], + }, async ({ page }) => { + await load(page, { data: { value: 40 }, showTooltip: false }); + + await hoverPie(locators.valuePie(page)); + await page.waitForTimeout(300); + await expect(locators.tooltip(page)).toBeHidden(); + }); + + test('Verify tooltip shows the rest of the score on the other pie', { + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, ...COMPONENT_TAGS], + }, async ({ page }) => { + test.fixme(true, 'B1: tooltip on the other pie shows data.value instead of 100 - value'); + await load(page, { data: { value: 40 } }); + + await hoverPieForTooltip(page, locators.otherPie(page)); + await expect(locators.tooltip(page)).toContainText('60'); + await expect(locators.tooltip(page)).not.toContainText('other'); + }); + }); + + test.describe('Legend', () => { + test('Verify legend toggles the value pie by mouse', { + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, ...COMPONENT_TAGS, '@chart-legend', '@checkbox'], + }, async ({ page }) => { + await load(page, { showLegend: true }); + + await test.step('Verify value pie is drawn by default', async () => { + await expect(locators.legendCheckbox(page)).toBeChecked(); + await expect(locators.valuePie(page)).toHaveCount(1); + }); + + await test.step('Verify unchecking hides the value pie', async () => { + await locators.legendItem(page).click(); + await expect(locators.legendCheckbox(page)).not.toBeChecked(); + await expect(locators.valuePie(page)).toHaveCount(0); + await expect(locators.emptyData(page)).toHaveCount(1); + }); + + await test.step('Verify checking again brings the value pie back', async () => { + await locators.legendItem(page).click(); + await expect(locators.legendCheckbox(page)).toBeChecked(); + await expect(locators.valuePie(page)).toHaveCount(1); + await expect(locators.emptyData(page)).toHaveCount(0); + }); + }); + + test('Verify legend toggles the value pie by keyboard', { + tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, ...COMPONENT_TAGS, '@chart-legend', '@checkbox'], + }, async ({ page, browserName }) => { + test.skip(browserName === 'webkit', 'Tab does not focus checkboxes in WebKit by default'); + await load(page, { showLegend: true }); + + await test.step('Verify checkbox receives focus by Tab', async () => { + await page.keyboard.press('Tab'); + await expect(locators.legendCheckbox(page)).toBeFocused(); + }); + + await test.step('Verify Space toggles the value pie', async () => { + await page.keyboard.press('Space'); + await expect(locators.legendCheckbox(page)).not.toBeChecked(); + await expect(locators.valuePie(page)).toHaveCount(0); + + await page.keyboard.press('Space'); + await expect(locators.legendCheckbox(page)).toBeChecked(); + await expect(locators.valuePie(page)).toHaveCount(1); + }); + }); + + test('Verify unchecked value item does not look like a 100% other ring', { + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, ...COMPONENT_TAGS, '@chart-legend'], + }, async ({ page }) => { + test.fixme(true, 'B3: the other pie is drawn over EmptyData when the value item is unchecked'); + await load(page, { showLegend: true }); + + await locators.legendItem(page).click(); + await expect(locators.otherPie(page)).toHaveCount(0); + }); + + test('Verify hover on the legend item highlights the value pie', { + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, ...COMPONENT_TAGS, '@chart-legend'], + }, async ({ page }) => { + await load(page, { showLegend: true }); + const idleRadius = await outerRadius(locators.valuePie(page)); + + await locators.legendItem(page).hover(); + await expect.poll(() => outerRadius(locators.valuePie(page))).toBe(idleRadius + 4); + }); + + test('Verify value pie returns to its size after leaving the legend item', { + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, ...COMPONENT_TAGS, '@chart-legend'], + }, async ({ page }) => { + test.fixme(true, 'B9: the value pie stays highlighted after the mouse leaves the legend item'); + await load(page, { showLegend: true }); + const idleRadius = await outerRadius(locators.valuePie(page)); + + await locators.legendItem(page).hover(); + await expect.poll(() => outerRadius(locators.valuePie(page))).toBe(idleRadius + 4); + + await page.mouse.move(0, 0); + await expect.poll(() => outerRadius(locators.valuePie(page))).toBe(idleRadius); + }); + + test('Verify side metric layout hides the legend', { + tag: [TAG.PRIORITY_LOW, ...COMPONENT_TAGS, '@divider'], + }, async ({ page }) => { + await load(page, { withSideMetric: true, data: { value: 3 } }); + + await expect(locators.legend(page)).toBeHidden(); + await expect(page.getByText('71,286')).toBeVisible(); + await expect(page.getByText('value name')).toBeVisible(); + }); + }); + + test.describe('Other pie', () => { + test('Verify mask covers the other pie in a full donut', { + tag: [TAG.PRIORITY_HIGH, ...COMPONENT_TAGS], + }, async ({ page }) => { + await load(page, { data: { value: 40 } }); + + expect(await maskCoversOtherPie(page)).toBe(true); + }); + + test('Verify mask covers the other pie in a semi donut', { + tag: [TAG.PRIORITY_HIGH, ...COMPONENT_TAGS], + }, async ({ page }) => { + test.fixme(true, 'B2: the mask rect is sized from the svg, so the top of a semi-donut arc is cut off'); + await load(page, { data: { value: 40 }, halfsize: true }); + + expect(await maskCoversOtherPie(page)).toBe(true); + }); + + test('Verify other pie is not painted black', { + tag: [TAG.PRIORITY_HIGH, ...COMPONENT_TAGS], + }, async ({ page }) => { + test.fixme(true, 'B5: token chart-palette-order-other-data was renamed to chart-data-other-value'); + await load(page, { data: { value: 40 } }); + + const fill = await locators.otherPie(page).evaluate((path) => getComputedStyle(path).fill); + expect(fill).not.toBe('rgb(0, 0, 0)'); + }); + + test('Verify other pie gets a single fill method with patterns', { + tag: [TAG.PRIORITY_MEDIUM, ...COMPONENT_TAGS], + }, async ({ page }) => { + test.fixme(true, 'B4: with patterns the other pie gets both a pattern fill and the hatch mask'); + await load(page, { data: { value: 40 }, patterns: true }); + + const otherPie = locators.otherPie(page); + const hasPattern = await otherPie.evaluate((path) => (path.getAttribute('pattern') ?? '').startsWith('url(')); + expect(hasPattern).toBe(false); + }); + }); + + test.describe('Layout', () => { + test('Verify chart follows the container size', { + tag: [TAG.PRIORITY_MEDIUM, ...COMPONENT_TAGS], + }, async ({ page }) => { + await load(page, { plotWidth: 500, plotHeight: 300 }); + const initialWidth = (await locators.plot(page).boundingBox())!.width; + + await page.locator('[data-testid="TEST"]').evaluate((chart) => { + (chart.parentElement as HTMLElement).style.width = '400px'; + }); + + await expect.poll(async () => (await locators.plot(page).boundingBox())!.width).toBeLessThan(initialWidth); + }); + }); + + test.describe('Accessibility', () => { + test('Verify aria attributes', { + tag: [TAG.PRIORITY_HIGH, ...COMPONENT_TAGS], + }, async ({ page }) => { + await load(page); + + await expect(locators.plot(page)).toHaveAttribute('aria-label', 'Score donut chart'); + await expect(page.locator('[data-testid="TEST"]')).toHaveAttribute('role', 'group'); + await expect(page.locator('[data-ui-name="Donut"]')).toHaveAttribute('aria-hidden', 'true'); + }); + + test('Verify data summary contains both values', { + tag: [TAG.PRIORITY_MEDIUM, TAG.KEYBOARD, ...COMPONENT_TAGS], + }, async ({ page }) => { + await load(page, { data: { value: 40 } }); + + await page.getByRole('button', { name: 'Open data summary' }).focus(); + await page.keyboard.press('Enter'); + + const summary = page.getByText(/Chart represents/); + await expect(summary).toBeVisible(); + await expect(summary).toContainText('40'); + await expect(summary).toContainText('60'); + }); + + test('Verify data summary uses human-readable names', { + tag: [TAG.PRIORITY_LOW, TAG.KEYBOARD, ...COMPONENT_TAGS], + }, async ({ page }) => { + test.fixme(true, 'B8: the data summary reads raw keys ("other of value 60 and value of value 40")'); + await load(page, { data: { value: 40 } }); + + await page.getByRole('button', { name: 'Open data summary' }).focus(); + await page.keyboard.press('Enter'); + + await expect(page.getByText(/Chart represents/)).not.toContainText('of value'); + }); + }); +}); diff --git a/stories/components/d3-chart/tests/examples/score-donut-chart/basic-usage.tsx b/stories/components/d3-chart/tests/examples/score-donut-chart/basic-usage.tsx index c4899e5582..c3175eace3 100644 --- a/stories/components/d3-chart/tests/examples/score-donut-chart/basic-usage.tsx +++ b/stories/components/d3-chart/tests/examples/score-donut-chart/basic-usage.tsx @@ -1,6 +1,8 @@ -import { Box } from '@semcore/ui/base-components'; +import DiffUp from '@semcore/icon/DiffUp/m'; +import { Box, Flex } from '@semcore/ui/base-components'; import type { ScoreDonutChartProps } from '@semcore/ui/d3-chart'; -import { Chart } from '@semcore/ui/d3-chart'; +import { Chart, Metric } from '@semcore/ui/d3-chart'; +import Divider from '@semcore/ui/divider'; import { Text } from '@semcore/ui/typography'; import React from 'react'; @@ -32,6 +34,12 @@ type ScoreDonutChartStoryProps = Omit & { * It is the only way to reach the `legendData.label` branch of `getDefaultDataDefinitions`. */ legendLabel?: string; + /** + * Hides the legend and renders a `Metric` (value + diff), a description and a divider to the + * right of the donut, as in the "Donut chart + ChartLegend" Figma layout. The chart has no + * slot for it, so the story composes it next to the chart. + */ + withSideMetric?: boolean; }; /** Storybook args: the story props plus the flattened legend type the helper un-flattens. */ @@ -50,14 +58,17 @@ const Demo: DemoComponent = (props: ScoreDonutChartStoryArgs) => { withInnerLabel, useCustomValueFormatter, legendLabel, + withSideMetric, ...chartProps } = getPropsToChart(props); const typedChartProps = chartProps as ScoreDonutChartProps; + const { legendProps: _legendProps, ...chartPropsWithoutLegend } = typedChartProps; // A custom legend label is meaningful only with a visible legend, so the override also // pins `showLegend: true` — the branch of the props union that accepts `legendProps`. - const resolvedChartProps: ScoreDonutChartProps = - legendLabel && typedChartProps.showLegend !== false + const resolvedChartProps: ScoreDonutChartProps = withSideMetric + ? { ...chartPropsWithoutLegend, showLegend: false } + : legendLabel && typedChartProps.showLegend !== false ? { ...typedChartProps, showLegend: true, @@ -71,6 +82,27 @@ const Demo: DemoComponent = (props: ScoreDonutChartStoryArgs) => { console.log('→ Event:', event); }; + const chart = ( + + {`${chartProps.data.value}%`} + + ), + } + : {})} + {...(useCustomValueFormatter ? { tooltipValueFormatter: formatValue } : {})} + // The Figma layout uses a 180×180 donut; in a row the chart cannot measure its own size. + {...(withSideMetric ? { plotWidth: 180, plotHeight: 180, w: 180, flex: 'none' } : {})} + onClickPie={onClickPieHandler} + /> + ); + return ( { h={plotHeight} overflow='auto' > - - {`${chartProps.data.value}%`} + {withSideMetric + ? ( + + {chart} + + + + value name - ), - } - : {})} - {...(useCustomValueFormatter ? { tooltipValueFormatter: formatValue } : {})} - onClickPie={onClickPieHandler} - /> + + + + ) + : chart} ); }; @@ -112,6 +142,7 @@ export const defaultProps: ScoreDonutChartStoryArgs = { withInnerLabel: false, useCustomValueFormatter: false, legendLabel: '', + withSideMetric: false, }), // The component defaults to a Table legend; the helper's baseline is Flex. 'legendProps.legendType': 'Table', diff --git a/stories/components/d3-chart/tests/score-donut-chart.stories.tsx b/stories/components/d3-chart/tests/score-donut-chart.stories.tsx index 2da3bfb956..a08783da2a 100644 --- a/stories/components/d3-chart/tests/score-donut-chart.stories.tsx +++ b/stories/components/d3-chart/tests/score-donut-chart.stories.tsx @@ -21,6 +21,7 @@ export const BasicUsage = { withInnerLabel: { control: { type: 'boolean' } }, useCustomValueFormatter: { control: { type: 'boolean' } }, legendLabel: { control: 'text' }, + withSideMetric: { control: { type: 'boolean' } }, // Omitted from ScoreDonutChartProps. showTotalInTooltip: hidden, showDeltaPercentInTooltip: hidden, From b4ed876502584458056cebee6dc1ef38c29d4937 Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Tue, 27 Oct 2026 10:44:43 +0100 Subject: [PATCH 06/21] [d3-chart] fixed color for the score donut chart pattern, fixed pattern for halfsized score donut --- semcore/d3-chart/src/Pattern.tsx | 4 ++-- semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/semcore/d3-chart/src/Pattern.tsx b/semcore/d3-chart/src/Pattern.tsx index e073fc44d4..dcacb5bf15 100644 --- a/semcore/d3-chart/src/Pattern.tsx +++ b/semcore/d3-chart/src/Pattern.tsx @@ -628,7 +628,7 @@ const resolvePattern = (patternKey: string, patternsConfig: PatternsConfig = tru return patterns[patternIndex]; }; -export const DefaultStrokePattern = (props: { id: string }) => { +export const DefaultStrokePattern = (props: { id: string; halfsize?: boolean }) => { return ( { - + ); diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx index 914d2fa401..ba94e69542 100644 --- a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -106,7 +106,7 @@ class ScoreDonutChartComponent extends AbstractChart< } renderChart() { - const { innerRadius, halfsize, innerLabel, onClickPie, data, uid } = this.asProps; + const { innerRadius, halfsize, innerLabel, onClickPie, uid } = this.asProps; const { dataDefinitions, highlightedItem } = this.state; const checkedLegendItems = dataDefinitions.filter((item) => item.checked); @@ -125,10 +125,10 @@ class ScoreDonutChartComponent extends AbstractChart< /> ); })} - + From 34d51b1517d24111c0fc0c555111575db3c11006 Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Tue, 27 Oct 2026 10:49:05 +0100 Subject: [PATCH 07/21] [d3-chart] hide tooltip for the empty donut part --- semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx index ba94e69542..b94eee233d 100644 --- a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -146,6 +146,12 @@ class ScoreDonutChartComponent extends AbstractChart< {({ dataKey }: any) => { const title = dataDefinitions.find((item) => item.id === dataKey); + if (!title) { + return { + visible: false, + }; + } + return { children: ( <> From 1281b6e5b7422cffcb1325851e2ba91156aa308f Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Tue, 27 Oct 2026 12:57:47 +0100 Subject: [PATCH 08/21] [d3-chart] some fixes for the 2 phase of donut chart redesign --- semcore/d3-chart/src/Donut.jsx | 29 ++++++++++++++----- .../src/component/Chart/DonutChart.tsx | 7 ++++- .../src/component/Chart/DonutChart.type.ts | 7 +++-- semcore/d3-chart/src/style/donut.shadow.css | 8 ----- .../skeleton/src/svg/donut-chart-halfsize.svg | 7 +++-- semcore/skeleton/src/svg/donut-chart.svg | 8 ++--- .../examples/donut-chart/basic-usage.tsx | 3 ++ 7 files changed, 44 insertions(+), 25 deletions(-) diff --git a/semcore/d3-chart/src/Donut.jsx b/semcore/d3-chart/src/Donut.jsx index a889a3b217..5a98c7c427 100644 --- a/semcore/d3-chart/src/Donut.jsx +++ b/semcore/d3-chart/src/Donut.jsx @@ -1,3 +1,4 @@ +import { Flex } from '@semcore/base-components'; import { Component, Root, sstyled } from '@semcore/core'; import { callAllEventHandlers } from '@semcore/core/lib/utils/assignProps'; import getOriginChildren from '@semcore/core/lib/utils/getOriginChildren'; @@ -66,15 +67,16 @@ class DonutRoot extends Component { static defaultProps = ({ innerRadius: innerRadiusProp, - paddingAngle = 0, + paddingAngle, outerRadius, halfsize = false, $rootProps: { size }, }) => { const radius = outerRadius || getOuterRadius({ size, halfsize }); const innerRadius = innerRadiusProp ?? Math.max(radius - donutThickness, 0); + const padAngle = paddingAngle ?? 2 / radius; - const d3Arc = arc().outerRadius(radius).innerRadius(innerRadius).padAngle(paddingAngle); + const d3Arc = arc().outerRadius(radius).innerRadius(innerRadius).padAngle(padAngle).cornerRadius(2); let d3Pie = pie() .sort(null) @@ -220,6 +222,14 @@ class DonutRoot extends Component { }; } + getLabelProps() { + const { halfsize } = this.asProps; + + return { + halfsize, + }; + } + componentDidUpdate(prevProps) { const { data } = this.asProps; if (prevProps.$rootProps.data !== data) { @@ -298,7 +308,9 @@ function Pie({ ); } -function EmptyData({ Element: SEmptyData, styles, d3Arc, color, resolveColor }) { +function EmptyData({ Element: SEmptyData, styles, d3Arc, color, resolveColor, emptyData }) { + const data = emptyData ?? { a: 8, b: 12, c: 36 }; + return sstyled(styles)( - + + + + , ); } diff --git a/semcore/d3-chart/src/component/Chart/DonutChart.tsx b/semcore/d3-chart/src/component/Chart/DonutChart.tsx index 9c1ddabece..ffa2ceedb7 100644 --- a/semcore/d3-chart/src/component/Chart/DonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/DonutChart.tsx @@ -83,7 +83,12 @@ class DonutChartComponent extends AbstractChart< /> ); })} - {innerLabel && {innerLabel}} + {innerLabel && ( + + {innerLabel[0]} + {innerLabel[1]} + + )} ); } diff --git a/semcore/d3-chart/src/component/Chart/DonutChart.type.ts b/semcore/d3-chart/src/component/Chart/DonutChart.type.ts index d7ea7055ce..38701e69d5 100644 --- a/semcore/d3-chart/src/component/Chart/DonutChart.type.ts +++ b/semcore/d3-chart/src/component/Chart/DonutChart.type.ts @@ -23,8 +23,11 @@ export type DonutChartProps = Intergalactic.InternalTypings.EfficientOmit< innerRadius?: number; /** Creates a semi-donut chart when enabled */ halfsize?: boolean; - /** Content displayed in the center of the donut */ - innerLabel?: React.ReactNode; + /** + * Content displayed in the center of the donut + * ["Label (usually total number)", "Description (some text)"] + */ + innerLabel?: [string, string]; /** Callback triggered when a user clicks on a pie */ onClickPie?: (key: DataKey, e: React.SyntheticEvent) => void; }; diff --git a/semcore/d3-chart/src/style/donut.shadow.css b/semcore/d3-chart/src/style/donut.shadow.css index 2b5dc81cb0..be61533dc0 100644 --- a/semcore/d3-chart/src/style/donut.shadow.css +++ b/semcore/d3-chart/src/style/donut.shadow.css @@ -25,11 +25,3 @@ SEmptyData { SEmptyData[color] { fill: var(--color); } - -SLabel { - text-anchor: middle; - vertical-anchor: middle; - font-size: var(--intergalactic-fs-200, 14px); - line-height: var(--intergalactic-lh-200, 1.4286); - -} diff --git a/semcore/skeleton/src/svg/donut-chart-halfsize.svg b/semcore/skeleton/src/svg/donut-chart-halfsize.svg index b35a514089..d21241bbb0 100644 --- a/semcore/skeleton/src/svg/donut-chart-halfsize.svg +++ b/semcore/skeleton/src/svg/donut-chart-halfsize.svg @@ -1,4 +1,5 @@ - - - + + + + diff --git a/semcore/skeleton/src/svg/donut-chart.svg b/semcore/skeleton/src/svg/donut-chart.svg index b6de3bb6b0..3e721e62a5 100644 --- a/semcore/skeleton/src/svg/donut-chart.svg +++ b/semcore/skeleton/src/svg/donut-chart.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/stories/components/d3-chart/tests/examples/donut-chart/basic-usage.tsx b/stories/components/d3-chart/tests/examples/donut-chart/basic-usage.tsx index 82d2a81d85..5ad45f4a7c 100644 --- a/stories/components/d3-chart/tests/examples/donut-chart/basic-usage.tsx +++ b/stories/components/d3-chart/tests/examples/donut-chart/basic-usage.tsx @@ -19,6 +19,7 @@ const Demo = (props: DonutChartProps) => { > @@ -33,6 +34,8 @@ const data = { const propsFromHelper = getChartProps({ data, + innerLabel: '71,240', + innerDescription: 'Description', }); // Remove showLegend from defaultProps to allow DonutChart's internal logic to work From 2a980609b8f7a9a3968207a78795493d6807f190 Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Tue, 27 Oct 2026 14:41:56 +0100 Subject: [PATCH 09/21] [d3-chart] in ScoreDonut use empty value pattern even with patterns --- semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx index b94eee233d..ac13645725 100644 --- a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -131,6 +131,7 @@ class ScoreDonutChartComponent extends AbstractChart< color='chart-data-other-value' mask={`url(#${uid}-mask)`} uid={uid} + patterns={false} /> {innerLabel && {innerLabel}} From d5bea2bc6644bf21ba7467e7b47a98710857ff8b Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Tue, 27 Oct 2026 15:06:23 +0100 Subject: [PATCH 10/21] [d3-chart] fixed donut size for differen heights --- semcore/d3-chart/src/Donut.jsx | 7 +-- semcore/d3-chart/src/style/area.shadow.css | 4 +- semcore/d3-chart/src/style/bar.shadow.css | 2 +- semcore/d3-chart/src/style/bubble.shadow.css | 6 +- semcore/d3-chart/src/style/donut.shadow.css | 2 +- semcore/d3-chart/src/style/dot.shadow.css | 2 +- semcore/d3-chart/src/style/line.shadow.css | 2 +- semcore/d3-chart/src/style/radar.shadow.css | 4 +- .../d3-chart/src/style/radial-tree.shadow.css | 8 +-- .../d3-chart/src/style/scatterplot.shadow.css | 4 +- semcore/d3-chart/src/style/venn.shadow.css | 6 +- .../docs/donut-chart.docs.stories.tsx | 5 -- .../docs/examples/donut-chart/basic-usage.tsx | 2 +- .../donut-controlled-highlight.tsx | 57 ------------------- .../docs/examples/donut-chart/donut.tsx | 2 +- .../docs/examples/donut-chart/edge-cases.tsx | 2 +- .../donut-chart/legend-and-pattern-fill.tsx | 4 +- .../docs/examples/donut-chart/semi-donut.tsx | 2 +- tools/theme/src/theme.ts | 7 +++ .../donut-chart/donut-chart-code.md | 12 ---- .../style/design-tokens/design-tokens.json | 8 +++ 21 files changed, 44 insertions(+), 104 deletions(-) delete mode 100644 stories/components/d3-chart/docs/examples/donut-chart/donut-controlled-highlight.tsx diff --git a/semcore/d3-chart/src/Donut.jsx b/semcore/d3-chart/src/Donut.jsx index 5a98c7c427..292fc652b1 100644 --- a/semcore/d3-chart/src/Donut.jsx +++ b/semcore/d3-chart/src/Donut.jsx @@ -51,13 +51,12 @@ function transitionAnglePie({ }); } -const increaseFactor = 4; -const donutThickness = 16; +const donutThickness = 12; function getOuterRadius({ size, halfsize }) { const [width, height] = size; const minORmax = halfsize ? Math.max : Math.min; - return minORmax(width - increaseFactor * 2, height - increaseFactor * 2) / 2; + return minORmax(width, height) / 2; } class DonutRoot extends Component { @@ -335,7 +334,7 @@ function Label({ return sstyled(styles)( - + , diff --git a/semcore/d3-chart/src/style/area.shadow.css b/semcore/d3-chart/src/style/area.shadow.css index a8e5f6a1e8..8921f535e5 100644 --- a/semcore/d3-chart/src/style/area.shadow.css +++ b/semcore/d3-chart/src/style/area.shadow.css @@ -17,7 +17,7 @@ SArea[pattern] { } SArea[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SAreaLine { @@ -31,7 +31,7 @@ SAreaLine[color] { } SAreaLine[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SArea, diff --git a/semcore/d3-chart/src/style/bar.shadow.css b/semcore/d3-chart/src/style/bar.shadow.css index ce77902571..f35e9c61e4 100644 --- a/semcore/d3-chart/src/style/bar.shadow.css +++ b/semcore/d3-chart/src/style/bar.shadow.css @@ -20,7 +20,7 @@ SBar[hide] { } SBar[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-data-transparent-opacity, undefined); } SBar[onClick] { diff --git a/semcore/d3-chart/src/style/bubble.shadow.css b/semcore/d3-chart/src/style/bubble.shadow.css index 2334559e22..3cbbdc26e5 100644 --- a/semcore/d3-chart/src/style/bubble.shadow.css +++ b/semcore/d3-chart/src/style/bubble.shadow.css @@ -31,7 +31,7 @@ SBubble[pattern][color] { } SBubble[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SCenter { @@ -46,7 +46,7 @@ SCenter[color] { } SCenter[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SLabel { @@ -66,5 +66,5 @@ SLabel[color] { } SLabel[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } diff --git a/semcore/d3-chart/src/style/donut.shadow.css b/semcore/d3-chart/src/style/donut.shadow.css index be61533dc0..571eabfc78 100644 --- a/semcore/d3-chart/src/style/donut.shadow.css +++ b/semcore/d3-chart/src/style/donut.shadow.css @@ -15,7 +15,7 @@ SPie[pattern][color] { } SPie[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SEmptyData { diff --git a/semcore/d3-chart/src/style/dot.shadow.css b/semcore/d3-chart/src/style/dot.shadow.css index b2799cc505..f53bb0befb 100644 --- a/semcore/d3-chart/src/style/dot.shadow.css +++ b/semcore/d3-chart/src/style/dot.shadow.css @@ -49,7 +49,7 @@ SDotBorder { } SDot[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SDotBackdrop { diff --git a/semcore/d3-chart/src/style/line.shadow.css b/semcore/d3-chart/src/style/line.shadow.css index 12269e56ab..57a96a73de 100644 --- a/semcore/d3-chart/src/style/line.shadow.css +++ b/semcore/d3-chart/src/style/line.shadow.css @@ -16,7 +16,7 @@ SLine[hide] { } SLine[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SNull { diff --git a/semcore/d3-chart/src/style/radar.shadow.css b/semcore/d3-chart/src/style/radar.shadow.css index fee353df8c..5e45a369b2 100644 --- a/semcore/d3-chart/src/style/radar.shadow.css +++ b/semcore/d3-chart/src/style/radar.shadow.css @@ -53,7 +53,7 @@ SPolygonDot { } SPolygonDot[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SPolygonDot[color] { @@ -67,7 +67,7 @@ SPolygonLine { } SPolygonLine[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SPolygonLine[color] { diff --git a/semcore/d3-chart/src/style/radial-tree.shadow.css b/semcore/d3-chart/src/style/radial-tree.shadow.css index 52e48b78d6..b558ef4571 100644 --- a/semcore/d3-chart/src/style/radial-tree.shadow.css +++ b/semcore/d3-chart/src/style/radial-tree.shadow.css @@ -12,7 +12,7 @@ SLine[stroke] { } SLine[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SCap { @@ -25,11 +25,11 @@ SCap[pattern] { } SCap[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SIcon[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SLabel { @@ -37,7 +37,7 @@ SLabel { } SLabel[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SRadian:hover SLabel { diff --git a/semcore/d3-chart/src/style/scatterplot.shadow.css b/semcore/d3-chart/src/style/scatterplot.shadow.css index 777d05dd51..4d4fc26ff3 100644 --- a/semcore/d3-chart/src/style/scatterplot.shadow.css +++ b/semcore/d3-chart/src/style/scatterplot.shadow.css @@ -21,7 +21,7 @@ SScatterPlot[pattern][color] { } SScatterPlot[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SValue { @@ -35,5 +35,5 @@ SValue[color] { } SValue[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } diff --git a/semcore/d3-chart/src/style/venn.shadow.css b/semcore/d3-chart/src/style/venn.shadow.css index bf1bf16338..4f1a742e05 100644 --- a/semcore/d3-chart/src/style/venn.shadow.css +++ b/semcore/d3-chart/src/style/venn.shadow.css @@ -28,7 +28,7 @@ SCircle[pattern][color] { } SCircle[transparent] { - opacity: 0.3; + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SIntersection { @@ -42,5 +42,5 @@ SIntersection { } SIntersection[transparent] { - opacity: 0.3; -} \ No newline at end of file + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); +} diff --git a/stories/components/d3-chart/docs/donut-chart.docs.stories.tsx b/stories/components/d3-chart/docs/donut-chart.docs.stories.tsx index 63b861e10c..16c0d892a5 100644 --- a/stories/components/d3-chart/docs/donut-chart.docs.stories.tsx +++ b/stories/components/d3-chart/docs/donut-chart.docs.stories.tsx @@ -2,7 +2,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import BasicUsageExample from './examples/donut-chart/basic-usage'; import DonutExample from './examples/donut-chart/donut'; -import DonutControlledHighlightExample from './examples/donut-chart/donut-controlled-highlight'; import EdgeCasesExample from './examples/donut-chart/edge-cases'; import LegendAndPatternFillExample from './examples/donut-chart/legend-and-pattern-fill'; import SemiDonutExample from './examples/donut-chart/semi-donut'; @@ -21,10 +20,6 @@ export const BasicUsage: StoryObj = { render: BasicUsageExample, }; -export const DonutControlledHighlight: StoryObj = { - render: DonutControlledHighlightExample, -}; - export const EdgeCases: StoryObj = { render: EdgeCasesExample, }; diff --git a/stories/components/d3-chart/docs/examples/donut-chart/basic-usage.tsx b/stories/components/d3-chart/docs/examples/donut-chart/basic-usage.tsx index 1738fb2880..ef99814d5e 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/basic-usage.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/basic-usage.tsx @@ -6,7 +6,7 @@ import DonutMockData from '../../../__mocks__/donut'; const Demo = () => { return (
- +
); }; diff --git a/stories/components/d3-chart/docs/examples/donut-chart/donut-controlled-highlight.tsx b/stories/components/d3-chart/docs/examples/donut-chart/donut-controlled-highlight.tsx deleted file mode 100644 index 1e6b92c04b..0000000000 --- a/stories/components/d3-chart/docs/examples/donut-chart/donut-controlled-highlight.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Flex } from '@semcore/ui/base-components'; -import Checkbox from '@semcore/ui/checkbox'; -import { Donut, Plot } from '@semcore/ui/d3-chart'; -import React from 'react'; - -import DonutMockData from '../../../__mocks__/donut'; - -const Demo = () => { - const [selected, setSelected] = React.useState(['b']); - const handleCheckboxToggle = React.useCallback( - (name: any) => () => { - setSelected((selected) => { - if (selected.includes(name)) { - return selected.filter((selectedName) => selectedName !== name); - } else { - return [...selected, name]; - } - }); - }, - [setSelected], - ); - - return ( - - - - {Object.keys(data).map((name, index) => ( - - ))} - - - - {Object.keys(data).map((name, index) => { - return ( - - - {`Option ${name.toUpperCase()}`} - - ); - })} - - - ); -}; - -const data = DonutMockData.Default; - -export default Demo; diff --git a/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx b/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx index 5c93a795c4..1e2bd51131 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/donut.tsx @@ -7,7 +7,7 @@ import DonutMockData from '../../../__mocks__/donut'; const Demo = () => { return ( - + diff --git a/stories/components/d3-chart/docs/examples/donut-chart/edge-cases.tsx b/stories/components/d3-chart/docs/examples/donut-chart/edge-cases.tsx index cb282a06f3..39c9289892 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/edge-cases.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/edge-cases.tsx @@ -5,7 +5,7 @@ import DonutMockData from '../../../__mocks__/donut'; const Demo = () => { return ( - + diff --git a/stories/components/d3-chart/docs/examples/donut-chart/legend-and-pattern-fill.tsx b/stories/components/d3-chart/docs/examples/donut-chart/legend-and-pattern-fill.tsx index af7e6a0aae..876fd6656f 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/legend-and-pattern-fill.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/legend-and-pattern-fill.tsx @@ -7,8 +7,8 @@ import DonutMockData from '../../../__mocks__/donut'; const dataHints = makeDataHintsContainer(); const Demo = () => { - const width = 250; - const height = 250; + const width = 180; + const height = 180; const [legendItems, setLegendItems] = React.useState( Object.keys(data).map((item, index) => { diff --git a/stories/components/d3-chart/docs/examples/donut-chart/semi-donut.tsx b/stories/components/d3-chart/docs/examples/donut-chart/semi-donut.tsx index a7baab3048..3e9aaeb5ce 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/semi-donut.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/semi-donut.tsx @@ -7,7 +7,7 @@ import DonutMockData from '../../../__mocks__/donut'; const Demo = () => { return ( - + diff --git a/tools/theme/src/theme.ts b/tools/theme/src/theme.ts index 16ca183a2d..0374727b82 100644 --- a/tools/theme/src/theme.ts +++ b/tools/theme/src/theme.ts @@ -700,6 +700,10 @@ export const theme: Theme = { value: colors.yellow['200'].value, description: 'Color for the transactional data intent on the chart.', }, + chart_transparent_opacity: { + value: '0.3', + description: 'Opacity for inactive parts of charts.', + }, chart_grid_bar_base_bg: { value: neutral.at(L_BG_MEDIUM), description: 'Default background color of a bar in the BarChart.', @@ -3380,6 +3384,9 @@ type SemanticColors = { }; }; chart: { + transparent: { + opacity: Value; + }; data: { success: Value; warning: Value; diff --git a/website/docs/data-display/donut-chart/donut-chart-code.md b/website/docs/data-display/donut-chart/donut-chart-code.md index 8ab4406565..ed37070c98 100644 --- a/website/docs/data-display/donut-chart/donut-chart-code.md +++ b/website/docs/data-display/donut-chart/donut-chart-code.md @@ -32,18 +32,6 @@ For core principles, concept description, API and changelog, refer to the [D3 ch ::: -## Controlled highlight - -Use `active` property to control segments highlight. - -::: sandbox - - - -::: - ## Semi-Donut To create a half-size chart, you need to specify the `halfsize` value and reduce the height of the chart by half. diff --git a/website/docs/style/design-tokens/design-tokens.json b/website/docs/style/design-tokens/design-tokens.json index 1dae9da49d..02e7ba739a 100644 --- a/website/docs/style/design-tokens/design-tokens.json +++ b/website/docs/style/design-tokens/design-tokens.json @@ -719,6 +719,14 @@ "description": "Color for the transactional data intent on the chart.", "components": [] }, + { + "name": "--intergalactic-chart-transparent-opacity", + "value": "0.3", + "description": "Opacity for inactive parts of charts.", + "components": [ + "d3-chart" + ] + }, { "name": "--intergalactic-chart-grid-bar-base-bg", "value": "oklch(0.92 0.008 233.5)", From fc27ef522c4c8aa37a8e1e601cf769cd79ea6128 Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Tue, 27 Oct 2026 15:45:18 +0100 Subject: [PATCH 11/21] [d3-chart] removed unused variable --- semcore/d3-chart/src/Donut.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/semcore/d3-chart/src/Donut.jsx b/semcore/d3-chart/src/Donut.jsx index 292fc652b1..c2c63ffb5d 100644 --- a/semcore/d3-chart/src/Donut.jsx +++ b/semcore/d3-chart/src/Donut.jsx @@ -308,8 +308,6 @@ function Pie({ } function EmptyData({ Element: SEmptyData, styles, d3Arc, color, resolveColor, emptyData }) { - const data = emptyData ?? { a: 8, b: 12, c: 36 }; - return sstyled(styles)( Date: Wed, 28 Oct 2026 13:45:46 +0100 Subject: [PATCH 12/21] [docs] update ScoreDonut examples --- playground/entries/Chart/ScoreDonut.tsx | 10 ++++++++++ .../docs/examples/score-donut-chart/basic-usage.tsx | 4 ++-- .../docs/examples/score-donut-chart/semi-donut.tsx | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/playground/entries/Chart/ScoreDonut.tsx b/playground/entries/Chart/ScoreDonut.tsx index 8340114d56..9ef5daca2c 100644 --- a/playground/entries/Chart/ScoreDonut.tsx +++ b/playground/entries/Chart/ScoreDonut.tsx @@ -76,6 +76,16 @@ const entry: PlaygroundEntry = { commonChartProps: ['showTotalInTooltip'], legendProps: ['direction'], }, + add: { + legendProps: { + shape: { + type: 'select', + value: 'Circle', + options: ['Checkbox', 'Circle', 'Pattern'], + displayName: 'Shape', + }, + }, + }, }), }, link: createGithubLink('d3-chart'), diff --git a/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx b/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx index dd792ca69b..d3818f857f 100644 --- a/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx +++ b/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx @@ -3,8 +3,8 @@ import React from 'react'; const Demo = () => { return ( -
- +
+
); }; diff --git a/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx b/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx index ebde6dc36a..cd6db04e83 100644 --- a/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx +++ b/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx @@ -3,8 +3,8 @@ import React from 'react'; const Demo = () => { return ( -
- +
+
); }; From 4aa4cc09816ca52864f813313cc7abd564992803 Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Wed, 28 Oct 2026 13:55:51 +0100 Subject: [PATCH 13/21] [d3-chart] fixed animation in scroe donut --- semcore/d3-chart/src/Donut.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/semcore/d3-chart/src/Donut.jsx b/semcore/d3-chart/src/Donut.jsx index 90b49bf092..0db200f5a4 100644 --- a/semcore/d3-chart/src/Donut.jsx +++ b/semcore/d3-chart/src/Donut.jsx @@ -311,6 +311,7 @@ class DonutRoot extends Component { } render() { + this.activeIndexPie = undefined; const { halfsize, size } = this.asProps; this.asProps.dataHintsHandler.establishDataType('values-set'); const [width, height] = size; @@ -357,10 +358,10 @@ function Pie({ React.useEffect(() => { if (!pieRef.current) return; - pieRef.current.dataset['currentRadius'] = (active ? d3ArcOut : d3Arc).outerRadius()(); // do not run animation on first render if (!isMount) { + pieRef.current.dataset['currentRadius'] = (active ? d3ArcOut : d3Arc).outerRadius()(); setIsMount(true); return; } @@ -376,7 +377,7 @@ function Pie({ React.useEffect(() => { if (!pieRef.current) return; pieRef.current.dataset['currentRadius'] = (active ? d3ArcOut : d3Arc).outerRadius()(); - }, [active, innerRadius, outerRadius, halfsize, Boolean(data)]); + }, [innerRadius, outerRadius, halfsize, Boolean(data)]); if (!data) return null; From 719addeb8af1f3fe935905d5c985ac4c8549390b Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Wed, 28 Oct 2026 14:15:29 +0100 Subject: [PATCH 14/21] [d3-chart] added default shape Circle to ScoreDonut --- semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx | 1 + .../d3-chart/docs/examples/score-donut-chart/semi-donut.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx index ac13645725..885ea07e81 100644 --- a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -102,6 +102,7 @@ class ScoreDonutChartComponent extends AbstractChart< defaultLegendProps() { return { legendType: 'Table' as const, + shape: 'Circle' as const, }; } diff --git a/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx b/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx index cd6db04e83..3a91bc809d 100644 --- a/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx +++ b/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx @@ -3,8 +3,8 @@ import React from 'react'; const Demo = () => { return ( -
- +
+
); }; From 40f41ef81e8a31321d8cf63a55c8bf8e7a91571a Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Wed, 28 Oct 2026 14:21:40 +0100 Subject: [PATCH 15/21] [d3-chart] removed empty donut for score donut --- semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx index 885ea07e81..0509f84d4c 100644 --- a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -114,7 +114,6 @@ class ScoreDonutChartComponent extends AbstractChart< return ( - {checkedLegendItems.length === 0 && } {checkedLegendItems.map((item, index) => { return ( Date: Wed, 28 Oct 2026 15:06:54 +0100 Subject: [PATCH 16/21] [d3-chart] added custom dataHintsHandlers for ScoreDonutChart --- semcore/d3-chart/src/Plot.jsx | 4 ++-- .../d3-chart/src/a11y/DataAccessibilityTable.tsx | 2 +- .../src/component/Chart/AbstractChart.tsx | 4 +++- .../src/component/Chart/ScoreDonutChart.tsx | 16 ++++++++++++++++ semcore/d3-chart/src/types/Plot.d.ts | 7 ++++++- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/semcore/d3-chart/src/Plot.jsx b/semcore/d3-chart/src/Plot.jsx index dd1e850c10..b8964c663e 100644 --- a/semcore/d3-chart/src/Plot.jsx +++ b/semcore/d3-chart/src/Plot.jsx @@ -34,8 +34,8 @@ class PlotRoot extends Component { rootRef = React.createRef(); - dataStructureHints = this.props.dataHints || makeDataHintsContainer(); - dataHintsHandler = makeDataHintsHandlers(this.dataStructureHints); + dataStructureHints = this.props.dataHints ?? makeDataHintsContainer(); + dataHintsHandler = this.props.dataHintsHandlers ?? makeDataHintsHandlers(this.dataStructureHints); handlerMouseMove = (e) => { const { scale } = this.asProps; diff --git a/semcore/d3-chart/src/a11y/DataAccessibilityTable.tsx b/semcore/d3-chart/src/a11y/DataAccessibilityTable.tsx index aa5903f154..286b6858ae 100644 --- a/semcore/d3-chart/src/a11y/DataAccessibilityTable.tsx +++ b/semcore/d3-chart/src/a11y/DataAccessibilityTable.tsx @@ -18,7 +18,7 @@ export const DataAccessibilityTable: React.FC<{ fromHints.push(...hints.fields.horizontalAxes); fromHints.push(...hints.fields.valueAxes); fromHints.push(...config.additionalFields); - fromHints.push(...Object.keys(hints.fields.values)); + fromHints.push(...Array.from(hints.fields.values)); fromHints = [...new Set(fromHints)].filter((key) => key in data[0]); diff --git a/semcore/d3-chart/src/component/Chart/AbstractChart.tsx b/semcore/d3-chart/src/component/Chart/AbstractChart.tsx index 1d0913dfbb..f91e1b328c 100644 --- a/semcore/d3-chart/src/component/Chart/AbstractChart.tsx +++ b/semcore/d3-chart/src/component/Chart/AbstractChart.tsx @@ -18,7 +18,7 @@ import type { BaseChartProps, BaseLegendProps, ListData, ObjectData, ObjectDataK import type { HoverLine, HoverRect } from '../..'; // @ts-ignore import { Plot, XAxis, YAxis } from '../..'; -import { makeDataHintsContainer } from '../../a11y/hints'; +import { makeDataHintsContainer, makeDataHintsHandlers } from '../../a11y/hints'; import style from '../../style/abstract-chart.shadow.css'; import { interpolateValue } from '../../utils'; import ChartLegend, { ChartLegendTable } from '../ChartLegend'; @@ -55,6 +55,7 @@ export abstract class AbstractChart< protected plotPadding = 10; protected dataHints = makeDataHintsContainer(); + protected dataHintsHandlers = makeDataHintsHandlers(this.dataHints); protected chartRef = React.createRef(); protected legendRef = React.createRef(); @@ -680,6 +681,7 @@ export abstract class AbstractChart< width={plotWidth} height={plotHeight} dataHints={this.dataHints} + dataHintsHandlers={this.dataHintsHandlers} a11yAltTextConfig={a11yAltTextConfig} patterns={patterns} duration={duration} diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx index 0509f84d4c..ad1e6278df 100644 --- a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -112,6 +112,9 @@ class ScoreDonutChartComponent extends AbstractChart< const checkedLegendItems = dataDefinitions.filter((item) => item.checked); + this.dataHintsHandlers.establishDataType('values-set'); + this.dataHintsHandlers.describeValueEntity('', '20%'); + return ( {checkedLegendItems.map((item, index) => { @@ -122,6 +125,12 @@ class ScoreDonutChartComponent extends AbstractChart< name={item.label} color={item.color} active={highlightedItem === index} + dataHintsHandler={{ + establishDataType: () => { + }, + describeValueEntity: () => { + }, + }} /> ); })} @@ -132,6 +141,12 @@ class ScoreDonutChartComponent extends AbstractChart< mask={`url(#${uid}-mask)`} uid={uid} patterns={false} + dataHintsHandler={{ + establishDataType: () => { + }, + describeValueEntity: () => { + }, + }} /> {innerLabel && {innerLabel}} @@ -185,6 +200,7 @@ class ScoreDonutChartComponent extends AbstractChart< width={plotWidth} height={plotHeight} dataHints={this.dataHints} + dataHintsHandlers={this.dataHintsHandlers} a11yAltTextConfig={a11yAltTextConfig} patterns={patterns} duration={duration} diff --git a/semcore/d3-chart/src/types/Plot.d.ts b/semcore/d3-chart/src/types/Plot.d.ts index 91127f4aa6..ea7ef56e59 100644 --- a/semcore/d3-chart/src/types/Plot.d.ts +++ b/semcore/d3-chart/src/types/Plot.d.ts @@ -1,7 +1,7 @@ import type { NSBox } from '@semcore/base-components'; import type { UnknownProperties, Intergalactic } from '@semcore/core'; -import type { DataStructureHints } from './a11y/hints'; +import type { DataStructureHints, makeDataHintHandler } from './a11y/hints'; import type { Context } from './context'; import type { PatternsConfig } from './Pattern'; import type { PlotEventEmitter } from './utils'; @@ -27,6 +27,11 @@ export type PlotProps = Context & * Optional container for data hints (use it if you have a legend component upper in tree) */ dataHints?: DataStructureHints; + + /** + * Optional container for data hints handlers (use it if you have a legend component upper in tree) + */ + dataHintsHandlers?: typeof makeDataHintHandler; /** Optional prop to tune up alt text generating for charts */ a11yAltTextConfig?: PlotSummarizerConfig; From b3a84de5aee7335e7caf2e860fc5626bc5d887c0 Mon Sep 17 00:00:00 2001 From: ilyabrower Date: Wed, 28 Oct 2026 15:15:12 +0100 Subject: [PATCH 17/21] Update semcore/d3-chart/src/style/bar.shadow.css Co-authored-by: slizhevskyv-semrush Signed-off-by: ilyabrower --- semcore/d3-chart/src/style/bar.shadow.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/semcore/d3-chart/src/style/bar.shadow.css b/semcore/d3-chart/src/style/bar.shadow.css index f35e9c61e4..9c33d7c5eb 100644 --- a/semcore/d3-chart/src/style/bar.shadow.css +++ b/semcore/d3-chart/src/style/bar.shadow.css @@ -20,7 +20,7 @@ SBar[hide] { } SBar[transparent] { - opacity: var(--intergalactic-chart-data-transparent-opacity, undefined); + opacity: var(--intergalactic-chart-transparent-opacity, 0.3); } SBar[onClick] { From 25660abafb642d7d4a379c5903f4a80b92120d63 Mon Sep 17 00:00:00 2001 From: Julia Mnizhek Date: Wed, 28 Oct 2026 16:05:29 +0100 Subject: [PATCH 18/21] [d3-chart] update pattern for ScoreDonut --- semcore/d3-chart/src/Pattern.tsx | 2 +- semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/semcore/d3-chart/src/Pattern.tsx b/semcore/d3-chart/src/Pattern.tsx index dcacb5bf15..4ff246cc3d 100644 --- a/semcore/d3-chart/src/Pattern.tsx +++ b/semcore/d3-chart/src/Pattern.tsx @@ -638,7 +638,7 @@ export const DefaultStrokePattern = (props: { id: string; halfsize?: boolean }) patternUnits='userSpaceOnUse' patternTransform='rotate(-117)' > - + diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx index ad1e6278df..0d19e421c9 100644 --- a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -137,7 +137,7 @@ class ScoreDonutChartComponent extends AbstractChart< Date: Wed, 28 Oct 2026 16:30:03 +0100 Subject: [PATCH 19/21] [docs] move ScoreDonut to Donut docs and examples --- .../src/component/Chart/ScoreDonutChart.tsx | 2 +- .../docs/examples/donut-chart/basic-usage.tsx | 4 +-- .../donut-controlled-highlight.tsx | 2 +- .../docs/examples/donut-chart/donut.tsx | 6 ++-- .../docs/examples/donut-chart/edge-cases.tsx | 2 +- .../donut-chart/legend-and-pattern-fill.tsx | 4 +-- .../docs/examples/donut-chart/semi-donut.tsx | 4 +-- .../score-donut-chart/basic-usage.tsx | 4 +-- .../examples/score-donut-chart/semi-donut.tsx | 2 +- website/docs/.vitepress/sidebarConfig.ts | 5 ---- .../donut-chart/donut-chart-api.md | 8 +++++ .../donut-chart/donut-chart-code.md | 26 +++++++++++++++- .../data-display/donut-chart/donut-chart.md | 15 ++++++++++ .../donut-chart/static/score-donut.png | Bin 0 -> 18874 bytes .../score-donut-chart-a11y.md | 5 ---- .../score-donut-chart-api.md | 18 ----------- .../score-donut-chart-changelog.md | 8 ----- .../score-donut-chart-code.md | 28 ------------------ .../score-donut-chart/score-donut-chart.md | 15 ---------- 19 files changed, 63 insertions(+), 95 deletions(-) create mode 100644 website/docs/data-display/donut-chart/static/score-donut.png delete mode 100644 website/docs/data-display/score-donut-chart/score-donut-chart-a11y.md delete mode 100644 website/docs/data-display/score-donut-chart/score-donut-chart-api.md delete mode 100644 website/docs/data-display/score-donut-chart/score-donut-chart-changelog.md delete mode 100644 website/docs/data-display/score-donut-chart/score-donut-chart-code.md delete mode 100644 website/docs/data-display/score-donut-chart/score-donut-chart.md diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx index 0d19e421c9..1b60be3d3e 100644 --- a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -231,7 +231,7 @@ class ScoreDonutChartComponent extends AbstractChart< /** * ScoreDonutChart * - * {@link https://developer.semrush.com/intergalactic/data-display/score-donut-chart/score-donut-chart-api/|API} | {@link https://developer.semrush.com/intergalactic/data-display/score-donut-chart/score-donut-chart-code/|Examples} + * {@link https://developer.semrush.com/intergalactic/data-display/donut-chart/donut-chart-api/#chartscoredonut|API} | {@link https://developer.semrush.com/intergalactic/data-display/donut-chart/donut-chart-code/#score-donut|Examples} */ export const ScoreDonutChart = createComponent< ScoreDonutChartType, diff --git a/stories/components/d3-chart/docs/examples/donut-chart/basic-usage.tsx b/stories/components/d3-chart/docs/examples/donut-chart/basic-usage.tsx index 1738fb2880..4cde60477c 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/basic-usage.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/basic-usage.tsx @@ -5,8 +5,8 @@ import DonutMockData from '../../../__mocks__/donut'; const Demo = () => { return ( -
- +
+
); }; diff --git a/stories/components/d3-chart/docs/examples/donut-chart/donut-controlled-highlight.tsx b/stories/components/d3-chart/docs/examples/donut-chart/donut-controlled-highlight.tsx index 8396c3905d..21586732fc 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/donut-controlled-highlight.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/donut-controlled-highlight.tsx @@ -22,7 +22,7 @@ const Demo = () => { return ( - + {Object.keys(data).map((name, index) => ( { return ( -
+
diff --git a/stories/components/d3-chart/docs/examples/donut-chart/edge-cases.tsx b/stories/components/d3-chart/docs/examples/donut-chart/edge-cases.tsx index cb282a06f3..39c9289892 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/edge-cases.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/edge-cases.tsx @@ -5,7 +5,7 @@ import DonutMockData from '../../../__mocks__/donut'; const Demo = () => { return ( - + diff --git a/stories/components/d3-chart/docs/examples/donut-chart/legend-and-pattern-fill.tsx b/stories/components/d3-chart/docs/examples/donut-chart/legend-and-pattern-fill.tsx index af7e6a0aae..876fd6656f 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/legend-and-pattern-fill.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/legend-and-pattern-fill.tsx @@ -7,8 +7,8 @@ import DonutMockData from '../../../__mocks__/donut'; const dataHints = makeDataHintsContainer(); const Demo = () => { - const width = 250; - const height = 250; + const width = 180; + const height = 180; const [legendItems, setLegendItems] = React.useState( Object.keys(data).map((item, index) => { diff --git a/stories/components/d3-chart/docs/examples/donut-chart/semi-donut.tsx b/stories/components/d3-chart/docs/examples/donut-chart/semi-donut.tsx index a7baab3048..e1c3e84c0b 100644 --- a/stories/components/d3-chart/docs/examples/donut-chart/semi-donut.tsx +++ b/stories/components/d3-chart/docs/examples/donut-chart/semi-donut.tsx @@ -7,13 +7,13 @@ import DonutMockData from '../../../__mocks__/donut'; const Demo = () => { return ( - + - + 71,240 diff --git a/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx b/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx index d3818f857f..af865f0fb0 100644 --- a/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx +++ b/stories/components/d3-chart/docs/examples/score-donut-chart/basic-usage.tsx @@ -3,8 +3,8 @@ import React from 'react'; const Demo = () => { return ( -
- +
+
); }; diff --git a/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx b/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx index 3a91bc809d..af8bfef98a 100644 --- a/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx +++ b/stories/components/d3-chart/docs/examples/score-donut-chart/semi-donut.tsx @@ -3,7 +3,7 @@ import React from 'react'; const Demo = () => { return ( -
+
); diff --git a/website/docs/.vitepress/sidebarConfig.ts b/website/docs/.vitepress/sidebarConfig.ts index 016eba17d9..758ac526c2 100644 --- a/website/docs/.vitepress/sidebarConfig.ts +++ b/website/docs/.vitepress/sidebarConfig.ts @@ -609,11 +609,6 @@ export const sideBarConfig: SidebarConfig = [ activeMatch: '/data-display/donut-chart/', text: 'Donut chart', }, - { - link: '/data-display/score-donut-chart/score-donut-chart', - activeMatch: '/data-display/score-donut-chart/', - text: 'Score donut chart', - }, { link: '/data-display/histogram-chart/histogram-chart', activeMatch: '/data-display/histogram-chart/', diff --git a/website/docs/data-display/donut-chart/donut-chart-api.md b/website/docs/data-display/donut-chart/donut-chart-api.md index ecc4c19df6..613ba02c8a 100644 --- a/website/docs/data-display/donut-chart/donut-chart-api.md +++ b/website/docs/data-display/donut-chart/donut-chart-api.md @@ -32,4 +32,12 @@ import { Donut } from '@semcore/ui/d3-chart'; +## Chart.ScoreDonut + +```js +import { Chart } from '@semcore/ui/d3-chart'; +``` + + + diff --git a/website/docs/data-display/donut-chart/donut-chart-code.md b/website/docs/data-display/donut-chart/donut-chart-code.md index 1f6a2866de..6ccde35ae4 100644 --- a/website/docs/data-display/donut-chart/donut-chart-code.md +++ b/website/docs/data-display/donut-chart/donut-chart-code.md @@ -42,7 +42,7 @@ Use `active` property to control segments highlight. ::: -## Semi-Donut +## Semi-donut To create a half-size chart, you need to specify the `halfsize` value and reduce the height of the chart by half. @@ -88,3 +88,27 @@ Note that for ChartLegend `patterns` property works only with default `shape={'C ::: + +## Score donut + +Shows one score on a 0–100 scale. The unfilled part of the ring is the rest of the scale. + +::: sandbox + + + +::: + +## Score semi-donut + +Set `halfsize` and use half the plot height when the chart should read as a gauge. + +::: sandbox + + + +::: diff --git a/website/docs/data-display/donut-chart/donut-chart.md b/website/docs/data-display/donut-chart/donut-chart.md index c0786cc790..c43156c416 100644 --- a/website/docs/data-display/donut-chart/donut-chart.md +++ b/website/docs/data-display/donut-chart/donut-chart.md @@ -133,6 +133,21 @@ Display name and value in tooltips. Disable tooltips for very small charts. ## Edge cases +### One data set + +If you need to show one data set, use `ScoreDonut`. This chart shows a single score on a 0–100 scale as a donut or a semi-donut. The filled part is the score. The rest of the ring is the remaining scale, not a second category. + +![](static/score-donut.png) + +Use it when one number should be read as a score, completion, or share of a maximum: + +- Use the full donut when the score is the main metric in the widget and the value can sit in the center. +- Use the half-size chart when the widget is narrow or the chart sits next to a legend. + +Do not use it to compare several categories. For parts of a whole, use the donut chart described above. For a tiny inline score, use the [Mini chart](/data-display/mini-chart/mini-chart) score donut. + +See [ScoreDonut examples](/data-display/donut-chart/donut-chart-code#score-donut). + ### Null values Show zeros in the legend but not on the chart. Show 0 as the values in the legend, use `--text-secondary` color. diff --git a/website/docs/data-display/donut-chart/static/score-donut.png b/website/docs/data-display/donut-chart/static/score-donut.png new file mode 100644 index 0000000000000000000000000000000000000000..72e475573f2bc39d9b5b32e2720ff76bad28cadf GIT binary patch literal 18874 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 - - diff --git a/website/docs/data-display/score-donut-chart/score-donut-chart-changelog.md b/website/docs/data-display/score-donut-chart/score-donut-chart-changelog.md deleted file mode 100644 index 4eb699e21d..0000000000 --- a/website/docs/data-display/score-donut-chart/score-donut-chart-changelog.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Score donut chart -tabs: Design('score-donut-chart'), A11y('score-donut-chart-a11y'), API('score-donut-chart-api'), Examples('score-donut-chart-code'), Changelog('score-donut-chart-changelog') ---- - -Common changelog of `d3-chart` package. - -::: changelog d3-chart ::: diff --git a/website/docs/data-display/score-donut-chart/score-donut-chart-code.md b/website/docs/data-display/score-donut-chart/score-donut-chart-code.md deleted file mode 100644 index 2fe21f9abc..0000000000 --- a/website/docs/data-display/score-donut-chart/score-donut-chart-code.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Score donut chart -tabs: Design('score-donut-chart'), A11y('score-donut-chart-a11y'), API('score-donut-chart-api'), Examples('score-donut-chart-code'), Changelog('score-donut-chart-changelog') ---- - -::: tip -For core principles, concept description, API and changelog, refer to the [D3 chart](/data-display/d3-chart/d3-chart). -::: - -## Basic usage - -::: sandbox - - - -::: - -## Half-size score donut - -::: sandbox - - - -::: diff --git a/website/docs/data-display/score-donut-chart/score-donut-chart.md b/website/docs/data-display/score-donut-chart/score-donut-chart.md deleted file mode 100644 index 90335bcf01..0000000000 --- a/website/docs/data-display/score-donut-chart/score-donut-chart.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Score donut chart -fileSource: d3-chart -tabs: Design('score-donut-chart'), A11y('score-donut-chart-a11y'), API('score-donut-chart-api'), Examples('score-donut-chart-code'), Changelog('score-donut-chart-changelog') ---- - - - -::: info -Basic data visualization rules are described in the [D3 chart](/data-display/d3-chart/d3-chart). -::: - -## Description - -**Score donut chart** shows score value in a donut or semi-donut view. From 734c6f97ba6f699869c8d073e24c81dd8d88b55d Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Thu, 29 Oct 2026 13:11:58 +0100 Subject: [PATCH 20/21] [d3-chart] fixed value for score donut in a11y modal summary --- semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx index 1b60be3d3e..d970e36478 100644 --- a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -113,7 +113,7 @@ class ScoreDonutChartComponent extends AbstractChart< const checkedLegendItems = dataDefinitions.filter((item) => item.checked); this.dataHintsHandlers.establishDataType('values-set'); - this.dataHintsHandlers.describeValueEntity('', '20%'); + this.dataHintsHandlers.describeValueEntity('', `${this.data.value}%`); return ( From ace6603e6cc958ba827acdd1d5a2ff756852ab30 Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Thu, 29 Oct 2026 13:15:55 +0100 Subject: [PATCH 21/21] [d3-chart] donut pies in score donut are always not transparent --- semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx index d970e36478..6bedc52d5a 100644 --- a/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx +++ b/semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx @@ -124,7 +124,7 @@ class ScoreDonutChartComponent extends AbstractChart< dataKey={item.id} name={item.label} color={item.color} - active={highlightedItem === index} + transparent={false} dataHintsHandler={{ establishDataType: () => { }, @@ -141,6 +141,7 @@ class ScoreDonutChartComponent extends AbstractChart< mask={`url(#${uid}-mask)`} uid={uid} patterns={false} + transparent={false} dataHintsHandler={{ establishDataType: () => { },