Skip to content

Commit b9487fb

Browse files
committed
fix(BarSeries): forced stacking for single series histogram
1 parent 76ded5e commit b9487fb

10 files changed

Lines changed: 97 additions & 75 deletions

File tree

packages/charts/src/chart_types/xy_chart/domains/y_domain.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ export function groupSeriesByYGroup(specs: YBasicSeriesSpec[]) {
141141
{ stackMode: StackMode | undefined; stacked: YBasicSeriesSpec[]; nonStacked: YBasicSeriesSpec[] }
142142
>();
143143

144-
const histogramEnabled = isHistogramEnabled(specs);
144+
const isStackedSpec = isStackedSpecFn(specs);
145+
145146
// split each specs by groupId and by stacked or not
146147
specs.forEach((spec) => {
147148
const group = specsByGroupIds.get(spec.groupId) || {
@@ -150,7 +151,7 @@ export function groupSeriesByYGroup(specs: YBasicSeriesSpec[]) {
150151
nonStacked: [],
151152
};
152153

153-
if (isStackedSpec(spec, histogramEnabled)) {
154+
if (isStackedSpec(spec)) {
154155
group.stacked.push(spec);
155156
} else {
156157
group.nonStacked.push(spec);
@@ -168,15 +169,20 @@ export function groupSeriesByYGroup(specs: YBasicSeriesSpec[]) {
168169
}
169170

170171
/** @internal */
171-
export function isHistogramEnabled(specs: YBasicSeriesSpec[]) {
172+
export function hasHistogramBarSpec(specs: YBasicSeriesSpec[]) {
172173
return specs.some(({ seriesType, enableHistogramMode }) => seriesType === SeriesType.Bar && enableHistogramMode);
173174
}
174175

175176
/** @internal */
176-
export function isStackedSpec(spec: YBasicSeriesSpec, histogramEnabled: boolean) {
177-
const isBarAndHistogram = spec.seriesType === SeriesType.Bar && histogramEnabled;
178-
const hasStackAccessors = spec.stackAccessors && spec.stackAccessors.length > 0;
179-
return isBarAndHistogram || hasStackAccessors;
177+
export function isStackedSpecFn(specs: YBasicSeriesSpec[]) {
178+
const barSpecs = specs.filter(({ seriesType }) => seriesType === SeriesType.Bar);
179+
const shouldForceStackBars = barSpecs.length > 1 && barSpecs.some(({ enableHistogramMode }) => enableHistogramMode);
180+
181+
return (spec: YBasicSeriesSpec) => {
182+
const isBarAndHistogram = spec.seriesType === SeriesType.Bar && shouldForceStackBars;
183+
const hasStackAccessors = spec.stackAccessors && spec.stackAccessors.length > 0;
184+
return isBarAndHistogram || hasStackAccessors;
185+
};
180186
}
181187

182188
/** @internal */

packages/charts/src/chart_types/xy_chart/legend/legend.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ import {
2727
getSeriesName,
2828
DataSeries,
2929
getSeriesKey,
30-
isBandedSpec,
3130
getSeriesIdentifierFromDataSeries,
31+
isBandedSpecFn,
3232
} from '../utils/series';
3333
import {
3434
AxisSpec,
@@ -107,6 +107,7 @@ export function computeLegend(
107107
): LegendItem[] {
108108
const legendItems: LegendItem[] = [];
109109
const defaultColor = theme.colors.defaultVizColor;
110+
const isBandedSpec = isBandedSpecFn(specs);
110111

111112
dataSeries.forEach((series) => {
112113
const { specId, yAccessor } = series;

packages/charts/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,34 @@
77
*/
88

99
import { computeSeriesDomainsSelector } from './compute_series_domains';
10-
import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled';
10+
import { getSeriesSpecsSelector } from './get_specs';
1111
import { SeriesType } from '../../../../specs';
1212
import { createCustomCachedSelector } from '../../../../state/create_selector';
1313
import { groupBy } from '../../utils/group_data_series';
14-
import { SeriesDomainsAndData } from '../utils/types';
15-
import { getBarIndexKey } from '../utils/utils';
14+
import { getBarIndexKeyFn } from '../utils/utils';
1615

1716
/** @internal */
1817
export const countBarsInClusterSelector = createCustomCachedSelector(
19-
[computeSeriesDomainsSelector, isHistogramModeEnabledSelector],
20-
countBarsInCluster,
21-
);
22-
23-
/** @internal */
24-
export function countBarsInCluster({ formattedDataSeries }: SeriesDomainsAndData, isHistogramEnabled: boolean): number {
25-
const barDataSeries = formattedDataSeries.filter(({ seriesType }) => seriesType === SeriesType.Bar);
26-
27-
const dataSeriesGroupedByPanel = groupBy(
28-
barDataSeries,
29-
['smVerticalAccessorValue', 'smHorizontalAccessorValue'],
30-
false,
31-
);
32-
33-
const barIndexByPanel = Object.keys(dataSeriesGroupedByPanel).reduce<Record<string, string[]>>((acc, panelKey) => {
34-
const panelBars = dataSeriesGroupedByPanel[panelKey] ?? [];
35-
const barDataSeriesByBarIndex = groupBy(
36-
panelBars,
37-
(d) => {
38-
return getBarIndexKey(d, isHistogramEnabled);
39-
},
18+
[computeSeriesDomainsSelector, getSeriesSpecsSelector],
19+
function countBarsInCluster({ formattedDataSeries }, specs): number {
20+
const getBarIndexKey = getBarIndexKeyFn(specs);
21+
const barDataSeries = formattedDataSeries.filter(({ seriesType }) => seriesType === SeriesType.Bar);
22+
const dataSeriesGroupedByPanel = groupBy(
23+
barDataSeries,
24+
['smVerticalAccessorValue', 'smHorizontalAccessorValue'],
4025
false,
4126
);
4227

43-
acc[panelKey] = Object.keys(barDataSeriesByBarIndex);
44-
return acc;
45-
}, {});
28+
const barIndexByPanel = Object.keys(dataSeriesGroupedByPanel).reduce<Record<string, string[]>>((acc, panelKey) => {
29+
const panelBars = dataSeriesGroupedByPanel[panelKey] ?? [];
30+
const barDataSeriesByBarIndex = groupBy(panelBars, getBarIndexKey, false);
31+
32+
acc[panelKey] = Object.keys(barDataSeriesByBarIndex);
33+
return acc;
34+
}, {});
4635

47-
return Object.values(barIndexByPanel).reduce((acc, curr) => {
48-
return Math.max(acc, curr.length);
49-
}, 0);
50-
}
36+
return Object.values(barIndexByPanel).reduce((acc, curr) => {
37+
return Math.max(acc, curr.length);
38+
}, 0);
39+
},
40+
);

packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { getTooltipCompareFn } from '../../../../utils/series_sort';
3939
import { isPointOnGeometry } from '../../rendering/utils';
4040
import { formatTooltipHeader, formatTooltipValue } from '../../tooltip/tooltip';
4141
import { defaultXYLegendSeriesSort } from '../../utils/default_series_sort_fn';
42-
import { DataSeries } from '../../utils/series';
42+
import { DataSeries, isBandedSpecFn } from '../../utils/series';
4343
import { BasicSeriesSpec, AxisSpec } from '../../utils/specs';
4444
import { getAxesSpecForSpecId, getSpecDomainGroupId, getSpecsById } from '../utils/spec';
4545
import { ComputedScales } from '../utils/types';
@@ -129,6 +129,7 @@ function getTooltipAndHighlightFromValue(
129129
const highlightedGeometries: IndexedGeometry[] = [];
130130
const xValues = new Set<any>();
131131
const hideNullValues = !tooltip.showNullValues;
132+
const isBandedSpec = isBandedSpecFn(seriesSpecs);
132133
const values = matchingGeoms.reduce<TooltipValue[]>((acc, indexedGeometry) => {
133134
if (hideNullValues && indexedGeometry.value.y === null) {
134135
return acc;
@@ -161,7 +162,14 @@ function getTooltipAndHighlightFromValue(
161162
}
162163

163164
// format the tooltip values
164-
const formattedTooltip = formatTooltipValue(indexedGeometry, spec, isHighlighted, hasSingleSeries, yAxis);
165+
const formattedTooltip = formatTooltipValue(
166+
indexedGeometry,
167+
spec,
168+
isHighlighted,
169+
hasSingleSeries,
170+
isBandedSpec(spec),
171+
yAxis,
172+
);
165173

166174
// format only one time the x value
167175
if (!header) {

packages/charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { clamp, Rotation } from '../../../../utils/common';
2828
import { Dimensions } from '../../../../utils/dimensions';
2929
import { hasDragged, DragCheckProps } from '../../../../utils/events';
3030
import { GroupId } from '../../../../utils/ids';
31-
import { isHistogramEnabled } from '../../domains/y_domain';
31+
import { hasHistogramBarSpec } from '../../domains/y_domain';
3232
import { isVerticalRotation } from '../utils/common';
3333

3434
const getLastDragSelector = (state: GlobalChartState) => state.interactions.pointer.lastDrag;
@@ -155,7 +155,7 @@ function getXBrushExtent(
155155
return;
156156
}
157157
const offset = histogramMode ? 0 : -(xScale.bandwidth + xScale.bandwidthPadding) / 2;
158-
const histogramEnabled = isHistogramEnabled(seriesSpecs);
158+
const histogramEnabled = hasHistogramBarSpec(seriesSpecs);
159159
const invertValue =
160160
histogramEnabled && roundHistogramBrushValues
161161
? (value: number) => xScale.invertWithStep(value, xScale.domain).value

packages/charts/src/chart_types/xy_chart/state/utils/utils.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { getRenderingCompareFn } from '../../../../utils/series_sort';
3232
import { ColorConfig, Theme } from '../../../../utils/themes/theme';
3333
import { XDomain } from '../../domains/types';
3434
import { mergeXDomain } from '../../domains/x_domain';
35-
import { isStackedSpec, mergeYDomain } from '../../domains/y_domain';
35+
import { YBasicSeriesSpec, isStackedSpecFn, mergeYDomain } from '../../domains/y_domain';
3636
import { renderArea } from '../../rendering/area';
3737
import { renderBars } from '../../rendering/bars';
3838
import { renderBubble } from '../../rendering/bubble';
@@ -47,7 +47,7 @@ import {
4747
getDataSeriesFromSpecs,
4848
getFormattedDataSeries,
4949
getSeriesKey,
50-
isBandedSpec,
50+
isBandedSpecFn,
5151
} from '../../utils/series';
5252
import {
5353
AnnotationDomainType,
@@ -204,9 +204,10 @@ export function computeSeriesGeometries(
204204
false,
205205
);
206206

207+
const getBarIndexKey = getBarIndexKeyFn(seriesSpecs);
207208
const barIndexByPanel = Object.keys(dataSeriesGroupedByPanel).reduce<Record<string, string[]>>((acc, panelKey) => {
208209
const panelBars = dataSeriesGroupedByPanel[panelKey] ?? [];
209-
const barDataSeriesByBarIndex = groupBy(panelBars, (d) => getBarIndexKey(d, enableHistogramMode), false);
210+
const barDataSeriesByBarIndex = groupBy(panelBars, getBarIndexKey, false);
210211
acc[panelKey] = Object.keys(barDataSeriesByBarIndex);
211212
return acc;
212213
}, {});
@@ -325,6 +326,8 @@ function renderGeometries(
325326
bubblePoints: 0,
326327
};
327328
const barsPadding = enableHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding;
329+
const isBandedSpec = isBandedSpecFn(seriesSpecs);
330+
const getBarIndexKey = getBarIndexKeyFn(seriesSpecs);
328331

329332
dataSeries.forEach((ds) => {
330333
const spec = getSpecsById<BasicSeriesSpec>(seriesSpecs, ds.specId);
@@ -370,7 +373,7 @@ function renderGeometries(
370373
const color = seriesColorsMap.get(dataSeriesKey) || defaultColor;
371374

372375
if (isBarSeriesSpec(spec)) {
373-
const shift = barIndexOrder.indexOf(getBarIndexKey(ds, enableHistogramMode));
376+
const shift = barIndexOrder.indexOf(getBarIndexKey(ds));
374377

375378
if (shift === -1) return; // skip bar dataSeries if index is not available
376379

@@ -528,14 +531,13 @@ function hasFitFnConfigured(fit?: Fit | FitConfig) {
528531
}
529532

530533
/** @internal */
531-
export function getBarIndexKey(
532-
{ spec, specId, groupId, yAccessor, splitAccessors }: DataSeries,
533-
histogramModeEnabled: boolean,
534-
) {
535-
const isStacked = isStackedSpec(spec, histogramModeEnabled);
536-
if (isStacked) {
537-
return [groupId, '__stacked__'].join('__-__');
538-
}
534+
export function getBarIndexKeyFn(specs: YBasicSeriesSpec[]) {
535+
const isStackedSpec = isStackedSpecFn(specs);
536+
return ({ spec, specId, groupId, yAccessor, splitAccessors }: DataSeries) => {
537+
if (isStackedSpec(spec)) {
538+
return [groupId, '__stacked__'].join('__-__');
539+
}
539540

540-
return [groupId, specId, ...splitAccessors.values(), yAccessor].join('__-__');
541+
return [groupId, specId, ...splitAccessors.values(), yAccessor].join('__-__');
542+
};
541543
}

packages/charts/src/chart_types/xy_chart/tooltip/tooltip.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { getAccessorFormatLabel } from '../../../utils/accessor';
1414
import { isDefined } from '../../../utils/common';
1515
import { BandedAccessorType, IndexedGeometry } from '../../../utils/geometry';
1616
import { defaultTickFormatter } from '../utils/axis_utils';
17-
import { getSeriesName, isBandedSpec } from '../utils/series';
17+
import { getSeriesName } from '../utils/series';
1818
import { AxisSpec, BasicSeriesSpec, isAreaSeriesSpec, isBarSeriesSpec, TickFormatterOptions } from '../utils/specs';
1919

2020
/** @internal */
@@ -55,11 +55,12 @@ export function formatTooltipValue(
5555
spec: BasicSeriesSpec,
5656
isHighlighted: boolean,
5757
hasSingleSeries: boolean,
58+
isBanded: boolean,
5859
axisSpec?: AxisSpec,
5960
): TooltipValue {
6061
let label = getSeriesName(seriesIdentifier, hasSingleSeries, true, spec);
6162

62-
if (isBandedSpec(spec) && (isAreaSeriesSpec(spec) || isBarSeriesSpec(spec))) {
63+
if (isBanded && (isAreaSeriesSpec(spec) || isBarSeriesSpec(spec))) {
6364
const { y0AccessorFormat = Y0_ACCESSOR_POSTFIX, y1AccessorFormat = Y1_ACCESSOR_POSTFIX } = spec;
6465
const formatter = accessor === BandedAccessorType.Y0 ? y0AccessorFormat : y1AccessorFormat;
6566
label = getAccessorFormatLabel(formatter, label);

packages/charts/src/chart_types/xy_chart/utils/series.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { Datum, isNil, stripUndefined } from '../../../utils/common';
2222
import { GroupId } from '../../../utils/ids';
2323
import { Logger } from '../../../utils/logger';
2424
import { ColorConfig } from '../../../utils/themes/theme';
25-
import { groupSeriesByYGroup, isHistogramEnabled, isStackedSpec } from '../domains/y_domain';
25+
import { YBasicSeriesSpec, groupSeriesByYGroup, isStackedSpecFn } from '../domains/y_domain';
2626
import { X_SCALE_DEFAULT } from '../scales/scale_defaults';
2727

2828
/** @internal */
@@ -112,6 +112,7 @@ export function splitSeriesDataByAccessors(
112112
spec: BasicSeriesSpec,
113113
xValueSums: Map<string | number, number>,
114114
isStacked = false,
115+
isBanded = false,
115116
stackMode?: StackMode,
116117
groupBySpec?: SmallMultiplesGroupBy,
117118
): {
@@ -170,7 +171,7 @@ export function splitSeriesDataByAccessors(
170171
datum,
171172
accessor,
172173
nonNumericValues,
173-
isBandedSpec(spec),
174+
isBanded,
174175
y0Accessors && y0Accessors[index],
175176
markSizeAccessor,
176177
);
@@ -318,7 +319,7 @@ export function getFormattedDataSeries(
318319
xValues: Set<string | number>,
319320
xScaleType: ScaleType,
320321
): DataSeries[] {
321-
const histogramEnabled = isHistogramEnabled(seriesSpecs);
322+
const isStackedSpec = isStackedSpecFn(seriesSpecs);
322323

323324
// apply fit function to every data series
324325
const fittedDataSeries = applyFitFunctionToDataSeries(
@@ -328,7 +329,7 @@ export function getFormattedDataSeries(
328329
);
329330

330331
// apply fitting for stacked DataSeries by YGroup, Panel
331-
const stackedDataSeries = fittedDataSeries.filter(({ spec }) => isStackedSpec(spec, histogramEnabled));
332+
const stackedDataSeries = fittedDataSeries.filter(({ spec }) => isStackedSpec(spec));
332333
const stackedGroups = groupBy<DataSeries>(
333334
stackedDataSeries,
334335
['smHorizontalAccessorValue', 'smVerticalAccessorValue', 'groupId'],
@@ -342,7 +343,7 @@ export function getFormattedDataSeries(
342343
return [...acc, ...formatted];
343344
}, []);
344345
// get already fitted non stacked dataSeries
345-
const nonStackedDataSeries = fittedDataSeries.filter(({ spec }) => !isStackedSpec(spec, histogramEnabled));
346+
const nonStackedDataSeries = fittedDataSeries.filter(({ spec }) => !isStackedSpec(spec));
346347

347348
return [...fittedAndStackedDataSeries, ...nonStackedDataSeries];
348349
}
@@ -370,6 +371,7 @@ export function getDataSeriesFromSpecs(
370371
let isOrdinalScale = false;
371372

372373
const specsByYGroup = groupSeriesByYGroup(seriesSpecs);
374+
const isBandedSpec = isBandedSpecFn(seriesSpecs);
373375

374376
// eslint-disable-next-line no-restricted-syntax
375377
for (const spec of seriesSpecs) {
@@ -381,10 +383,13 @@ export function getDataSeriesFromSpecs(
381383

382384
const specGroup = specsByYGroup.get(spec.groupId);
383385
const isStacked = Boolean(specGroup?.stacked.find(({ id }) => id === spec.id));
386+
const isBanded = isBandedSpec(spec);
387+
384388
const { dataSeries, xValues } = splitSeriesDataByAccessors(
385389
spec,
386390
mutatedXValueSums,
387391
isStacked,
392+
isBanded,
388393
specGroup?.stackMode,
389394
groupBySpec,
390395
);
@@ -461,8 +466,11 @@ export function getDataSeriesFromSpecs(
461466
* TODO: Add check for chart type other than area and bar.
462467
* @internal
463468
*/
464-
export function isBandedSpec(spec: BasicSeriesSpec): boolean {
465-
return Boolean(spec.y0Accessors && spec.y0Accessors.length > 0 && !isStackedSpec(spec, false));
469+
export function isBandedSpecFn(specs: YBasicSeriesSpec[]) {
470+
const isStackedSpec = isStackedSpecFn(specs);
471+
return (spec: BasicSeriesSpec) => {
472+
return Boolean(spec.y0Accessors && spec.y0Accessors.length > 0 && !isStackedSpec(spec));
473+
};
466474
}
467475

468476
function getSortedOrdinalXValues(

0 commit comments

Comments
 (0)