Skip to content

Commit

Permalink
feat(style): point style overrides (#385)
Browse files Browse the repository at this point in the history
Allow overrides for point styles on area and line charts per datum
  • Loading branch information
nickofthyme authored Oct 1, 2019
1 parent 602edce commit 0f587d0
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 91 deletions.
9 changes: 1 addition & 8 deletions src/chart_types/xy_chart/domains/y_domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,7 @@ export type YDomain = BaseDomain & {
};
export type YBasicSeriesSpec = Pick<
BasicSeriesSpec,
| 'id'
| 'seriesType'
| 'yScaleType'
| 'groupId'
| 'stackAccessors'
| 'yScaleToDataExtent'
| 'styleAccessor'
| 'useDefaultGroupDomain'
'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'useDefaultGroupDomain'
> & { stackAsPercentage?: boolean };

interface GroupSpecs {
Expand Down
76 changes: 63 additions & 13 deletions src/chart_types/xy_chart/rendering/rendering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
getGeometryStyle,
isPointOnGeometry,
PointGeometry,
getStyleOverrides,
getBarStyleOverrides,
GeometryId,
getPointStyleOverrides,
} from './rendering';
import { BarSeriesStyle, SharedGeometryStyle } from '../../../utils/themes/theme';
import { BarSeriesStyle, SharedGeometryStyle, PointStyle } from '../../../utils/themes/theme';
import { DataSeriesDatum } from '../utils/series';
import { RecursivePartial, mergePartial } from '../../../utils/commons';

Expand Down Expand Up @@ -169,7 +170,7 @@ describe('Rendering utils', () => {
expect(noHover).toBe(sharedThemeStyle.highlighted);
});

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

const sampleSeriesStyle: BarSeriesStyle = {
Expand Down Expand Up @@ -205,29 +206,29 @@ describe('Rendering utils', () => {
mockAccessor = jest.fn();
});

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

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

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

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

it('should call styleAccessor with datum and geometryId', () => {
getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
it('should call barStyleAccessor with datum and geometryId', () => {
getBarStyleOverrides(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 styleOverrides = getBarStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
const expectedStyles: BarSeriesStyle = {
...sampleSeriesStyle,
rect: {
Expand All @@ -240,7 +241,7 @@ describe('Rendering utils', () => {

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

expect(styleOverrides).not.toBe(sampleSeriesStyle);
});
Expand All @@ -255,7 +256,7 @@ describe('Rendering utils', () => {
},
};
mockAccessor.mockReturnValue(partialStyle);
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
const styleOverrides = getBarStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
const expectedStyles = mergePartial(sampleSeriesStyle, partialStyle, {
mergeOptionalPartialValues: true,
});
Expand All @@ -269,9 +270,58 @@ describe('Rendering utils', () => {
fill: 'blue',
},
});
const styleOverrides = getStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);
const styleOverrides = getBarStyleOverrides(datum, geometryId, sampleSeriesStyle, mockAccessor);

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

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

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 undefined if no pointStyleAccessor is passed', () => {
const styleOverrides = getPointStyleOverrides(datum, geometryId);

expect(styleOverrides).toBeUndefined();
});

it('should return undefined if pointStyleAccessor returns null', () => {
mockAccessor.mockReturnValue(null);
const styleOverrides = getPointStyleOverrides(datum, geometryId, mockAccessor);

expect(styleOverrides).toBeUndefined();
});

it('should call pointStyleAccessor with datum and geometryId', () => {
getPointStyleOverrides(datum, geometryId, mockAccessor);

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

it('should return seriesStyle with updated stroke color', () => {
const stroke = 'blue';
mockAccessor.mockReturnValue(stroke);
const styleOverrides = getPointStyleOverrides(datum, geometryId, mockAccessor);
const expectedStyles: Partial<PointStyle> = {
stroke,
};
expect(styleOverrides).toEqual(expectedStyles);
});
});
});
48 changes: 39 additions & 9 deletions src/chart_types/xy_chart/rendering/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { CurveType, getCurveFactory } from '../../../utils/curves';
import { LegendItem } from '../legend/legend';
import { DataSeriesDatum } from '../utils/series';
import { belongsToDataSeries } from '../utils/series_utils';
import { DisplayValueSpec, StyleAccessor } from '../utils/specs';
import { DisplayValueSpec, BarStyleAccessor, PointStyleAccessor } from '../utils/specs';
import { mergePartial } from '../../../utils/commons';

export interface GeometryId {
Expand Down Expand Up @@ -54,6 +54,7 @@ export interface PointGeometry {
};
geometryId: GeometryId;
value: GeometryValue;
styleOverrides?: Partial<PointStyle>;
}
export interface BarGeometry {
x: number;
Expand Down Expand Up @@ -121,11 +122,31 @@ export function mutableIndexedGeometryMapUpsert(
}
}

export function getStyleOverrides(
export function getPointStyleOverrides(
datum: DataSeriesDatum,
geometryId: GeometryId,
pointStyleAccessor?: PointStyleAccessor,
): Partial<PointStyle> | undefined {
const styleOverride = pointStyleAccessor && pointStyleAccessor(datum, geometryId);

if (!styleOverride) {
return;
}

if (typeof styleOverride === 'string') {
return {
stroke: styleOverride,
};
}

return styleOverride;
}

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

Expand Down Expand Up @@ -157,6 +178,7 @@ export function renderPoints(
specId: SpecId,
hasY0Accessors: boolean,
seriesKey: any[],
styleAccessor?: PointStyleAccessor,
): {
pointGeometries: PointGeometry[];
indexedGeometries: Map<any, IndexedGeometry[]>;
Expand Down Expand Up @@ -191,6 +213,11 @@ export function renderPoints(
y = yScale.scale(yDatum);
}
const originalY = hasY0Accessors && index === 0 ? datum.initialY0 : datum.initialY1;
const geometryId = {
specId,
seriesKey,
};
const styleOverrides = getPointStyleOverrides(datum, geometryId, styleAccessor);
const pointGeometry: PointGeometry = {
radius,
x,
Expand All @@ -205,10 +232,8 @@ export function renderPoints(
x: shift,
y: 0,
},
geometryId: {
specId,
seriesKey,
},
geometryId,
styleOverrides,
};
mutableIndexedGeometryMapUpsert(indexedGeometries, datum.x, pointGeometry);
// use the geometry only if the yDatum in contained in the current yScale domain
Expand Down Expand Up @@ -236,7 +261,7 @@ export function renderBars(
seriesKey: any[],
sharedSeriesStyle: BarSeriesStyle,
displayValueSettings?: DisplayValueSpec,
styleAccessor?: StyleAccessor,
styleAccessor?: BarStyleAccessor,
): {
barGeometries: BarGeometry[];
indexedGeometries: Map<any, IndexedGeometry[]>;
Expand Down Expand Up @@ -321,7 +346,7 @@ export function renderBars(
seriesKey,
};

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

const barGeometry: BarGeometry = {
displayValue,
Expand Down Expand Up @@ -362,6 +387,7 @@ export function renderLine(
seriesKey: any[],
xScaleOffset: number,
seriesStyle: LineSeriesStyle,
pointStyleAccessor?: PointStyleAccessor,
): {
lineGeometry: LineGeometry;
indexedGeometries: Map<any, IndexedGeometry[]>;
Expand All @@ -387,7 +413,9 @@ export function renderLine(
specId,
hasY0Accessors,
seriesKey,
pointStyleAccessor,
);

const lineGeometry = {
line: pathGenerator(dataset) || '',
points: pointGeometries,
Expand Down Expand Up @@ -422,6 +450,7 @@ export function renderArea(
xScaleOffset: number,
seriesStyle: AreaSeriesStyle,
isStacked: boolean = false,
pointStyleAccessor?: PointStyleAccessor,
): {
areaGeometry: AreaGeometry;
indexedGeometries: Map<any, IndexedGeometry[]>;
Expand Down Expand Up @@ -464,6 +493,7 @@ export function renderArea(
specId,
hasY0Accessors,
seriesKey,
pointStyleAccessor,
);

const areaGeometry = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,6 @@ function mouseOverTestSuite(scaleType: ScaleType) {

store.annotationSpecs.set(rectAnnotationSpec.annotationId, rectAnnotationSpec);
store.annotationDimensions.set(rectAnnotationSpec.annotationId, annotationDimensions);
debugger;
// isHighlighted false, chart tooltip true; should show annotationTooltip only
store.setCursorPosition(chartLeft + 51, chartTop + 1);
expect(store.isTooltipVisible.get()).toBe(false);
Expand Down
2 changes: 2 additions & 0 deletions src/chart_types/xy_chart/store/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ export function renderGeometries(
ds.key,
xScaleOffset,
lineSeriesStyle,
spec.pointStyleAccessor,
);
lineGeometriesIndex = mergeGeometriesIndexes(lineGeometriesIndex, renderedLines.indexedGeometries);
lines.push(renderedLines.lineGeometry);
Expand All @@ -504,6 +505,7 @@ export function renderGeometries(
xScaleOffset,
areaSeriesStyle,
isStacked,
spec.pointStyleAccessor,
);
areaGeometriesIndex = mergeGeometriesIndexes(areaGeometriesIndex, renderedAreas.indexedGeometries);
areas.push(renderedAreas.areaGeometry);
Expand Down
37 changes: 33 additions & 4 deletions src/chart_types/xy_chart/utils/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
LineSeriesStyle,
RectAnnotationStyle,
BarSeriesStyle,
PointStyle,
} from '../../../utils/themes/theme';
import { Accessor } from '../../../utils/accessor';
import { Omit, RecursivePartial } from '../../../utils/commons';
Expand All @@ -19,8 +20,26 @@ 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;
export type BarStyleOverride = RecursivePartial<BarSeriesStyle> | Color | null;
export type PointStyleOverride = RecursivePartial<PointStyle> | Color | null;
/**
* Override for bar styles per datum
*
* Return types:
* - `Color`: Color value as a `string` will set the bar `fill` to that color
* - `RecursivePartial<BarSeriesStyle>`: Style values to be merged with base bar styles
* - `null`: Keep existing bar style
*/
export type BarStyleAccessor = (datum: RawDataSeriesDatum, geometryId: GeometryId) => BarStyleOverride;
/**
* Override for bar styles per datum
*
* Return types:
* - `Color`: Color value as a `string` will set the point `stroke` to that color
* - `RecursivePartial<PointStyle>`: Style values to be merged with base point styles
* - `null`: Keep existing point style
*/
export type PointStyleAccessor = (datum: RawDataSeriesDatum, geometryId: GeometryId) => PointStyleOverride;
export const DEFAULT_GLOBAL_ID = '__global__';

interface DomainMinInterval {
Expand Down Expand Up @@ -104,8 +123,6 @@ export interface SeriesAccessors {
splitSeriesAccessors?: Accessor[];
/** An array of fields thats indicates the stack membership */
stackAccessors?: Accessor[];
/** An optional functional accessor to return custom datum color or style */
styleAccessor?: StyleAccessor;
}

export interface SeriesScales {
Expand Down Expand Up @@ -148,6 +165,10 @@ export type BarSeriesSpec = BasicSeriesSpec & {
* Stack each series in percentage for each point.
*/
stackAsPercentage?: boolean;
/**
* An optional functional accessor to return custom color or style for bar datum
*/
styleAccessor?: BarStyleAccessor;
};

/**
Expand All @@ -167,6 +188,10 @@ export type LineSeriesSpec = BasicSeriesSpec &
seriesType: 'line';
curve?: CurveType;
lineSeriesStyle?: RecursivePartial<LineSeriesStyle>;
/**
* An optional functional accessor to return custom color or style for point datum
*/
pointStyleAccessor?: PointStyleAccessor;
};

/**
Expand All @@ -183,6 +208,10 @@ export type AreaSeriesSpec = BasicSeriesSpec &
* Stack each series in percentage for each point.
*/
stackAsPercentage?: boolean;
/**
* An optional functional accessor to return custom color or style for point datum
*/
pointStyleAccessor?: PointStyleAccessor;
};

interface HistogramConfig {
Expand Down
Loading

0 comments on commit 0f587d0

Please sign in to comment.