Skip to content

Commit

Permalink
feat(bar_chart): color/style override accessor (#271)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nickofthyme committed Aug 5, 2019
1 parent a145c79 commit 7634f5c
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 70 deletions.
2 changes: 1 addition & 1 deletion src/chart_types/xy_chart/domains/y_domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type YDomain = BaseDomain & {
};
export type YBasicSeriesSpec = Pick<
BasicSeriesSpec,
'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'colorAccessors'
'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'styleAccessor'
> & { stackAsPercentage?: boolean };

export function mergeYDomain(
Expand Down
118 changes: 117 additions & 1 deletion src/chart_types/xy_chart/rendering/rendering.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { DEFAULT_GEOMETRY_STYLES } from '../../../utils/themes/theme_commons';
import { getSpecId } from '../../../utils/ids';
import { BarGeometry, getGeometryStyle, isPointOnGeometry, PointGeometry } from './rendering';
import {
BarGeometry,
getGeometryStyle,
isPointOnGeometry,
PointGeometry,
getStyleOverrides,
GeometryId,
} from './rendering';
import { BarSeriesStyle } from '../../../utils/themes/theme';
import { DataSeriesDatum } from '../utils/series';
import { RecursivePartial, mergePartial } from '../../../utils/commons';

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

expect(noHover).toEqual({ opacity: 1 });
});

describe('getStyleOverrides', () => {
let mockAccessor: jest.Mock;

const sampleSeriesStyle: BarSeriesStyle = {
rect: {
opacity: 1,
},
rectBorder: {
visible: true,
strokeWidth: 1,
},
displayValue: {
fontSize: 10,
fontFamily: 'helvetica',
fill: 'blue',
padding: 1,
offsetX: 1,
offsetY: 1,
},
};
const datum: DataSeriesDatum = {
x: 1,
y1: 2,
y0: 3,
initialY1: 4,
initialY0: 5,
};
const geometryId: GeometryId = {
specId: getSpecId('test'),
seriesKey: ['test'],
};

beforeEach(() => {
mockAccessor = jest.fn();
});

it('should return input seriesStyle if no styleAccessor is passed', () => {
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle);

expect(styleOverrides).toBe(sampleSeriesStyle);
});

it('should return input seriesStyle if styleAccessor returns null', () => {
mockAccessor.mockReturnValue(null);
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);

expect(styleOverrides).toBe(sampleSeriesStyle);
});

it('should call styleAccessor with datum and geometryId', () => {
getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);

expect(mockAccessor).toBeCalledWith(datum, geometryId);
});

it('should return seriesStyle with updated fill color', () => {
const color = 'blue';
mockAccessor.mockReturnValue(color);
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
const expectedStyles: BarSeriesStyle = {
...sampleSeriesStyle,
rect: {
...sampleSeriesStyle.rect,
fill: color,
},
};
expect(styleOverrides).toEqual(expectedStyles);
});

it('should return a new seriesStyle object with color', () => {
mockAccessor.mockReturnValue('blue');
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);

expect(styleOverrides).not.toBe(sampleSeriesStyle);
});

it('should return seriesStyle with updated partial style', () => {
const partialStyle: RecursivePartial<BarSeriesStyle> = {
rect: {
fill: 'blue',
},
rectBorder: {
strokeWidth: 10,
},
};
mockAccessor.mockReturnValue(partialStyle);
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
const expectedStyles = mergePartial(sampleSeriesStyle, partialStyle, {
mergeOptionalPartialValues: true,
});

expect(styleOverrides).toEqual(expectedStyles);
});

it('should return a new seriesStyle object with partial styles', () => {
mockAccessor.mockReturnValue({
rect: {
fill: 'blue',
},
});
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);

expect(styleOverrides).not.toBe(sampleSeriesStyle);
});
});
});
50 changes: 42 additions & 8 deletions src/chart_types/xy_chart/rendering/rendering.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { area, line } from 'd3-shape';

