Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(D3 plugin): bar-x dataLabels option #260

Merged
merged 4 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
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,
korvin89 marked this conversation as resolved.
Show resolved Hide resolved
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
164 changes: 113 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,121 @@ 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}) => {
korvin89 marked this conversation as resolved.
Show resolved Hide resolved
data.forEach((dataItem) => acc.push(Number(dataItem.x)));
return acc;
}, []);
const minPointDistance = minDiff(xValues);

const stackedSeriesMap = group(series, (item) => item.stackId);
korvin89 marked this conversation as resolved.
Show resolved Hide resolved
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
.append('g')
korvin89 marked this conversation as resolved.
Show resolved Hide resolved
.attr('fill', item.color)
.selectAll('rect')
.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)
.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
.append('g')
korvin89 marked this conversation as resolved.
Show resolved Hide resolved
.style('font-size', item.dataLabels.style.fontSize)
.selectAll('text')
.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');

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;
};