Skip to content

Commit

Permalink
Allow functional accessors (range) accessors on split series
Browse files Browse the repository at this point in the history
- refactor series filter logic to allow fn accessors
- cleanup filter logic to reuse code
- fix filters on _all buckets with no x metric
  • Loading branch information
nickofthyme committed Dec 17, 2020
1 parent 57ed86d commit 8e7b5d0
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 132 deletions.
166 changes: 109 additions & 57 deletions src/plugins/charts/public/static/utils/transform_click_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from '@elastic/charts';

import { RangeSelectContext, ValueClickContext } from '../../../../embeddable/public';
import { Datatable } from '../../../../expressions/common/expression_types/specs';
import { Datatable } from '../../../../expressions/public';

export interface ClickTriggerEvent {
name: 'filterBucket';
Expand All @@ -39,6 +39,13 @@ export interface BrushTriggerEvent {
data: RangeSelectContext['data'];
}

type AllSeriesAccessors = Array<[accessor: Accessor | AccessorFn, value: string | number]>;

/**
* returns accessor value from string or function accessor
* @param datum
* @param accessor
*/
function getAccessorValue(datum: Datum, accessor: Accessor | AccessorFn) {
if (typeof accessor === 'function') {
return accessor(datum);
Expand All @@ -52,8 +59,12 @@ function getAccessorValue(datum: Datum, accessor: Accessor | AccessorFn) {
* difficult to match the correct column. This creates a test object to throw
* an error when the target id is accessed, thus matcing the target column.
*/
function validateFnAccessorId(id: string, accessor: AccessorFn) {
const matchedMessage = 'validateFnAccessorId matched';
function validateAccessorId(id: string, accessor: Accessor | AccessorFn) {
if (typeof accessor !== 'function') {
return id === accessor;
}

const matchedMessage = 'validateAccessorId matched';

try {
accessor({
Expand All @@ -67,58 +78,100 @@ function validateFnAccessorId(id: string, accessor: AccessorFn) {
}
}

/**
* Groups split accessors by their accessor string or function and related value
*
* @param splitAccessors
* @param splitSeriesAccessorFnMap
*/
const getAllSplitAccessors = (
splitAccessors: Map<string | number, string | number>,
splitSeriesAccessorFnMap?: Map<string | number, AccessorFn>
): Array<[accessor: Accessor | AccessorFn, value: string | number]> =>
[...splitAccessors.entries()].map(([key, value]) => [
splitSeriesAccessorFnMap?.get?.(key) ?? key,
value,
]);

/**
* Reduces matching column indexes
*
* @param xAccessor
* @param yAccessor
* @param splitAccessors
*/
const columnReducer = (
xAccessor: Accessor | AccessorFn | null,
yAccessor: Accessor | AccessorFn | null,
splitAccessors: AllSeriesAccessors
) => (acc: number[], { id }: Datatable['columns'][number], index: number): number[] => {
if (
(xAccessor !== null && validateAccessorId(id, xAccessor)) ||
(yAccessor !== null && validateAccessorId(id, yAccessor)) ||
splitAccessors.some(([accessor]) => validateAccessorId(id, accessor))
) {
acc.push(index);
}

return acc;
};

/**
* Finds matching row index for given accessors and geometry values
*
* @param geometry
* @param xAccessor
* @param yAccessor
* @param splitAccessors
*/
const rowFindPredicate = (
geometry: GeometryValue | null,
xAccessor: Accessor | AccessorFn | null,
yAccessor: Accessor | AccessorFn | null,
splitAccessors: AllSeriesAccessors
) => (row: Datatable['rows'][number]): boolean =>
(geometry === null ||
(xAccessor !== null &&
getAccessorValue(row, xAccessor) === geometry.x &&
yAccessor !== null &&
getAccessorValue(row, yAccessor) === geometry.y)) &&
[...splitAccessors].every(([accessor, value]) => getAccessorValue(row, accessor) === value);

/**
* Helper function to transform `@elastic/charts` click event into filter action event
*
* @param table
* @param xAccessor
* @param splitSeriesAccessorFnMap needed when using `splitSeriesAccessors` as `AccessorFn`
* @param negate
*/
export const getFilterFromChartClickEventFn = (
table: Datatable,
xAccessor: Accessor | AccessorFn,
splitSeriesAccessorFnMap?: Map<string | number, AccessorFn>,
negate: boolean = false
) => (points: Array<[GeometryValue, XYChartSeriesIdentifier]>): ClickTriggerEvent => {
const data: ValueClickContext['data']['data'] = [];
const seenKeys = new Set<string>();

points.forEach((point) => {
const [geometry, { yAccessor, splitAccessors }] = point;
const columnIndices = table.columns.reduce<number[]>((acc, { id }, index) => {
if (
(typeof xAccessor === 'function' && validateFnAccessorId(id, xAccessor)) ||
[xAccessor, yAccessor, ...splitAccessors.keys()].includes(id)
) {
acc.push(index);
}

return acc;
}, []);

const rowIndex = table.rows.findIndex((row) => {
return (
getAccessorValue(row, xAccessor) === geometry.x &&
row[yAccessor] === geometry.y &&
[...splitAccessors.entries()].every(([key, value]) => row[key] === value)
);
});

data.push(
...columnIndices
.map((column) => ({
table,
column,
row: rowIndex,
value: null,
}))
.filter((column) => {
// filter duplicate values when multiple geoms are highlighted
const key = `column:${column},row:${rowIndex}`;
if (seenKeys.has(key)) {
return false;
}

seenKeys.add(key);

return true;
})
const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap);
const columnIndices = table.columns.reduce<number[]>(
columnReducer(xAccessor, yAccessor, allSplitAccessors),
[]
);
const row = table.rows.findIndex(
rowFindPredicate(geometry, xAccessor, yAccessor, allSplitAccessors)
);
const value = getAccessorValue(table.rows[row], yAccessor);
const newData = columnIndices.map((column) => ({
table,
column,
row,
value,
}));

data.push(...newData);
});

return {
Expand All @@ -135,22 +188,21 @@ export const getFilterFromChartClickEventFn = (
*/
export const getFilterFromSeriesFn = (table: Datatable) => (
{ splitAccessors }: XYChartSeriesIdentifier,
splitSeriesAccessorFnMap?: Map<string | number, AccessorFn>,
negate = false
): ClickTriggerEvent => {
const data = table.columns.reduce<ValueClickContext['data']['data']>((acc, { id }, column) => {
if ([...splitAccessors.keys()].includes(id)) {
const value = splitAccessors.get(id);
const row = table.rows.findIndex((r) => r[id] === value);
acc.push({
table,
column,
row,
value,
});
}

return acc;
}, []);
const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap);
const columnIndices = table.columns.reduce<number[]>(
columnReducer(null, null, allSplitAccessors),
[]
);
const row = table.rows.findIndex(rowFindPredicate(null, null, null, allSplitAccessors));
const data: ValueClickContext['data']['data'] = columnIndices.map((column) => ({
table,
column,
row,
value: null,
}));

return {
name: 'filterBucket',
Expand All @@ -170,7 +222,7 @@ export const getBrushFromChartBrushEventFn = (
) => ({ x: selectedRange }: XYBrushArea): BrushTriggerEvent => {
const [start, end] = selectedRange ?? [0, 0];
const range: [number, number] = [start, end];
const column = table.columns.findIndex((c) => c.id === xAccessor);
const column = table.columns.findIndex(({ id }) => validateAccessorId(id, xAccessor));

return {
data: {
Expand Down
10 changes: 8 additions & 2 deletions src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { Aspects } from '../types';

import './_detailed_tooltip.scss';
import { fillEmptyValue } from '../utils/get_series_name_fn';
import { COMPLEX_SPLIT_ACCESSOR } from '../utils/accessors';

interface TooltipData {
label: string;
Expand Down Expand Up @@ -75,12 +76,17 @@ const getTooltipData = (
}

valueSeries.splitAccessors.forEach((splitValue, key) => {
const split = (aspects.series ?? []).find(({ accessor }) => accessor === key);
const split = (aspects.series ?? []).find(({ accessor }, i) => {
return accessor === key || key === `${COMPLEX_SPLIT_ACCESSOR}::${i}`;
});

if (split) {
data.push({
label: split?.title,
value: split?.formatter ? split?.formatter(splitValue) : `${splitValue}`,
value:
split?.formatter && !key.toString().startsWith(COMPLEX_SPLIT_ACCESSOR)
? split?.formatter(splitValue)
: `${splitValue}`,
});
}
});
Expand Down
90 changes: 90 additions & 0 deletions src/plugins/vis_type_xy/public/utils/accessors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { AccessorFn, Accessor } from '@elastic/charts';
import { BUCKET_TYPES } from '../../../data/public';
import { FakeParams, Aspect } from '../types';

export const COMPLEX_X_ACCESSOR = '__customXAccessor__';
export const COMPLEX_SPLIT_ACCESSOR = '__complexSplitAccessor__';

export const getXAccessor = (aspect: Aspect): Accessor | AccessorFn => {
return (
getComplexAccessor(COMPLEX_X_ACCESSOR)(aspect) ??
(() => (aspect.params as FakeParams)?.defaultValue)
);
};

const getFieldName = (fieldName: string, index?: number) => {
const indexStr = index !== undefined ? `::${index}` : '';

return `${fieldName}${indexStr}`;
};

/**
* Returns accessor function for complex accessor types
* @param aspect
*/
export const getComplexAccessor = (fieldName: string) => (
aspect: Aspect,
index?: number
): Accessor | AccessorFn | undefined => {
if (!aspect.accessor) {
return;
}

if (
!(
(aspect.aggType === BUCKET_TYPES.DATE_RANGE || aspect.aggType === BUCKET_TYPES.RANGE) &&
aspect.formatter
)
) {
return aspect.accessor;
}

const formatter = aspect.formatter;
const accessor = aspect.accessor;
const fn: AccessorFn = (d) => {
const v = d[accessor];
if (!v) {
return;
}
const f = formatter(v);
return f;
};

fn.fieldName = getFieldName(fieldName, index);

return fn;
};

export const getSplitSeriesAccessorFnMap = (
splitSeriesAccessors: Array<Accessor | AccessorFn>
): Map<string | number, AccessorFn> => {
const m = new Map<string | number, AccessorFn>();

splitSeriesAccessors.forEach((accessor, index) => {
if (typeof accessor === 'function') {
const fieldName = getFieldName(COMPLEX_SPLIT_ACCESSOR, index);
m.set(fieldName, accessor);
}
});

return m;
};
48 changes: 0 additions & 48 deletions src/plugins/vis_type_xy/public/utils/get_x_accessor.tsx

This file was deleted.

1 change: 1 addition & 0 deletions src/plugins/vis_type_xy/public/utils/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export { getLegendActions } from './get_legend_actions';
export { getSeriesNameFn } from './get_series_name_fn';
export { getXDomain, getAdjustedDomain } from './domain';
export { useColorPicker } from './use_color_picker';
export { getXAccessor } from './accessors';
Loading

0 comments on commit 8e7b5d0

Please sign in to comment.