import { CanvasTextBBoxCalculator } from '../../../utils/bbox/canvas_text_bbox_calculator';
import {
AreaSeriesStyle,
Expand All @@ -16,7 +17,8 @@ import { CurveType, getCurveFactory } from '../../../utils/curves';
import { LegendItem } from '../legend/legend';
import { DataSeriesDatum } from '../utils/series';
import { belongsToDataSeries } from '../utils/series_utils';
import { DisplayValueSpec } from '../utils/specs';
import { DisplayValueSpec, StyleAccessor } from '../utils/specs';
import { mergePartial } from '../../../utils/commons';

export interface GeometryId {
specId: SpecId;
Expand Down Expand Up @@ -113,6 +115,33 @@ export function mutableIndexedGeometryMapUpsert(
}
}

export function getStyleOverrides(
datum: DataSeriesDatum,
geometryId: GeometryId,
seriesStyle: BarSeriesStyle,
styleAccessor?: StyleAccessor,
): BarSeriesStyle {
const styleOverride = styleAccessor && styleAccessor(datum, geometryId);

if (!styleOverride) {
return seriesStyle;
}

if (typeof styleOverride === 'string') {
return {
...seriesStyle,
rect: {
...seriesStyle.rect,
fill: styleOverride,
},
};
}

return mergePartial(seriesStyle, styleOverride, {
mergeOptionalPartialValues: true,
});
}

export function renderPoints(
shift: number,
dataset: DataSeriesDatum[],
Expand Down Expand Up @@ -195,8 +224,9 @@ export function renderBars(
color: string,
specId: SpecId,
seriesKey: any[],
seriesStyle: BarSeriesStyle,
sharedSeriesStyle: BarSeriesStyle,
displayValueSettings?: DisplayValueSpec,
styleAccessor?: StyleAccessor,
): {
barGeometries: BarGeometry[];
indexedGeometries: Map<any, IndexedGeometry[]>;
Expand All @@ -210,8 +240,8 @@ export function renderBars(

// default padding to 1 for now
const padding = 1;
const fontSize = seriesStyle.displayValue.fontSize;
const fontFamily = seriesStyle.displayValue.fontFamily;
const fontSize = sharedSeriesStyle.displayValue.fontSize;
const fontFamily = sharedSeriesStyle.displayValue.fontFamily;

dataset.forEach((datum) => {
const { y0, y1, initialY1 } = datum;
Expand Down Expand Up @@ -278,6 +308,13 @@ export function renderBars(
}
: undefined;

const geometryId = {
specId,
seriesKey,
};

const seriesStyle = getStyleOverrides(datum, geometryId, sharedSeriesStyle, styleAccessor);

const barGeometry: BarGeometry = {
displayValue,
x,
Expand All @@ -290,10 +327,7 @@ export function renderBars(
y: initialY1,
accessor: 'y1',
},
geometryId: {
specId,
seriesKey,
},
geometryId,
seriesStyle,
};
mutableIndexedGeometryMapUpsert(indexedGeometries, datum.x, barGeometry);
Expand Down
1 change: 1 addition & 0 deletions src/chart_types/xy_chart/store/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ export function renderGeometries(
ds.key,
barSeriesStyle,
displayValueSettings,
spec.styleAccessor,
);
barGeometriesIndex = mergeGeometriesIndexes(barGeometriesIndex, renderedBars.indexedGeometries);
bars.push(...renderedBars.barGeometries);
Expand Down
9 changes: 4 additions & 5 deletions src/chart_types/xy_chart/utils/series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ export function splitSeries(
xValues: Set<any>;
} {
const { xAccessor, yAccessors, y0Accessors, splitSeriesAccessors = [] } = accessors;
const colorAccessors = accessors.colorAccessors ? accessors.colorAccessors : splitSeriesAccessors;
const isMultipleY = yAccessors && yAccessors.length > 1;
const series = new Map<string, RawDataSeries>();
const colorsValues = new Map<string, any[]>();
Expand All @@ -102,15 +101,15 @@ export function splitSeries(
const seriesKey = getAccessorsValues(datum, splitSeriesAccessors);
if (isMultipleY) {
yAccessors.forEach((accessor, index) => {
const colorValues = getColorValues(datum, colorAccessors, accessor);
const colorValues = getColorValues(datum, splitSeriesAccessors, accessor);
const colorValuesKey = getColorValuesAsString(colorValues, specId);
colorsValues.set(colorValuesKey, colorValues);
const cleanedDatum = cleanDatum(datum, xAccessor, accessor, y0Accessors && y0Accessors[index]);
xValues.add(cleanedDatum.x);
updateSeriesMap(series, [...seriesKey, accessor], cleanedDatum, specId, colorValuesKey);
}, {});
} else {
const colorValues = getColorValues(datum, colorAccessors);
const colorValues = getColorValues(datum, splitSeriesAccessors);
const colorValuesKey = getColorValuesAsString(colorValues, specId);
colorsValues.set(colorValuesKey, colorValues);
const cleanedDatum = cleanDatum(datum, xAccessor, yAccessors[0], y0Accessors && y0Accessors[0]);
Expand Down Expand Up @@ -165,8 +164,8 @@ function getAccessorsValues(datum: Datum, accessors: Accessor[] = []): any[] {
/**
* Get the array of values that forms a series key
*/
function getColorValues(datum: Datum, colorAccessors: Accessor[] = [], yAccessorValue?: any): any[] {
const colorValues = getAccessorsValues(datum, colorAccessors);
function getColorValues(datum: Datum, accessors: Accessor[] = [], yAccessorValue?: any): any[] {
const colorValues = getAccessorsValues(datum, accessors);
if (yAccessorValue) {
return [...colorValues, yAccessorValue];
}
Expand Down
10 changes: 7 additions & 3 deletions src/chart_types/xy_chart/utils/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ import { Omit, RecursivePartial } from '../../../utils/commons';
import { AnnotationId, AxisId, GroupId, SpecId } from '../../../utils/ids';
import { ScaleContinuousType, ScaleType } from '../../../utils/scales/scales';
import { CurveType } from '../../../utils/curves';
import { DataSeriesColorsValues, RawDataSeriesDatum } from './series';
import { GeometryId } from '../rendering/rendering';
import { AnnotationTooltipFormatter } from '../annotations/annotation_utils';
import { DataSeriesColorsValues } from './series';

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

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

export interface SeriesScales {
Expand Down
Loading

0 comments on commit 7634f5c

Please sign in to comment.