Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions playground/entries/Chart/ScoreDonut.tsx
Original file line number Diff line number Diff line change
@@ -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<CommonChartProps, 'showTotalInTooltip'>;
legendProps: Omit<LegendChartProps, 'direction'>;
donutProps: {
halfsize: boolean;
innerLabel: string;
};
};
export type DonutChartJSXProps = JSXProps<DonutChartProps>;

const data = {
value: 33,
};

function getJSX(props: DonutChartJSXProps) {
const legendProps = {
...props.legendProps,
};
return (
<Chart.ScoreDonut
plotWidth={300}
plotHeight={200}
data={data}
aria-label='Donut chart'
{...props.donutProps}
innerLabel={(
props.donutProps.halfsize
? (
<Text tag='tspan' dy='-1em'>
{props.donutProps.innerLabel}
</Text>
)
: props.donutProps.innerLabel
)}
{...props.commonChartProps}
{...(props.legendProps && {
legendProps,
showLegend: props.commonChartProps.showLegend as true,
})}
{...(props.legendProps?.patterns && { patterns: props.legendProps.patterns })}
/>
);
}

const entry: PlaygroundEntry<DonutChartJSXProps> = {
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;
2 changes: 2 additions & 0 deletions playground/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 9 additions & 16 deletions semcore/d3-chart/src/Pattern.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -632,26 +632,19 @@ export const DefaultStrokePattern = (props: { id: string }) => {
return (
<defs>
<pattern
id={props.id}
width='10'
height='8'
id={`${props.id}-pattern`}
width='6'
height='6'
patternUnits='userSpaceOnUse'
patternTransform='rotate(-117)'
>
<rect
width='10'
height='8'
fill='transparent'
/>
<line
x1='0'
y1='0'
x2='0'
y2='8'
stroke='#fff'
strokeWidth='8'
/>
<rect width='6' height='6' fill='#fff' opacity={0.4}></rect>
<line x1='0' y1='0' x2='0' y2='8' stroke='#fff' strokeWidth='6'></line>
</pattern>

<mask id={props.id}>
<rect width='100%' height='100%' x='-50%' y='-50%' fill={`url(#${props.id}-pattern)`} />
</mask>
</defs>
);
};
Expand Down
216 changes: 216 additions & 0 deletions semcore/d3-chart/src/component/Chart/ScoreDonutChart.tsx
Original file line number Diff line number Diff line change
@@ -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<LegendItem & ({ columns: React.ReactNode[] } | { rows: React.ReactNode[] })> {
const { data, legendProps } = this.props;

const legendData = legendProps?.legendMap?.value;

// @ts-expect-error will fill columns or rows later
const dataDefinition: LegendItem & ({ columns: Array<React.ReactNode> } | { rows: Array<React.ReactNode> }) = {
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 = [
<Text key='percent' use='secondary'>
{percent !== undefined ? `${percent}%` : ''}
</Text>,
<Text key='value' use='primary'>
{value}
</Text>,
];
}

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 (
<Donut onClick={onClickPie} halfsize={halfsize} innerRadius={innerRadius}>
{checkedLegendItems.length === 0 && <Donut.EmptyData />}
{checkedLegendItems.map((item, index) => {
return (
<Donut.Pie
key={item.id}
dataKey={item.id}
name={item.label}
color={item.color}
active={highlightedItem === index}
/>
);
})}
<DefaultStrokePattern id={`${uid}-mask`} />
<Donut.Pie
dataKey='other'
color='chart-palette-order-other-data'
mask={`url(#${uid}-mask)`}
uid={uid}
/>
{innerLabel && <Donut.Label>{innerLabel}</Donut.Label>}
</Donut>
);
}

renderTooltip() {
const { data } = this.asProps;
const { dataDefinitions } = this.state;

return (
<Donut.Tooltip>
{({ dataKey }: any) => {
const title = dataDefinitions.find((item) => item.id === dataKey);

return {
children: (
<>
<Donut.Tooltip.Title>{this.tooltipTitleFormatter(title?.label || dataKey)}</Donut.Tooltip.Title>
<Flex justifyContent='space-between'>
<Text bold>{this.tooltipValueFormatter(data.value)}</Text>
</Flex>
</>
),
};
}}
</Donut.Tooltip>
);
}

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)(
<SChart render={Flex} gap={5} __excludeProps={['data', 'eventEmitter']} role='group' ref={this.chartRef}>
{this.renderLegend()}
<Plot
data={this.data}
scale={[this.xScale, this.yScale]}
width={plotWidth}
height={plotHeight}
dataHints={this.dataHints}
a11yAltTextConfig={a11yAltTextConfig}
patterns={patterns}
duration={duration}
eventEmitter={eventEmitter}
locale={locale}
{...extractedAriaProps}
>
{this.renderAxis()}
{!showTooltip ? null : this.renderTooltip()}
{this.renderChart()}
</Plot>
</SChart>,
);
}

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);
Loading
Loading