From 1c1577afe0be88ee44de21ca8c252fb20316b19d Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Wed, 21 Oct 2026 12:33:07 +0200 Subject: [PATCH 1/2] [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 52e0d26c0fe1035a1d9f11ad6eeeb82b5c32c3bb Mon Sep 17 00:00:00 2001 From: "ilia.brauer" Date: Wed, 21 Oct 2026 20:29:11 +0200 Subject: [PATCH 2/2] [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 + .../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 ++ 16 files changed, 481 insertions(+), 16 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/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.