Skip to content

Commit 0554e7c

Browse files
authored
fix: series with a single point in the data (#296)
1 parent 46c2d9f commit 0554e7c

File tree

10 files changed

+97
-67
lines changed

10 files changed

+97
-67
lines changed
3.38 KB
Loading
3.51 KB
Loading
1.63 KB
Loading

src/__tests__/area-series.visual.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ test.describe('Area series', () => {
118118
});
119119
});
120120

121+
test('Single point (with marker enabled)', async ({mount}) => {
122+
const chartData: ChartData = {
123+
series: {
124+
data: [
125+
{
126+
name: 'Series 1',
127+
type: 'area',
128+
data: [{y: 10, x: 10, marker: {states: {normal: {enabled: true}}}}],
129+
},
130+
],
131+
},
132+
yAxis: [{maxPadding: 0}],
133+
xAxis: {maxPadding: 0},
134+
};
135+
const component = await mount(<ChartTestStory data={chartData} />);
136+
await expect(component.locator('svg')).toHaveScreenshot();
137+
});
138+
121139
test('Two points with the same y value', async ({mount}) => {
122140
const chartData: ChartData = {
123141
series: {

src/__tests__/bar-x-series.visual.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,22 @@ test.describe('Bar-x series', () => {
279279
const component = await mount(<ChartTestStory data={barXSplitData} />);
280280
await expect(component.locator('svg')).toHaveScreenshot();
281281
});
282+
283+
test('Single point', async ({mount}) => {
284+
const chartData: ChartData = {
285+
series: {
286+
data: [
287+
{
288+
name: 'Series 1',
289+
type: 'bar-x',
290+
data: [{y: 10, x: 10}],
291+
},
292+
],
293+
},
294+
yAxis: [{maxPadding: 0}],
295+
xAxis: {maxPadding: 0},
296+
};
297+
const component = await mount(<ChartTestStory data={chartData} />);
298+
await expect(component.locator('svg')).toHaveScreenshot();
299+
});
282300
});

src/__tests__/line-series.visual.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ test.describe('Line series', () => {
9898
await expect(component.locator('svg')).toHaveScreenshot();
9999
});
100100

101+
test('Single point (with marker enabled)', async ({mount}) => {
102+
const chartData: ChartData = {
103+
series: {
104+
data: [
105+
{
106+
name: 'Series 1',
107+
type: 'line',
108+
data: [{y: 10, x: 10, marker: {states: {normal: {enabled: true}}}}],
109+
},
110+
],
111+
},
112+
yAxis: [{maxPadding: 0}],
113+
xAxis: {maxPadding: 0},
114+
};
115+
const component = await mount(<ChartTestStory data={chartData} />);
116+
await expect(component.locator('svg')).toHaveScreenshot();
117+
});
118+
101119
test.describe('Data labels', () => {
102120
test('Positioning of extreme point dataLabels', async ({mount}) => {
103121
const chartData: ChartData = {

src/hooks/useShapes/bar-x/prepare-data.ts

Lines changed: 26 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import {ascending, descending, max, sort} from 'd3';
1+
import {ascending, descending, sort} from 'd3';
22
import type {ScaleBand, ScaleLinear, ScaleTime} from 'd3';
33
import get from 'lodash/get';
44

55
import type {BarXSeriesData, LabelData} from '../../../types';
66
import {getDataCategoryValue, getLabelsSize} from '../../../utils';
77
import {getFormattedValue} from '../../../utils/chart/format';
8-
import {MIN_BAR_GAP, MIN_BAR_GROUP_GAP, MIN_BAR_WIDTH} from '../../constants';
98
import type {PreparedXAxis, PreparedYAxis} from '../../useAxis/types';
109
import type {ChartScale} from '../../useAxisScales';
11-
import type {PreparedBarXSeries, PreparedSeriesOptions} from '../../useSeries/types';
10+
import type {PreparedBarXSeries, PreparedSeriesOptions, StackedSeries} from '../../useSeries/types';
11+
import {getSeriesStackId} from '../../useSeries/utils';
1212
import type {PreparedSplit} from '../../useSplit/types';
13+
import {getBarXLayout} from '../../utils/bar-x';
1314

1415
import type {PreparedBarXData} from './types';
1516

@@ -86,10 +87,7 @@ export const prepareBarXData = async (args: {
8687
split,
8788
} = args;
8889
const stackGap: number = seriesOptions['bar-x'].stackGap;
89-
const categories = get(xAxis, 'categories', [] as string[]);
90-
const barMaxWidth = get(seriesOptions, 'bar-x.barMaxWidth');
91-
const barPadding = get(seriesOptions, 'bar-x.barPadding');
92-
const groupPadding = get(seriesOptions, 'bar-x.groupPadding');
90+
const categories = xAxis?.categories ?? [];
9391
const sortingOptions = get(seriesOptions, 'bar-x.dataSorting');
9492
const comparator = sortingOptions?.direction === 'desc' ? descending : ascending;
9593
const sortKey = (() => {
@@ -122,64 +120,42 @@ export const prepareBarXData = async (args: {
122120
if (!isSeriesDataValid(d)) {
123121
return;
124122
}
125-
const xValue =
123+
const key =
126124
xAxis.type === 'category'
127125
? getDataCategoryValue({axisDirection: 'x', categories, data: d})
128126
: d.x;
129127

130-
if (typeof xValue !== 'undefined') {
131-
if (!data[xValue]) {
132-
data[xValue] = {};
128+
if (key !== undefined) {
129+
if (!data[key]) {
130+
data[key] = {};
133131
}
134132

135-
const xGroup = data[xValue];
136-
137-
if (!xGroup[s.stackId]) {
138-
xGroup[s.stackId] = [];
133+
const stackId = getSeriesStackId(s as StackedSeries);
134+
if (!data[key][stackId]) {
135+
data[key][stackId] = [];
139136
}
140137

141-
xGroup[s.stackId].push({data: d, series: s});
138+
data[key][stackId].push({data: d, series: s});
142139
}
143140
});
144141
});
145142

146-
let bandWidth = Infinity;
147-
148-
if (xAxis.type === 'category') {
149-
const xBandScale = xScale as ScaleBand<string>;
150-
bandWidth = xBandScale.bandwidth();
151-
} else {
152-
const xLinearScale = xScale as ScaleLinear<number, number> | ScaleTime<number, number>;
153-
const xValues = series.reduce<number[]>((acc, s) => {
154-
s.data.forEach((dataItem) => acc.push(Number(dataItem.x)));
155-
return acc;
156-
}, []);
157-
158-
xValues.sort().forEach((xValue, index) => {
159-
if (index > 0 && xValue !== xValues[index - 1]) {
160-
const dist = xLinearScale(xValue) - xLinearScale(xValues[index - 1]);
161-
if (dist < bandWidth) {
162-
bandWidth = dist;
163-
}
164-
}
165-
});
166-
}
167-
168143
const result: PreparedBarXData[] = [];
169144

170145
const plotIndexes = Array.from(dataByPlots.keys());
171146
for (let plotDataIndex = 0; plotDataIndex < plotIndexes.length; plotDataIndex++) {
172147
const data = dataByPlots.get(plotIndexes[plotDataIndex]) ?? {};
173-
const maxGroupSize = max(Object.values(data), (d) => Object.values(d).length) || 1;
174-
const groupGap = Math.max(bandWidth * groupPadding, MIN_BAR_GROUP_GAP);
175-
const groupWidth = bandWidth - groupGap;
176-
const rectGap = Math.max(bandWidth * barPadding, MIN_BAR_GAP);
177-
const rectWidth = Math.max(
178-
MIN_BAR_WIDTH,
179-
Math.min(groupWidth / maxGroupSize - rectGap, barMaxWidth),
180-
);
181-
182148
const groupedData = Object.entries(data);
149+
const {
150+
bandSize,
151+
barGap: rectGap,
152+
barSize: rectWidth,
153+
} = getBarXLayout({
154+
groupedData: data,
155+
seriesOptions,
156+
scale: xScale,
157+
});
158+
183159
for (let groupedDataIndex = 0; groupedDataIndex < groupedData.length; groupedDataIndex++) {
184160
const [xValue, val] = groupedData[groupedDataIndex];
185161
const stacks = Object.values(val);
@@ -216,12 +192,12 @@ export const prepareBarXData = async (args: {
216192
continue;
217193
}
218194

219-
xCenter = (xBandScale(xValue as string) || 0) + xBandScale.bandwidth() / 2;
195+
xCenter = (xBandScale(xValue as string) || 0) + bandSize / 2;
220196
} else {
221-
const xLinearScale = xScale as
197+
const scale = xScale as
222198
| ScaleLinear<number, number>
223199
| ScaleTime<number, number>;
224-
xCenter = xLinearScale(Number(xValue));
200+
xCenter = scale(Number(xValue));
225201
}
226202

227203
const x =

src/hooks/useShapes/bar-y/prepare-data.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export async function prepareBarYData(args: {
5151
};
5252
}
5353

54-
const yScaleRange = yLinearScale.range();
5554
const sortingOptions = get(seriesOptions, 'bar-y.dataSorting');
5655
const comparator = sortingOptions?.direction === 'desc' ? descending : ascending;
5756
const sortKey = (() => {
@@ -69,11 +68,9 @@ export async function prepareBarYData(args: {
6968
})();
7069

7170
const groupedData = groupBarYDataByYValue(series, yAxis);
72-
const plotHeight = Math.abs(yScaleRange[0] - yScaleRange[1]);
7371
const {bandSize, barGap, barSize} = getBarYLayout({
7472
groupedData,
7573
seriesOptions,
76-
plotHeight,
7774
scale: yScale,
7875
});
7976

src/hooks/utils/bar-x.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import type {AxisDomain, AxisScale} from 'd3';
2+
import {max} from 'd3';
13
import get from 'lodash/get';
24

35
import type {BarXSeries, BarXSeriesData} from '../../types';
46
import {getDataCategoryValue} from '../../utils';
57
import {MIN_BAR_GAP, MIN_BAR_GROUP_GAP, MIN_BAR_WIDTH} from '../constants';
68
import type {PreparedXAxis} from '../useAxis/types';
9+
import type {ChartScale} from '../useAxisScales';
710
import type {PreparedBarXSeries, PreparedSeriesOptions, StackedSeries} from '../useSeries/types';
811
import {getSeriesStackId} from '../useSeries/utils';
912

13+
import {getBandSize} from './get-band-size';
14+
1015
export function groupBarXDataByXValue<T extends BarXSeries | PreparedBarXSeries>(
1116
series: T[],
1217
xAxis: PreparedXAxis,
@@ -38,27 +43,24 @@ export function groupBarXDataByXValue<T extends BarXSeries | PreparedBarXSeries>
3843
return data;
3944
}
4045

41-
export function getBarXLayoutForNumericScale(args: {
42-
plotWidth: number;
46+
export function getBarXLayout(args: {
4347
seriesOptions: PreparedSeriesOptions;
4448
groupedData: ReturnType<typeof groupBarXDataByXValue>;
49+
scale: ChartScale | undefined;
4550
}) {
46-
const {plotWidth, groupedData, seriesOptions} = args;
51+
const {groupedData, seriesOptions, scale} = args;
4752
const barMaxWidth = get(seriesOptions, 'bar-x.barMaxWidth');
4853
const barPadding = get(seriesOptions, 'bar-x.barPadding');
4954
const groupPadding = get(seriesOptions, 'bar-x.groupPadding');
50-
const groups = Object.values(groupedData);
51-
const maxGroupItemCount = groups.reduce(
52-
(acc, items) => Math.max(acc, Object.keys(items).length),
53-
0,
54-
);
55-
const bandSize = plotWidth / groups.length;
55+
const domain = Object.keys(groupedData);
56+
const bandSize = getBandSize({domain, scale: scale as AxisScale<AxisDomain>});
5657
const groupGap = Math.max(bandSize * groupPadding, MIN_BAR_GROUP_GAP);
58+
const maxGroupSize = max(Object.values(groupedData), (d) => Object.values(d).length) || 1;
5759
const groupSize = bandSize - groupGap;
5860
const barGap = Math.max(bandSize * barPadding, MIN_BAR_GAP);
5961
const barSize = Math.max(
6062
MIN_BAR_WIDTH,
61-
Math.min((groupSize - barGap) / maxGroupItemCount, barMaxWidth),
63+
Math.min(groupSize / maxGroupSize - barGap, barMaxWidth),
6264
);
6365

6466
return {bandSize, barGap, barSize};

src/hooks/utils/bar-y.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ export function groupBarYDataByYValue<T extends BarYSeries | PreparedBarYSeries>
3030
Record<string, {data: PreparedBarYSeriesData; series: T}[]>
3131
> = {};
3232
series.forEach((s) => {
33+
const axisIndex = get(s, 'yAxis', 0);
34+
const seriesYAxis = yAxis[axisIndex];
35+
const categories = get(seriesYAxis, 'categories', [] as string[]);
36+
3337
s.data.forEach((d) => {
3438
if (!isSeriesDataValid(d)) {
3539
return;
3640
}
37-
const axisIndex = get(s, 'yAxis', 0);
38-
const seriesYAxis = yAxis[axisIndex];
39-
const categories = get(seriesYAxis, 'categories', [] as string[]);
41+
4042
const key =
4143
seriesYAxis.type === 'category'
4244
? getDataCategoryValue({axisDirection: 'y', categories, data: d})
@@ -61,7 +63,6 @@ export function groupBarYDataByYValue<T extends BarYSeries | PreparedBarYSeries>
6163
}
6264

6365
export function getBarYLayout(args: {
64-
plotHeight: number;
6566
seriesOptions: PreparedSeriesOptions;
6667
groupedData: ReturnType<typeof groupBarYDataByYValue>;
6768
scale: ChartScale | undefined;

0 commit comments

Comments
 (0)