Skip to content

Commit

Permalink
feat(D3 plugin): add basic pie chart (#252)
Browse files Browse the repository at this point in the history
* feat(d3): add basic pie chart

* fix: fix styled pies story

* fix: fix labels fill css variable

* fix: rebase fixes

* fix: fix pies stackId generating
  • Loading branch information
korvin89 authored Aug 28, 2023
1 parent 17a0679 commit 71d3139
Show file tree
Hide file tree
Showing 16 changed files with 204 additions and 75 deletions.
15 changes: 12 additions & 3 deletions src/plugins/d3/__stories__/pie/BasicPie.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,34 @@ const Template: Story = () => {
data: [
{
type: 'pie',
dataLabels: {enabled: true},
data: [
{
name: 'One',
value: 50,
},
{
name: 'Two',
value: 20,
value: 130,
},
{
name: 'Three',
value: 90,
value: 50,
},
{
name: 'Four',
value: 95,
},
{
name: 'Five',
value: 100,
},
],
},
],
},
title: {text: 'Basic pie'},
legend: {enabled: false},
legend: {enabled: true},
tooltip: {enabled: false},
};

Expand Down
2 changes: 2 additions & 0 deletions src/plugins/d3/__stories__/pie/Styled.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const Template: Story = () => {
borderWidth: 3,
center: ['25%', null],
radius: '75%',
dataLabels: {enabled: false},
data: [
{
name: 'One 1',
Expand All @@ -42,6 +43,7 @@ const Template: Story = () => {
center: ['75%', null],
innerRadius: '50%',
radius: '75%',
dataLabels: {enabled: false},
data: [
{
name: 'One 2',
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/d3/renderer/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ export const DEFAULT_PALETTE = [
'#FFB46C',
'#DCA3D7',
];

export const DEFAULT_AXIS_LABEL_FONT_SIZE = '11px';
export const DEFAULT_AXIS_LABEL_PADDING = 10;
export const DEFAULT_AXIS_TITLE_FONT_SIZE = '14px';
12 changes: 8 additions & 4 deletions src/plugins/d3/renderer/hooks/useChartOptions/chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import get from 'lodash/get';

import type {ChartKitWidgetData, ChartKitWidgetSeries} from '../../../../../types/widget-data';

import {formatAxisTickLabel, getDomainDataYBySeries, isAxisRelatedSeries} from '../../utils';
import {
formatAxisTickLabel,
getDomainDataYBySeries,
getHorisontalSvgTextHeight,
isAxisRelatedSeries,
} from '../../utils';

import type {PreparedAxis, PreparedChart} from './types';
import {getHorisontalSvgTextDimensions} from './utils';

const AXIS_WIDTH = 1;

Expand Down Expand Up @@ -73,14 +77,14 @@ export const getPreparedChart = (args: {
if (hasAxisRelatedSeries) {
marginBottom +=
preparedXAxis.labels.padding +
getHorisontalSvgTextDimensions({text: 'Tmp', style: preparedXAxis.labels.style});
getHorisontalSvgTextHeight({text: 'Tmp', style: preparedXAxis.labels.style});
marginLeft +=
AXIS_WIDTH +
preparedY1Axis.labels.padding +
getAxisLabelMaxWidth({axis: preparedY1Axis, series: series.data}) +
(preparedY1Axis.title.height || 0);
marginTop +=
getHorisontalSvgTextDimensions({text: 'Tmp', style: preparedY1Axis.labels.style}) / 2;
getHorisontalSvgTextHeight({text: 'Tmp', style: preparedY1Axis.labels.style}) / 2;
marginRight += getAxisLabelMaxWidth({axis: preparedXAxis, series: series.data}) / 2;
}

Expand Down
3 changes: 0 additions & 3 deletions src/plugins/d3/renderer/hooks/useChartOptions/constants.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/plugins/d3/renderer/hooks/useChartOptions/title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import get from 'lodash/get';

import type {BaseTextStyle, ChartKitWidgetData} from '../../../../../types/widget-data';

import {getHorisontalSvgTextHeight} from '../../utils';
import type {PreparedTitle} from './types';
import {getHorisontalSvgTextDimensions} from './utils';

const DEFAULT_TITLE_FONT_SIZE = '15px';
const TITLE_PADDINGS = 8 * 2;
Expand All @@ -18,7 +18,7 @@ export const getPreparedTitle = ({
fontSize: get(title, 'style.fontSize', DEFAULT_TITLE_FONT_SIZE),
};
const titleHeight = titleText
? getHorisontalSvgTextDimensions({text: titleText, style: titleStyle}) + TITLE_PADDINGS
? getHorisontalSvgTextHeight({text: titleText, style: titleStyle}) + TITLE_PADDINGS
: 0;
const preparedTitle: PreparedTitle | undefined = titleText
? {text: titleText, style: titleStyle, height: titleHeight}
Expand Down
27 changes: 0 additions & 27 deletions src/plugins/d3/renderer/hooks/useChartOptions/utils.ts

This file was deleted.

8 changes: 4 additions & 4 deletions src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import get from 'lodash/get';

import type {ChartKitWidgetData} from '../../../../../types/widget-data';

import type {PreparedAxis} from './types';
import {
DEFAULT_AXIS_LABEL_FONT_SIZE,
DEFAULT_AXIS_LABEL_PADDING,
DEFAULT_AXIS_TITLE_FONT_SIZE,
} from './constants';
} from '../../constants';
import type {PreparedAxis} from './types';
import {BaseTextStyle} from '../../../../../types/widget-data';
import {getHorisontalSvgTextDimensions} from './utils';
import {getHorisontalSvgTextHeight} from '../../utils';

export const getPreparedXAxis = ({xAxis}: {xAxis: ChartKitWidgetData['xAxis']}): PreparedAxis => {
const titleText = get(xAxis, 'title.text', '');
Expand All @@ -32,7 +32,7 @@ export const getPreparedXAxis = ({xAxis}: {xAxis: ChartKitWidgetData['xAxis']}):
text: titleText,
style: titleStyle,
height: titleText
? getHorisontalSvgTextDimensions({text: titleText, style: titleStyle})
? getHorisontalSvgTextHeight({text: titleText, style: titleStyle})
: 0,
},
min: get(xAxis, 'min'),
Expand Down
8 changes: 4 additions & 4 deletions src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import get from 'lodash/get';

import type {BaseTextStyle, ChartKitWidgetData} from '../../../../../types/widget-data';

import type {PreparedAxis} from './types';
import {
DEFAULT_AXIS_LABEL_FONT_SIZE,
DEFAULT_AXIS_LABEL_PADDING,
DEFAULT_AXIS_TITLE_FONT_SIZE,
} from './constants';
import {getHorisontalSvgTextDimensions} from './utils';
} from '../../constants';
import {getHorisontalSvgTextHeight} from '../../utils';
import type {PreparedAxis} from './types';

export const getPreparedYAxis = ({yAxis}: {yAxis: ChartKitWidgetData['yAxis']}): PreparedAxis[] => {
// FIXME: add support for n axises
Expand Down Expand Up @@ -36,7 +36,7 @@ export const getPreparedYAxis = ({yAxis}: {yAxis: ChartKitWidgetData['yAxis']}):
text: y1TitleText,
style: y1TitleStyle,
height: y1TitleText
? getHorisontalSvgTextDimensions({text: y1TitleText, style: y1TitleStyle})
? getHorisontalSvgTextHeight({text: y1TitleText, style: y1TitleStyle})
: 0,
},
min: get(yAxis1, 'min'),
Expand Down
11 changes: 8 additions & 3 deletions src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,16 @@ function preparePieSeries(args: PreparePieSeriesArgs) {
const {series, legend} = args;
const dataNames = series.data.map((d) => d.name);
const colorScale = scaleOrdinal(dataNames, DEFAULT_PALETTE);
const stackId = getRandomCKId();

const preparedSeries: PreparedSeries[] = series.data.map<PreparedPieSeries>((dataItem) => {
const preparedSeries: PreparedPieSeries = {
const result: PreparedPieSeries = {
type: 'pie',
data: dataItem.value,
dataLabels: {
enabled: get(series, 'dataLabels.enabled', true),
},
label: dataItem.label,
visible: typeof dataItem.visible === 'boolean' ? dataItem.visible : true,
name: dataItem.name,
color: dataItem.color || colorScale(dataItem.name),
Expand All @@ -79,10 +84,10 @@ function preparePieSeries(args: PreparePieSeriesArgs) {
borderWidth: series.borderWidth ?? 1,
radius: series.radius || '100%',
innerRadius: series.innerRadius || 0,
stackId: getRandomCKId(),
stackId,
};

return preparedSeries;
return result;
});

return preparedSeries;
Expand Down
14 changes: 2 additions & 12 deletions src/plugins/d3/renderer/hooks/useSeries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,10 @@ export type PreparedBarXSeries = {
} & BasePreparedSeries;

export type PreparedPieSeries = BasePreparedSeries &
Required<
Pick<
PieSeries,
| 'type'
| 'center'
| 'radius'
| 'innerRadius'
| 'borderColor'
| 'borderWidth'
| 'borderRadius'
>
> & {
Required<Omit<PieSeries, 'data'>> & {
data: PieSeriesData['value'];
stackId: string;
label?: PieSeriesData['label'];
};

export type PreparedSeries = PreparedScatterSeries | PreparedBarXSeries | PreparedPieSeries;
95 changes: 88 additions & 7 deletions src/plugins/d3/renderer/hooks/useShapes/pie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {PieArcDatum} from 'd3';
import type {PieSeries} from '../../../../../types/widget-data';
import {block} from '../../../../../utils/cn';

import {calculateNumericProperty} from '../../utils';
import {calculateNumericProperty, getHorisontalSvgTextHeight} from '../../utils';
import type {OnSeriesMouseLeave, OnSeriesMouseMove} from '../useTooltip/types';
import {PreparedPieSeries} from '../useSeries/types';

Expand Down Expand Up @@ -51,10 +51,24 @@ export function PieSeriesComponent(args: PreparePieSeriesArgs) {
}

const svgElement = select(ref.current);
const radiusRelatedToChart = Math.min(boundsWidth, boundsHeight) / 2;
const radius =
calculateNumericProperty({value: series[0].radius, base: radiusRelatedToChart}) ??
const isLabelsEnabled = series[0]?.dataLabels?.enabled;
let radiusRelatedToChart = Math.min(boundsWidth, boundsHeight) / 2;

if (isLabelsEnabled) {
// To have enough space for labels
radiusRelatedToChart *= 0.9;
}

let radius =
calculateNumericProperty({value: series[0]?.radius, base: radiusRelatedToChart}) ??
radiusRelatedToChart;
const labelsArcRadius = series[0]?.radius ? radius : radiusRelatedToChart;

if (isLabelsEnabled) {
// To have enough space for labels lines
radius *= 0.9;
}

const innerRadius =
calculateNumericProperty({value: series[0].innerRadius, base: radius}) ?? 0;
const pieGenerator = pie<PreparedPieSeries>().value((d) => d.data);
Expand All @@ -67,15 +81,82 @@ export function PieSeriesComponent(args: PreparePieSeriesArgs) {
svgElement.selectAll('*').remove();

svgElement
.selectAll('*')
.selectAll('allSlices')
.data(dataReady)
.enter()
.append('path')
.attr('d', arcGenerator)
.attr('class', b('segment'))
.attr('fill', (d) => d.data.color || '')
.style('stroke', (d) => d.data.borderColor)
.style('stroke-width', (d) => d.data.borderWidth);
.style('stroke', series[0]?.borderColor || '')
.style('stroke-width', series[0]?.borderWidth ?? 1);

if (series[0]?.dataLabels?.enabled) {
const labelHeight = getHorisontalSvgTextHeight({text: 'tmp'});
const outerArc = arc<PieArcDatum<PreparedPieSeries>>()
.innerRadius(labelsArcRadius)
.outerRadius(labelsArcRadius);
// Add the polylines between chart and labels
svgElement
.selectAll('allPolylines')
.data(dataReady)
.enter()
.append('polyline')
.attr('stroke', (d) => d.data.color || '')
.style('fill', 'none')
.attr('stroke-width', 1)
.attr('points', (d) => {
// Line insertion in the slice
const posA = arcGenerator.centroid(d);
// Line break: we use the other arc generator that has been built only for that
const posB = outerArc.centroid(d);
const posC = outerArc.centroid(d);
// We need the angle to see if the X position will be at the extreme right or extreme left
const midangle = d.startAngle + (d.endAngle - d.startAngle) / 2;
const result = [posA, posB, posC];

if (midangle < Math.PI) {
// polylines located to the right
const nextCx = radiusRelatedToChart * 0.95;

if (nextCx > result[1][0]) {
result[2][0] = nextCx;
} else {
result.splice(2, 1);
}
} else {
// polylines located to the left
const nextCx = radiusRelatedToChart * 0.95 * -1;

if (nextCx < result[1][0]) {
result[2][0] = nextCx;
} else {
result.splice(2, 1);
}
}

return result.join(' ');
});

// Add the polylines between chart and labels
svgElement
.selectAll('allLabels')
.data(dataReady)
.join('text')
.text((d) => d.data.label || d.value)
.attr('class', b('label'))
.attr('transform', (d) => {
const pos = outerArc.centroid(d);
const midangle = d.startAngle + (d.endAngle - d.startAngle) / 2;
pos[0] = radiusRelatedToChart * 0.99 * (midangle < Math.PI ? 1 : -1);
pos[1] += labelHeight / 4;
return `translate(${pos})`;
})
.style('text-anchor', (d) => {
const midangle = d.startAngle + (d.endAngle - d.startAngle) / 2;
return midangle < Math.PI ? 'start' : 'end';
});
}
}, [boundsWidth, boundsHeight, series, onSeriesMouseMove, onSeriesMouseLeave, svgContainer]);

return <g ref={ref} className={b()} transform={`translate(${x}, ${y})`} />;
Expand Down
6 changes: 6 additions & 0 deletions src/plugins/d3/renderer/hooks/useShapes/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@
&__segment {
stroke: var(--g-color-base-background);
}

&__label {
fill: var(--g-color-text-complementary);
font-size: 11px;
font-weight: bold;
}
}
Loading

0 comments on commit 71d3139

Please sign in to comment.