Skip to content

Commit

Permalink
feat(D3 plugin): bar-x dataLabels option (#260)
Browse files Browse the repository at this point in the history
* feat(D3 plugin): bar-x dataLabels option

* fix BarXSeriesShapes props

* fix types

* fix: remove extra g element from bar-x selections

---------

Co-authored-by: Evgeny Alaev <[email protected]>
  • Loading branch information
kuzmadom and korvin89 authored Aug 31, 2023
1 parent 2b9f47d commit 4444f72
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 71 deletions.
7 changes: 5 additions & 2 deletions src/plugins/d3/__stories__/bar-x/category.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,19 @@ const Template: Story = () => {
data: [
{
category: 'A',
x: 10,
label: 10,
y: 100,
},
{
category: 'B',
x: 12,
label: 12,
y: 80,
},
],
name: 'AB',
dataLabels: {
enabled: true,
},
},
{
type: 'bar-x',
Expand Down
15 changes: 11 additions & 4 deletions src/plugins/d3/__stories__/bar-x/stacked.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ const Template: Story = () => {
},
],
name: 'Sales',
dataLabels: {
enabled: true,
inside: true,
style: {
fontWeight: 'normal',
fontColor: '#fff',
},
},
},
{
type: 'bar-x',
Expand All @@ -62,12 +70,11 @@ const Template: Story = () => {
category: 'B',
y: 25,
},
{
category: 'C',
y: 0,
},
],
name: 'Discount',
dataLabels: {
enabled: true,
},
},
],
},
Expand Down
14 changes: 14 additions & 0 deletions src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import get from 'lodash/get';
import {DEFAULT_PALETTE} from '../../constants';
import {DEFAULT_LEGEND_SYMBOL_SIZE} from './constants';
import {getRandomCKId} from '../../../../../utils';
import {BaseTextStyle} from '../../../../../types/widget-data';

const DEFAULT_DATALABELS_STYLE: BaseTextStyle = {
fontSize: '11px',
fontWeight: 'bold',
};

function prepareLegendSymbol(series: ChartKitWidgetSeries): PreparedLegendSymbol {
switch (series.type) {
Expand Down Expand Up @@ -84,6 +90,14 @@ function prepareBarXSeries(args: PrepareBarXSeriesArgs): PreparedSeries[] {
data: singleSeries.data,
stacking: singleSeries.stacking,
stackId: singleSeries.stacking === 'normal' ? commonStackId : getRandomCKId(),
dataLabels: {
enabled: singleSeries.dataLabels?.enabled || false,
inside:
typeof singleSeries.dataLabels?.inside === 'boolean'
? singleSeries.dataLabels?.inside
: false,
style: Object.assign({}, DEFAULT_DATALABELS_STYLE, singleSeries.dataLabels?.style),
},
};
}, []);
}
Expand Down
6 changes: 6 additions & 0 deletions src/plugins/d3/renderer/hooks/useSeries/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BarXSeries,
BarXSeriesData,
BaseTextStyle,
PieSeries,
PieSeriesData,
RectLegendSymbolOptions,
Expand Down Expand Up @@ -33,6 +34,11 @@ export type PreparedBarXSeries = {
type: BarXSeries['type'];
data: BarXSeriesData[];
stackId: string;
dataLabels: {
enabled: boolean;
inside: boolean;
style: BaseTextStyle;
};
} & BasePreparedSeries;

export type PreparedPieSeries = BasePreparedSeries &
Expand Down
162 changes: 111 additions & 51 deletions src/plugins/d3/renderer/hooks/useShapes/bar-x.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import {ChartScale} from '../useAxisScales';
import {OnSeriesMouseLeave, OnSeriesMouseMove} from '../useTooltip/types';
import {BarXSeriesData} from '../../../../../types/widget-data';
import {block} from '../../../../../utils/cn';
import {group, pointer, ScaleBand, ScaleLinear, ScaleTime} from 'd3';
import {group, pointer, ScaleBand, ScaleLinear, ScaleTime, select} from 'd3';
import {PreparedBarXSeries} from '../useSeries/types';

const DEFAULT_BAR_RECT_WIDTH = 50;
const DEFAULT_LINEAR_BAR_RECT_WIDTH = 20;
const MIN_RECT_GAP = 1;
const DEFAULT_LABEL_PADDING = 7;

const b = block('d3-bar');
const b = block('d3-bar-x');

