Skip to content

Commit 8e7b5d0

Browse files
committed
Allow functional accessors (range) accessors on split series
- refactor series filter logic to allow fn accessors - cleanup filter logic to reuse code - fix filters on _all buckets with no x metric
1 parent 57ed86d commit 8e7b5d0

File tree

7 files changed

+252
-132
lines changed

7 files changed

+252
-132
lines changed

src/plugins/charts/public/static/utils/transform_click_event.ts

Lines changed: 109 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
} from '@elastic/charts';
2828

2929
import { RangeSelectContext, ValueClickContext } from '../../../../embeddable/public';
30-
import { Datatable } from '../../../../expressions/common/expression_types/specs';
30+
import { Datatable } from '../../../../expressions/public';
3131

3232
export interface ClickTriggerEvent {
3333
name: 'filterBucket';
@@ -39,6 +39,13 @@ export interface BrushTriggerEvent {
3939
data: RangeSelectContext['data'];
4040
}
4141

42+
type AllSeriesAccessors = Array<[accessor: Accessor | AccessorFn, value: string | number]>;
43+
44+
/**
45+
* returns accessor value from string or function accessor
46+
* @param datum
47+
* @param accessor
48+
*/
4249
function getAccessorValue(datum: Datum, accessor: Accessor | AccessorFn) {
4350
if (typeof accessor === 'function') {
4451
return accessor(datum);
@@ -52,8 +59,12 @@ function getAccessorValue(datum: Datum, accessor: Accessor | AccessorFn) {
5259
* difficult to match the correct column. This creates a test object to throw
5360
* an error when the target id is accessed, thus matcing the target column.
5461
*/
55-
function validateFnAccessorId(id: string, accessor: AccessorFn) {
56-
const matchedMessage = 'validateFnAccessorId matched';
62+
function validateAccessorId(id: string, accessor: Accessor | AccessorFn) {
63+
if (typeof accessor !== 'function') {
64+
return id === accessor;
65+
}
66+
67+
const matchedMessage = 'validateAccessorId matched';
5768

5869
try {
5970
accessor({
@@ -67,58 +78,100 @@ function validateFnAccessorId(id: string, accessor: AccessorFn) {
6778
}
6879
}
6980

81+
/**
82+
* Groups split accessors by their accessor string or function and related value
83+
*
84+
* @param splitAccessors
85+
* @param splitSeriesAccessorFnMap
86+
*/
87+
const getAllSplitAccessors = (
88+
splitAccessors: Map<string | number, string | number>,
89+
splitSeriesAccessorFnMap?: Map<string | number, AccessorFn>
90+
): Array<[accessor: Accessor | AccessorFn, value: string | number]> =>
91+
[...splitAccessors.entries()].map(([key, value]) => [
92+
splitSeriesAccessorFnMap?.get?.(key) ?? key,
93+
value,
94+
]);
95+
96+
/**
97+
* Reduces matching column indexes
98+
*
99+
* @param xAccessor
100+
* @param yAccessor
101+
* @param splitAccessors
102+
*/
103+
const columnReducer = (
104+
xAccessor: Accessor | AccessorFn | null,
105+
yAccessor: Accessor | AccessorFn | null,
106+
splitAccessors: AllSeriesAccessors
107+
) => (acc: number[], { id }: Datatable['columns'][number], index: number): number[] => {
108+
if (
109+
(xAccessor !== null && validateAccessorId(id, xAccessor)) ||
110+
(yAccessor !== null && validateAccessorId(id, yAccessor)) ||
111+
splitAccessors.some(([accessor]) => validateAccessorId(id, accessor))
112+
) {
113+
acc.push(index);
114+
}
115+
116+
return acc;
117+
};
118+
119+
/**
120+
* Finds matching row index for given accessors and geometry values
121+
*
122+
* @param geometry
123+
* @param xAccessor
124+
* @param yAccessor
125+
* @param splitAccessors
126+
*/
127+
const rowFindPredicate = (
128+
geometry: GeometryValue | null,
129+
xAccessor: Accessor | AccessorFn | null,
130+
yAccessor: Accessor | AccessorFn | null,
131+
splitAccessors: AllSeriesAccessors
132+
) => (row: Datatable['rows'][number]): boolean =>
133+
(geometry === null ||
134+
(xAccessor !== null &&
135+
getAccessorValue(row, xAccessor) === geometry.x &&
136+
yAccessor !== null &&
137+
getAccessorValue(row, yAccessor) === geometry.y)) &&
138+
[...splitAccessors].every(([accessor, value]) => getAccessorValue(row, accessor) === value);
139+
70140
/**
71141
* Helper function to transform `@elastic/charts` click event into filter action event
142+
*
143+
* @param table
144+
* @param xAccessor
145+
* @param splitSeriesAccessorFnMap needed when using `splitSeriesAccessors` as `AccessorFn`
146+
* @param negate
72147
*/
73148
export const getFilterFromChartClickEventFn = (
74149
table: Datatable,
75150
xAccessor: Accessor | AccessorFn,
151+
splitSeriesAccessorFnMap?: Map<string | number, AccessorFn>,
76152
negate: boolean = false
77153
) => (points: Array<[GeometryValue, XYChartSeriesIdentifier]>): ClickTriggerEvent => {
78154
const data: ValueClickContext['data']['data'] = [];
79-
const seenKeys = new Set<string>();
80155

81156
points.forEach((point) => {
82157
const [geometry, { yAccessor, splitAccessors }] = point;
83-
const columnIndices = table.columns.reduce<number[]>((acc, { id }, index) => {
84-
if (
85-
(typeof xAccessor === 'function' && validateFnAccessorId(id, xAccessor)) ||
86-
[xAccessor, yAccessor, ...splitAccessors.keys()].includes(id)
87-
) {
88-
acc.push(index);
89-
}
90-
91-
return acc;
92-
}, []);
93-
94-
const rowIndex = table.rows.findIndex((row) => {
95-
return (
96-
getAccessorValue(row, xAccessor) === geometry.x &&
97-
row[yAccessor] === geometry.y &&
98-
[...splitAccessors.entries()].every(([key, value]) => row[key] === value)
99-
);
100-
});
101-
102-
data.push(
103-
...columnIndices
104-
.map((column) => ({
105-
table,
106-
column,
107-
row: rowIndex,
108-
value: null,
109-
}))
110-
.filter((column) => {
111-
// filter duplicate values when multiple geoms are highlighted
112-
const key = `column:${column},row:${rowIndex}`;
113-
if (seenKeys.has(key)) {
114-
return false;
115-
}
116-
117-
seenKeys.add(key);
118-
119-
return true;
120-
})
158+
const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap);
159+
const columnIndices = table.columns.reduce<number[]>(
160+
columnReducer(xAccessor, yAccessor, allSplitAccessors),
161+
[]
162+
);
163+
const row = table.rows.findIndex(
164+
rowFindPredicate(geometry, xAccessor, yAccessor, allSplitAccessors)
121165
);
166+
const value = getAccessorValue(table.rows[row], yAccessor);
167+
const newData = columnIndices.map((column) => ({
168+
table,
169+
column,
170+
row,
171+
value,
172+
}));
173+
174+
data.push(...newData);
122175
});
123176

124177
return {
@@ -135,22 +188,21 @@ export const getFilterFromChartClickEventFn = (
135188
*/
136189
export const getFilterFromSeriesFn = (table: Datatable) => (
137190
{ splitAccessors }: XYChartSeriesIdentifier,
191+
splitSeriesAccessorFnMap?: Map<string | number, AccessorFn>,
138192
negate = false
139193
): ClickTriggerEvent => {
140-
const data = table.columns.reduce<ValueClickContext['data']['data']>((acc, { id }, column) => {
141-
if ([...splitAccessors.keys()].includes(id)) {
142-
const value = splitAccessors.get(id);
143-
const row = table.rows.findIndex((r) => r[id] === value);
144-
acc.push({
145-
table,
146-
column,
147-
row,
148-
value,
149-
});
150-
}
151-
152-
return acc;
153-
}, []);
194+
const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap);
195+
const columnIndices = table.columns.reduce<number[]>(
196+
columnReducer(null, null, allSplitAccessors),
197+
[]
198+
);
199+
const row = table.rows.findIndex(rowFindPredicate(null, null, null, allSplitAccessors));
200+
const data: ValueClickContext['data']['data'] = columnIndices.map((column) => ({
201+
table,
202+
column,
203+
row,
204+
value: null,
205+
}));
154206

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

175227
return {
176228
data: {

src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { Aspects } from '../types';
3333

3434
import './_detailed_tooltip.scss';
3535
import { fillEmptyValue } from '../utils/get_series_name_fn';
36+
import { COMPLEX_SPLIT_ACCESSOR } from '../utils/accessors';
3637

3738
interface TooltipData {
3839
label: string;
@@ -75,12 +76,17 @@ const getTooltipData = (
7576
}
7677

7778
valueSeries.splitAccessors.forEach((splitValue, key) => {
78-
const split = (aspects.series ?? []).find(({ accessor }) => accessor === key);
79+
const split = (aspects.series ?? []).find(({ accessor }, i) => {
80+
return accessor === key || key === `${COMPLEX_SPLIT_ACCESSOR}::${i}`;
81+
});
7982

8083
if (split) {
8184
data.push({
8285
label: split?.title,
83-
value: split?.formatter ? split?.formatter(splitValue) : `${splitValue}`,
86+
value:
87+
split?.formatter && !key.toString().startsWith(COMPLEX_SPLIT_ACCESSOR)
88+
? split?.formatter(splitValue)
89+
: `${splitValue}`,
8490
});
8591
}
8692
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { AccessorFn, Accessor } from '@elastic/charts';
21+
import { BUCKET_TYPES } from '../../../data/public';
22+
import { FakeParams, Aspect } from '../types';
23+
24+
export const COMPLEX_X_ACCESSOR = '__customXAccessor__';
25+
export const COMPLEX_SPLIT_ACCESSOR = '__complexSplitAccessor__';
26+
27+
export const getXAccessor = (aspect: Aspect): Accessor | AccessorFn => {
28+
return (
29+
getComplexAccessor(COMPLEX_X_ACCESSOR)(aspect) ??
30+
(() => (aspect.params as FakeParams)?.defaultValue)
31+
);
32+
};
33+
34+
const getFieldName = (fieldName: string, index?: number) => {
35+
const indexStr = index !== undefined ? `::${index}` : '';
36+
37+
return `${fieldName}${indexStr}`;
38+
};
39+
40+
/**
41+
* Returns accessor function for complex accessor types
42+
* @param aspect
43+
*/
44+
export const getComplexAccessor = (fieldName: string) => (
45+
aspect: Aspect,
46+
index?: number
47+
): Accessor | AccessorFn | undefined => {
48+
if (!aspect.accessor) {
49+
return;
50+
}
51+
52+
if (
53+
!(
54+
(aspect.aggType === BUCKET_TYPES.DATE_RANGE || aspect.aggType === BUCKET_TYPES.RANGE) &&
55+
aspect.formatter
56+
)
57+
) {
58+
return aspect.accessor;
59+
}
60+
61+
const formatter = aspect.formatter;
62+
const accessor = aspect.accessor;
63+
const fn: AccessorFn = (d) => {
64+
const v = d[accessor];
65+
if (!v) {
66+
return;
67+
}
68+
const f = formatter(v);
69+
return f;
70+
};
71+
72+
fn.fieldName = getFieldName(fieldName, index);
73+
74+
return fn;
75+
};
76+
77+
export const getSplitSeriesAccessorFnMap = (
78+
splitSeriesAccessors: Array<Accessor | AccessorFn>
79+
): Map<string | number, AccessorFn> => {
80+
const m = new Map<string | number, AccessorFn>();
81+
82+
splitSeriesAccessors.forEach((accessor, index) => {
83+
if (typeof accessor === 'function') {
84+
const fieldName = getFieldName(COMPLEX_SPLIT_ACCESSOR, index);
85+
m.set(fieldName, accessor);
86+
}
87+
});
88+
89+
return m;
90+
};

src/plugins/vis_type_xy/public/utils/get_x_accessor.tsx

Lines changed: 0 additions & 48 deletions
This file was deleted.

src/plugins/vis_type_xy/public/utils/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ export { getLegendActions } from './get_legend_actions';
2323
export { getSeriesNameFn } from './get_series_name_fn';
2424
export { getXDomain, getAdjustedDomain } from './domain';
2525
export { useColorPicker } from './use_color_picker';
26+
export { getXAccessor } from './accessors';

0 commit comments

Comments
 (0)