Skip to content

Commit 2ff94b2

Browse files
authored
Merge branch 'feature/feature-anywhere' into add-layer-to-embeddable
2 parents c0b8771 + d63ef1f commit 2ff94b2

File tree

13 files changed

+610
-21
lines changed

13 files changed

+610
-21
lines changed

src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { i18n } from '@osd/i18n';
3737

3838
import { VisOptionsProps } from 'src/plugins/vis_default_editor/public';
3939
import { getNotifications } from '../services';
40-
import { VisParams } from '../vega_fn';
40+
import { VisParams } from '../expressions/vega_fn';
4141
import { VegaHelpMenu } from './vega_help_menu';
4242
import { VegaActionsMenu } from './vega_actions_menu';
4343

src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.test.js

+200
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { cloneDeep } from 'lodash';
7+
import { i18n } from '@osd/i18n';
8+
import {
9+
ExpressionFunctionDefinition,
10+
OpenSearchDashboardsDatatable,
11+
OpenSearchDashboardsDatatableColumn,
12+
} from '../../../expressions/public';
13+
import { VegaVisualizationDependencies } from '../plugin';
14+
import { VislibDimensions, VisParams } from '../../../visualizations/public';
15+
16+
type Input = OpenSearchDashboardsDatatable;
17+
type Output = Promise<string>;
18+
19+
interface Arguments {
20+
visLayers: string | null;
21+
visParams: string;
22+
dimensions: string;
23+
}
24+
25+
export type LineVegaSpecExpressionFunctionDefinition = ExpressionFunctionDefinition<
26+
'line_vega_spec',
27+
Input,
28+
Arguments,
29+
Output
30+
>;
31+
32+
// TODO: move this to the visualization plugin that has VisParams once all of these parameters have been better defined
33+
interface ValueAxis {
34+
id: string;
35+
labels: {
36+
filter: boolean;
37+
rotate: number;
38+
show: boolean;
39+
truncate: number;
40+
};
41+
name: string;
42+
position: string;
43+
scale: {
44+
mode: string;
45+
type: string;
46+
};
47+
show: true;
48+
style: any;
49+
title: {
50+
text: string;
51+
};
52+
type: string;
53+
}
54+
55+
// Get the first xaxis field as only 1 setup of X Axis will be supported and
56+
// there won't be support for split series and split chart
57+
const getXAxisId = (dimensions: any, columns: OpenSearchDashboardsDatatableColumn[]): string => {
58+
return columns.filter((column) => column.name === dimensions.x.label)[0].id;
59+
};
60+
61+
export const cleanString = (rawString: string): string => {
62+
return rawString.replaceAll('"', '');
63+
};
64+
65+
export const formatDataTable = (
66+
datatable: OpenSearchDashboardsDatatable
67+
): OpenSearchDashboardsDatatable => {
68+
datatable.columns.forEach((column) => {
69+
// clean quotation marks from names in columns
70+
column.name = cleanString(column.name);
71+
});
72+
return datatable;
73+
};
74+
75+
export const setupConfig = (visParams: VisParams) => {
76+
const legendPosition = visParams.legendPosition;
77+
return {
78+
view: {
79+
stroke: null,
80+
},
81+
concat: {
82+
spacing: 0,
83+
},
84+
legend: {
85+
orient: legendPosition,
86+
},
87+
};
88+
};
89+
90+
export const buildLayerMark = (seriesParams: {
91+
type: string;
92+
interpolate: string;
93+
lineWidth: number;
94+
showCircles: boolean;
95+
}) => {
96+
return {
97+
// Possible types are: line, area, histogram. The eligibility checker will
98+
// prevent area and histogram (though area works in vega-lite)
99+
type: seriesParams.type,
100+
// Possible types: linear, cardinal, step-after. All of these types work in vega-lite
101+
interpolate: seriesParams.interpolate,
102+
// The possible values is any number, which matches what vega-lite supports
103+
strokeWidth: seriesParams.lineWidth,
104+
// this corresponds to showing the dots in the visbuilder for each data point
105+
point: seriesParams.showCircles,
106+
};
107+
};
108+
109+
export const buildXAxis = (
110+
xAxisTitle: string,
111+
xAxisId: string,
112+
startTime: number,
113+
endTime: number,
114+
visParams: VisParams
115+
) => {
116+
return {
117+
axis: {
118+
title: xAxisTitle,
119+
grid: visParams.grid.categoryLines,
120+
},
121+
field: xAxisId,
122+
// Right now, the line charts can only set the x-axis value to be a date attribute, so
123+
// this should always be of type temporal
124+
type: 'temporal',
125+
scale: {
126+
domain: [startTime, endTime],
127+
},
128+
};
129+
};
130+
131+
export const buildYAxis = (
132+
column: OpenSearchDashboardsDatatableColumn,
133+
valueAxis: ValueAxis,
134+
visParams: VisParams
135+
) => {
136+
return {
137+
axis: {
138+
title: cleanString(valueAxis.title.text) || column.name,
139+
grid: visParams.grid.valueAxis,
140+
orient: valueAxis.position,
141+
labels: valueAxis.labels.show,
142+
labelAngle: valueAxis.labels.rotate,
143+
},
144+
field: column.id,
145+
type: 'quantitative',
146+
};
147+
};
148+
149+
export const createSpecFromDatatable = (
150+
datatable: OpenSearchDashboardsDatatable,
151+
visParams: VisParams,
152+
dimensions: VislibDimensions
153+
): object => {
154+
// TODO: we can try to use VegaSpec type but it is currently very outdated, where many
155+
// of the fields and sub-fields don't have other optional params that we want for customizing.
156+
// For now, we make this more loosely-typed by just specifying it as a generic object.
157+
const spec = {} as any;
158+
159+
spec.$schema = 'https://vega.github.io/schema/vega-lite/v5.json';
160+
spec.data = {
161+
values: datatable.rows,
162+
};
163+
spec.config = setupConfig(visParams);
164+
165+
// Get the valueAxes data and generate a map to easily fetch the different valueAxes data
166+
const valueAxis = new Map();
167+
visParams?.valueAxes?.forEach((yAxis: ValueAxis) => {
168+
valueAxis.set(yAxis.id, yAxis);
169+
});
170+
171+
spec.layer = [] as any[];
172+
173+
if (datatable.rows.length > 0 && dimensions.x !== null) {
174+
const xAxisId = getXAxisId(dimensions, datatable.columns);
175+
const xAxisTitle = cleanString(dimensions.x.label);
176+
// get x-axis bounds for the chart
177+
const startTime = new Date(dimensions.x.params.bounds.min).valueOf();
178+
const endTime = new Date(dimensions.x.params.bounds.max).valueOf();
179+
let skip = 0;
180+
datatable.columns.forEach((column, index) => {
181+
// Check if it's not xAxis column data
182+
if (column.meta?.aggConfigParams?.interval !== undefined) {
183+
skip++;
184+
} else {
185+
const currentSeriesParams = visParams.seriesParams[index - skip];
186+
const currentValueAxis = valueAxis.get(currentSeriesParams.valueAxis.toString());
187+
let tooltip: Array<{ field: string; type: string; title: string }> = [];
188+
if (visParams.addTooltip) {
189+
tooltip = [
190+
{ field: xAxisId, type: 'temporal', title: xAxisTitle },
191+
{ field: column.id, type: 'quantitative', title: column.name },
192+
];
193+
}
194+
spec.layer.push({
195+
mark: buildLayerMark(currentSeriesParams),
196+
encoding: {
197+
x: buildXAxis(xAxisTitle, xAxisId, startTime, endTime, visParams),
198+
y: buildYAxis(column, currentValueAxis, visParams),
199+
tooltip,
200+
color: {
201+
// This ensures all the different metrics have their own distinct and unique color
202+
datum: column.name,
203+
},
204+
},
205+
});
206+
}
207+
});
208+
}
209+
210+
if (visParams.addTimeMarker) {
211+
spec.transform = [
212+
{
213+
calculate: 'now()',
214+
as: 'now_field',
215+
},
216+
];
217+
218+
spec.layer.push({
219+
mark: 'rule',
220+
encoding: {
221+
x: {
222+
type: 'temporal',
223+
field: 'now_field',
224+
},
225+
// The time marker on vislib is red, so keeping this consistent
226+
color: {
227+
value: 'red',
228+
},
229+
size: {
230+
value: 1,
231+
},
232+
},
233+
});
234+
}
235+
236+
if (visParams.thresholdLine.show as boolean) {
237+
const layer = {
238+
mark: {
239+
type: 'rule',
240+
color: visParams.thresholdLine.color,
241+
strokeDash: [1, 0],
242+
},
243+
encoding: {
244+
y: {
245+
datum: visParams.thresholdLine.value,
246+
},
247+
},
248+
};
249+
250+
// Can only support making a threshold line with full or dashed style, but not dot-dashed
251+
// due to vega-lite limitations
252+
if (visParams.thresholdLine.style !== 'full') {
253+
layer.mark.strokeDash = [8, 8];
254+
}
255+
256+
spec.layer.push(layer);
257+
}
258+
259+
return spec;
260+
};
261+
262+
export const createLineVegaSpecFn = (
263+
dependencies: VegaVisualizationDependencies
264+
): LineVegaSpecExpressionFunctionDefinition => ({
265+
name: 'line_vega_spec',
266+
type: 'string',
267+
inputTypes: ['opensearch_dashboards_datatable'],
268+
help: i18n.translate('visTypeVega.function.help', {
269+
defaultMessage: 'Construct line vega spec',
270+
}),
271+
args: {
272+
visLayers: {
273+
types: ['string', 'null'],
274+
default: '',
275+
help: '',
276+
},
277+
visParams: {
278+
types: ['string'],
279+
default: '""',
280+
help: '',
281+
},
282+
dimensions: {
283+
types: ['string'],
284+
default: '""',
285+
help: '',
286+
},
287+
},
288+
async fn(input, args, context) {
289+
const table = cloneDeep(input);
290+
291+
// creating initial vega spec from table
292+
const spec = createSpecFromDatatable(
293+
formatDataTable(table),
294+
JSON.parse(args.visParams),
295+
JSON.parse(args.dimensions)
296+
);
297+
return JSON.stringify(spec);
298+
},
299+
});