type Args = {
top: number;
Expand Down Expand Up @@ -82,7 +83,7 @@ function minDiff(arr: number[]) {
return result;
}

export function prepareBarXSeries(args: Args) {
export function BarXSeriesShapes(args: Args) {
const {
top,
left,
Expand All @@ -96,60 +97,119 @@ export function prepareBarXSeries(args: Args) {
svgContainer,
} = args;

const stackedSeriesMap = group(series, (item) => item.stackId);
const ref = React.useRef<SVGGElement>(null);

const seriesData = series.map(({data}) => data).flat(2);
const minPointDistance = minDiff(seriesData.map((item) => Number(item.x)));

const result: React.ReactElement[] = [];
React.useEffect(() => {
if (!ref.current) {
return;
}

Array.from(stackedSeriesMap).forEach(([stackId, stackedSeries]) => {
const stackHeights: Record<string, number> = {};
stackedSeries.forEach((item, seriesIndex) => {
item.data.forEach((point, i) => {
const rectProps = getRectProperties({
point,
xAxis,
xScale,
yAxis,
yScale,
minPointDistance,
const svgElement = select(ref.current);
svgElement.selectAll('*').remove();

const xValues =
xAxis.type === 'category'
? []
: series.reduce<number[]>((acc, {data}) => {
data.forEach((dataItem) => acc.push(Number(dataItem.x)));
return acc;
}, []);
const minPointDistance = minDiff(xValues);

const stackedSeriesMap = group(series, (item) => item.stackId);
Array.from(stackedSeriesMap).forEach(([, stackedSeries]) => {
const stackHeights: Record<string, number> = {};
stackedSeries.forEach((item) => {
const shapes = item.data.map((dataItem) => {
const rectProps = getRectProperties({
point: dataItem,
xAxis,
xScale,
yAxis,
yScale,
minPointDistance,
});

if (!stackHeights[rectProps.x]) {
stackHeights[rectProps.x] = 0;
}

const rectY = rectProps.y - stackHeights[rectProps.x];
stackHeights[rectProps.x] += rectProps.height + 1;

return {
...rectProps,
y: rectY,
data: dataItem,
};
});

if (!stackHeights[rectProps.x]) {
stackHeights[rectProps.x] = 0;
}

const rectY = rectProps.y - stackHeights[rectProps.x];
stackHeights[rectProps.x] += rectProps.height + 1;

if (!rectProps.height) {
return;
svgElement
.selectAll('allRects')
.data(shapes)
.join('rect')
.attr('class', b('segment'))
.attr('x', (d) => d.x)
.attr('y', (d) => d.y)
.attr('height', (d) => d.height)
.attr('width', (d) => d.width)
.attr('fill', item.color)
.on('mousemove', (e, point) => {
const [x, y] = pointer(e, svgContainer);
onSeriesMouseMove?.({
hovered: {
data: point.data,
series: item,
},
pointerPosition: [x - left, y - top],
});
})
.on('mouseleave', () => {
if (onSeriesMouseLeave) {
onSeriesMouseLeave();
}
});

if (item.dataLabels.enabled) {
const selection = svgElement
.selectAll('allLabels')
.data(shapes)
.join('text')
.text((d) => String(d.data.label || d.data.y))
.attr('class', b('label'))
.attr('x', (d) => d.x + d.width / 2)
.attr('y', (d) => {
if (item.dataLabels.inside) {
return d.y + d.height / 2;
}

return d.y - DEFAULT_LABEL_PADDING;
})
.attr('text-anchor', 'middle')
.style('font-size', item.dataLabels.style.fontSize);

if (item.dataLabels.style.fontWeight) {
selection.style('font-weight', item.dataLabels.style.fontWeight);
}

if (item.dataLabels.style.fontColor) {
selection.style('fill', item.dataLabels.style.fontColor);
}
}

result.push(
<rect
key={`${i}-${seriesIndex}-${stackId}`}
className={b('rect')}
fill={item.color}
{...rectProps}
y={rectY}
onMouseMove={function (e) {
const [x, y] = pointer(e, svgContainer);
onSeriesMouseMove?.({
hovered: {
data: point,
series: item,
},
pointerPosition: [x - left, y - top],
});
}}
onMouseLeave={onSeriesMouseLeave}
/>,
);
});
});
});
}, [
onSeriesMouseMove,
onSeriesMouseLeave,
svgContainer,
xAxis,
xScale,
yAxis,
yScale,
series,
left,
top,
]);

return result;
return <g ref={ref} className={b()} />;
}
21 changes: 8 additions & 13 deletions src/plugins/d3/renderer/hooks/useShapes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {ChartOptions} from '../useChartOptions/types';
import type {ChartScale} from '../useAxisScales';
import type {PreparedBarXSeries, PreparedPieSeries, PreparedSeries} from '../';
import type {OnSeriesMouseMove, OnSeriesMouseLeave} from '../useTooltip/types';
import {prepareBarXSeries} from './bar-x';
import {BarXSeriesShapes} from './bar-x';
import {prepareScatterSeries} from './scatter';
import {PieSeriesComponent} from './pie';

Expand Down Expand Up @@ -55,18 +55,13 @@ export const useShapes = (args: Args) => {
case 'bar-x': {
if (xScale && yScale) {
acc.push(
...prepareBarXSeries({
top,
left,
series: chartSeries as PreparedBarXSeries[],
xAxis,
xScale,
yAxis,
yScale,
onSeriesMouseMove,
onSeriesMouseLeave,
svgContainer,
}),
<BarXSeriesShapes
{...args}
key="bar-x"
series={chartSeries as PreparedBarXSeries[]}
xScale={xScale}
yScale={yScale}
/>,
);
}
break;
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 @@ -24,3 +24,9 @@
font-weight: bold;
}
}

.chartkit-d3-bar-x {
&__label {
fill: var(--g-color-text-complementary);
}
}
9 changes: 8 additions & 1 deletion src/types/widget-data/bar-x.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export type BarXSeriesData<T = any> = BaseSeriesData<T> & {
y?: number;
/** Corresponding value of axis category */
category?: string;

/** Data label value of the bar-x column. If not specified, the y value is used. */
label?: string | number;
};

export type BarXSeries<T = any> = BaseSeries & {
Expand Down Expand Up @@ -42,7 +45,11 @@ export type BarXSeries<T = any> = BaseSeries & {
grouping?: boolean;

dataLabels?: ChartKitWidgetSeriesOptions['dataLabels'] & {
/** Whether to align the data label inside the box or to the actual value point */
/**
* Whether to align the data label inside or outside the box
*
* @default false
* */
inside?: boolean;
};

Expand Down
4 changes: 4 additions & 0 deletions src/types/widget-data/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type BaseSeries = {
* @default true
*/
enabled?: boolean;

style?: Partial<BaseTextStyle>;
};
};

Expand All @@ -26,4 +28,6 @@ export type BaseSeriesData<T = any> = {

export type BaseTextStyle = {
fontSize: string;
fontWeight?: string;
fontColor?: string;
};

0 comments on commit 4444f72

Please sign in to comment.