Skip to content

Commit 7634f5c

Browse files
authored
feat(bar_chart): color/style override accessor (#271)
Allow user to override colors/styles of given BarChart datum based on accessor function prop called StyleAccessor. BREAKING CHANGE: colorAccessors removed from YBasicSeriesSpec (aka for all series) which had acted similarly to a split accessor. #216
1 parent a145c79 commit 7634f5c

File tree

8 files changed

+262
-70
lines changed

8 files changed

+262
-70
lines changed

src/chart_types/xy_chart/domains/y_domain.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type YDomain = BaseDomain & {
1616
};
1717
export type YBasicSeriesSpec = Pick<
1818
BasicSeriesSpec,
19-
'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'colorAccessors'
19+
'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'styleAccessor'
2020
> & { stackAsPercentage?: boolean };
2121

2222
export function mergeYDomain(

src/chart_types/xy_chart/rendering/rendering.test.ts

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { DEFAULT_GEOMETRY_STYLES } from '../../../utils/themes/theme_commons';
22
import { getSpecId } from '../../../utils/ids';
3-
import { BarGeometry, getGeometryStyle, isPointOnGeometry, PointGeometry } from './rendering';
3+
import {
4+
BarGeometry,
5+
getGeometryStyle,
6+
isPointOnGeometry,
7+
PointGeometry,
8+
getStyleOverrides,
9+
GeometryId,
10+
} from './rendering';
11+
import { BarSeriesStyle } from '../../../utils/themes/theme';
12+
import { DataSeriesDatum } from '../utils/series';
13+
import { RecursivePartial, mergePartial } from '../../../utils/commons';
414

515
describe('Rendering utils', () => {
616
test('check if point is in geometry', () => {
@@ -168,4 +178,110 @@ describe('Rendering utils', () => {
168178

169179
expect(noHover).toEqual({ opacity: 1 });
170180
});
181+
182+
describe('getStyleOverrides', () => {
183+
let mockAccessor: jest.Mock;
184+
185+
const sampleSeriesStyle: BarSeriesStyle = {
186+
rect: {
187+
opacity: 1,
188+
},
189+
rectBorder: {
190+
visible: true,
191+
strokeWidth: 1,
192+
},
193+
displayValue: {
194+
fontSize: 10,
195+
fontFamily: 'helvetica',
196+
fill: 'blue',
197+
padding: 1,
198+
offsetX: 1,
199+
offsetY: 1,
200+
},
201+
};
202+
const datum: DataSeriesDatum = {
203+
x: 1,
204+
y1: 2,
205+
y0: 3,
206+
initialY1: 4,
207+
initialY0: 5,
208+
};
209+
const geometryId: GeometryId = {
210+
specId: getSpecId('test'),
211+
seriesKey: ['test'],
212+
};
213+
214+
beforeEach(() => {
215+
mockAccessor = jest.fn();
216+
});
217+
218+
it('should return input seriesStyle if no styleAccessor is passed', () => {
219+
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle);
220+
221+
expect(styleOverrides).toBe(sampleSeriesStyle);
222+
});
223+
224+
it('should return input seriesStyle if styleAccessor returns null', () => {
225+
mockAccessor.mockReturnValue(null);
226+
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
227+
228+
expect(styleOverrides).toBe(sampleSeriesStyle);
229+
});
230+
231+
it('should call styleAccessor with datum and geometryId', () => {
232+
getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
233+
234+
expect(mockAccessor).toBeCalledWith(datum, geometryId);
235+
});
236+
237+
it('should return seriesStyle with updated fill color', () => {
238+
const color = 'blue';
239+
mockAccessor.mockReturnValue(color);
240+
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
241+
const expectedStyles: BarSeriesStyle = {
242+
...sampleSeriesStyle,
243+
rect: {
244+
...sampleSeriesStyle.rect,
245+
fill: color,
246+
},
247+
};
248+
expect(styleOverrides).toEqual(expectedStyles);
249+
});
250+
251+
it('should return a new seriesStyle object with color', () => {
252+
mockAccessor.mockReturnValue('blue');
253+
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
254+
255+
expect(styleOverrides).not.toBe(sampleSeriesStyle);
256+
});
257+
258+
it('should return seriesStyle with updated partial style', () => {
259+
const partialStyle: RecursivePartial<BarSeriesStyle> = {
260+
rect: {
261+
fill: 'blue',
262+
},
263+
rectBorder: {
264+
strokeWidth: 10,
265+
},
266+
};
267+
mockAccessor.mockReturnValue(partialStyle);
268+
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
269+
const expectedStyles = mergePartial(sampleSeriesStyle, partialStyle, {
270+
mergeOptionalPartialValues: true,
271+
});
272+
273+
expect(styleOverrides).toEqual(expectedStyles);
274+
});
275+
276+
it('should return a new seriesStyle object with partial styles', () => {
277+
mockAccessor.mockReturnValue({
278+
rect: {
279+
fill: 'blue',
280+
},
281+
});
282+
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
283+
284+
expect(styleOverrides).not.toBe(sampleSeriesStyle);
285+
});
286+
});
171287
});

src/chart_types/xy_chart/rendering/rendering.ts

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { area, line } from 'd3-shape';
2+
23
import { CanvasTextBBoxCalculator } from '../../../utils/bbox/canvas_text_bbox_calculator';
34
import {
45
AreaSeriesStyle,
@@ -16,7 +17,8 @@ import { CurveType, getCurveFactory } from '../../../utils/curves';
1617
import { LegendItem } from '../legend/legend';
1718
import { DataSeriesDatum } from '../utils/series';
1819
import { belongsToDataSeries } from '../utils/series_utils';
19-
import { DisplayValueSpec } from '../utils/specs';
20+
import { DisplayValueSpec, StyleAccessor } from '../utils/specs';
21+
import { mergePartial } from '../../../utils/commons';
2022

2123
export interface GeometryId {
2224
specId: SpecId;
@@ -113,6 +115,33 @@ export function mutableIndexedGeometryMapUpsert(
113115
}
114116
}
115117

118+
export function getStyleOverrides(
119+
datum: DataSeriesDatum,
120+
geometryId: GeometryId,
121+
seriesStyle: BarSeriesStyle,
122+
styleAccessor?: StyleAccessor,
123+
): BarSeriesStyle {
124+
const styleOverride = styleAccessor && styleAccessor(datum, geometryId);
125+
126+
if (!styleOverride) {
127+
return seriesStyle;
128+
}
129+
130+
if (typeof styleOverride === 'string') {
131+
return {
132+
...seriesStyle,
133+
rect: {
134+
...seriesStyle.rect,
135+
fill: styleOverride,
136+
},
137+
};
138+
}
139+
140+
return mergePartial(seriesStyle, styleOverride, {
141+
mergeOptionalPartialValues: true,
142+
});
143+
}
144+
116145
export function renderPoints(
117146
shift: number,
118147
dataset: DataSeriesDatum[],
@@ -195,8 +224,9 @@ export function renderBars(
195224
color: string,
196225
specId: SpecId,
197226
seriesKey: any[],
198-
seriesStyle: BarSeriesStyle,
227+
sharedSeriesStyle: BarSeriesStyle,
199228
displayValueSettings?: DisplayValueSpec,
229+
styleAccessor?: StyleAccessor,
200230
): {
201231
barGeometries: BarGeometry[];
202232
indexedGeometries: Map<any, IndexedGeometry[]>;
@@ -210,8 +240,8 @@ export function renderBars(
210240

211241
// default padding to 1 for now
212242
const padding = 1;
213-
const fontSize = seriesStyle.displayValue.fontSize;
214-
const fontFamily = seriesStyle.displayValue.fontFamily;
243+
const fontSize = sharedSeriesStyle.displayValue.fontSize;
244+
const fontFamily = sharedSeriesStyle.displayValue.fontFamily;
215245

216246
dataset.forEach((datum) => {
217247
const { y0, y1, initialY1 } = datum;
@@ -278,6 +308,13 @@ export function renderBars(
278308
}
279309
: undefined;
280310

311+
const geometryId = {
312+
specId,
313+
seriesKey,
314+
};
315+
316+
const seriesStyle = getStyleOverrides(datum, geometryId, sharedSeriesStyle, styleAccessor);
317+
281318
const barGeometry: BarGeometry = {
282319
displayValue,
283320
x,
@@ -290,10 +327,7 @@ export function renderBars(
290327
y: initialY1,
291328
accessor: 'y1',
292329
},
293-
geometryId: {
294-
specId,
295-
seriesKey,
296-
},
330+
geometryId,
297331
seriesStyle,
298332
};
299333
mutableIndexedGeometryMapUpsert(indexedGeometries, datum.x, barGeometry);

src/chart_types/xy_chart/store/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ export function renderGeometries(
451451
ds.key,
452452
barSeriesStyle,
453453
displayValueSettings,
454+
spec.styleAccessor,
454455
);
455456
barGeometriesIndex = mergeGeometriesIndexes(barGeometriesIndex, renderedBars.indexedGeometries);
456457
bars.push(...renderedBars.barGeometries);

src/chart_types/xy_chart/utils/series.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ export function splitSeries(
9292
xValues: Set<any>;
9393
} {
9494
const { xAccessor, yAccessors, y0Accessors, splitSeriesAccessors = [] } = accessors;
95-
const colorAccessors = accessors.colorAccessors ? accessors.colorAccessors : splitSeriesAccessors;
9695
const isMultipleY = yAccessors && yAccessors.length > 1;
9796
const series = new Map<string, RawDataSeries>();
9897
const colorsValues = new Map<string, any[]>();
@@ -102,15 +101,15 @@ export function splitSeries(
102101
const seriesKey = getAccessorsValues(datum, splitSeriesAccessors);
103102
if (isMultipleY) {
104103
yAccessors.forEach((accessor, index) => {
105-
const colorValues = getColorValues(datum, colorAccessors, accessor);
104+
const colorValues = getColorValues(datum, splitSeriesAccessors, accessor);
106105
const colorValuesKey = getColorValuesAsString(colorValues, specId);
107106
colorsValues.set(colorValuesKey, colorValues);
108107
const cleanedDatum = cleanDatum(datum, xAccessor, accessor, y0Accessors && y0Accessors[index]);
109108
xValues.add(cleanedDatum.x);
110109
updateSeriesMap(series, [...seriesKey, accessor], cleanedDatum, specId, colorValuesKey);
111110
}, {});
112111
} else {
113-
const colorValues = getColorValues(datum, colorAccessors);
112+
const colorValues = getColorValues(datum, splitSeriesAccessors);
114113
const colorValuesKey = getColorValuesAsString(colorValues, specId);
115114
colorsValues.set(colorValuesKey, colorValues);
116115
const cleanedDatum = cleanDatum(datum, xAccessor, yAccessors[0], y0Accessors && y0Accessors[0]);
@@ -165,8 +164,8 @@ function getAccessorsValues(datum: Datum, accessors: Accessor[] = []): any[] {
165164
/**
166165
* Get the array of values that forms a series key
167166
*/
168-
function getColorValues(datum: Datum, colorAccessors: Accessor[] = [], yAccessorValue?: any): any[] {
169-
const colorValues = getAccessorsValues(datum, colorAccessors);
167+
function getColorValues(datum: Datum, accessors: Accessor[] = [], yAccessorValue?: any): any[] {
168+
const colorValues = getAccessorsValues(datum, accessors);
170169
if (yAccessorValue) {
171170
return [...colorValues, yAccessorValue];
172171
}

src/chart_types/xy_chart/utils/specs.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ import { Omit, RecursivePartial } from '../../../utils/commons';
1111
import { AnnotationId, AxisId, GroupId, SpecId } from '../../../utils/ids';
1212
import { ScaleContinuousType, ScaleType } from '../../../utils/scales/scales';
1313
import { CurveType } from '../../../utils/curves';
14+
import { DataSeriesColorsValues, RawDataSeriesDatum } from './series';
15+
import { GeometryId } from '../rendering/rendering';
1416
import { AnnotationTooltipFormatter } from '../annotations/annotation_utils';
15-
import { DataSeriesColorsValues } from './series';
1617

1718
export type Datum = any;
1819
export type Rotation = 0 | 90 | -90 | 180;
1920
export type Rendering = 'canvas' | 'svg';
21+
export type Color = string;
22+
export type StyleOverride = RecursivePartial<BarSeriesStyle> | Color | null;
23+
export type StyleAccessor = (datum: RawDataSeriesDatum, geometryId: GeometryId) => StyleOverride;
2024

2125
interface DomainMinInterval {
2226
/** Custom minInterval for the domain which will affect data bucket size.
@@ -97,8 +101,8 @@ export interface SeriesAccessors {
97101
splitSeriesAccessors?: Accessor[];
98102
/** An array of fields thats indicates the stack membership */
99103
stackAccessors?: Accessor[];
100-
/** An optional array of field name thats indicates the stack membership */
101-
colorAccessors?: Accessor[];
104+
/** An optional functional accessor to return custom datum color or style */
105+
styleAccessor?: StyleAccessor;
102106
}
103107

104108
export interface SeriesScales {

0 commit comments

Comments
 (0)