src/plugins/vis_type_vega/public/vega_fn.ts src/plugins/vis_type_vega/public/expressions/vega_fn.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ import {
3535
ExpressionFunctionDefinition,
3636
OpenSearchDashboardsContext,
3737
Render,
38-
} from '../../expressions/public';
39-
import { VegaVisualizationDependencies } from './plugin';
40-
import { createVegaRequestHandler } from './vega_request_handler';
41-
import { VegaInspectorAdapters } from './vega_inspector/index';
42-
import { TimeRange, Query } from '../../data/public';
43-
import { VisRenderValue } from '../../visualizations/public';
44-
import { VegaParser } from './data_model/vega_parser';
38+
} from '../../../expressions/public';
39+
import { VegaVisualizationDependencies } from '../plugin';
40+
import { createVegaRequestHandler } from '../vega_request_handler';
41+
import { VegaInspectorAdapters } from '../vega_inspector';
42+
import { TimeRange, Query } from '../../../data/public';
43+
import { VisRenderValue } from '../../../visualizations/public';
44+
import { VegaParser } from '../data_model/vega_parser';
4545

4646
type Input = OpenSearchDashboardsContext | null;
4747
type Output = Promise<Render<RenderValue>>;
@@ -52,6 +52,14 @@ interface Arguments {
5252

5353
export type VisParams = Required<Arguments>;
5454

55+
export type VegaExpressionFunctionDefinition = ExpressionFunctionDefinition<
56+
'vega',
57+
Input,
58+
Arguments,
59+
Output,
60+
ExecutionContext<unknown, VegaInspectorAdapters>
61+
>;
62+
5563
interface RenderValue extends VisRenderValue {
5664
visData: VegaParser;
5765
visType: 'vega';
@@ -60,13 +68,7 @@ interface RenderValue extends VisRenderValue {
6068

6169
export const createVegaFn = (
6270
dependencies: VegaVisualizationDependencies
63-
): ExpressionFunctionDefinition<
64-
'vega',
65-
Input,
66-
Arguments,
67-
Output,
68-
ExecutionContext<unknown, VegaInspectorAdapters>
69-
> => ({
71+
): VegaExpressionFunctionDefinition => ({
7072
name: 'vega',
7173
type: 'render',
7274
inputTypes: ['opensearch_dashboards_context', 'null'],

src/plugins/vis_type_vega/public/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ import { VegaPlugin as Plugin } from './plugin';
3535
export function plugin(initializerContext: PluginInitializerContext<ConfigSchema>) {
3636
return new Plugin(initializerContext);
3737
}
38+
39+
export { VegaExpressionFunctionDefinition } from './expressions/vega_fn';
40+
export { LineVegaSpecExpressionFunctionDefinition } from './expressions/line_vega_spec_fn';

src/plugins/vis_type_vega/public/plugin.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ import {
4343
setInjectedMetadata,
4444
} from './services';
4545

46-
import { createVegaFn } from './vega_fn';
46+
import { createVegaFn } from './expressions/vega_fn';
4747
import { createVegaTypeDefinition } from './vega_type';
4848
import { IServiceSettings } from '../../maps_legacy/public';
4949
import './index.scss';
5050
import { ConfigSchema } from '../config';
5151

5252
import { getVegaInspectorView } from './vega_inspector';
53+
import { createLineVegaSpecFn } from './expressions/line_vega_spec_fn';
5354

5455
/** @internal */
5556
export interface VegaVisualizationDependencies {
@@ -104,6 +105,7 @@ export class VegaPlugin implements Plugin<Promise<void>, void> {
104105
inspector.registerView(getVegaInspectorView({ uiSettings: core.uiSettings }));
105106

106107
expressions.registerFunction(() => createVegaFn(visualizationDependencies));
108+
expressions.registerFunction(() => createLineVegaSpecFn(visualizationDependencies));
107109

108110
visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies));
109111
}

src/plugins/vis_type_vega/public/vega_request_handler.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { SearchAPI } from './data_model/search_api';
3434
import { TimeCache } from './data_model/time_cache';
3535

3636
import { VegaVisualizationDependencies } from './plugin';
37-
import { VisParams } from './vega_fn';
37+
import { VisParams } from './expressions/vega_fn';
3838
import { getData, getInjectedMetadata } from './services';
3939
import { VegaInspectorAdapters } from './vega_inspector';
4040

0 commit comments

Comments
 (0)