Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create switch to render line charts using vega-lite #3106

4 changes: 1 addition & 3 deletions src/plugins/input_control_vis/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,7 @@ export class InputControlVisPlugin implements Plugin<void, void> {
};

expressions.registerFunction(createInputControlVisFn);
visualizations.createBaseVisualization(
createInputControlVisTypeDefinition(visualizationDependencies)
);
visualizations.createBaseVisualization(createInputControlVisTypeDefinition(visualizationDependencies));
lezzago marked this conversation as resolved.
Show resolved Hide resolved
}

public start(core: CoreStart, deps: InputControlVisPluginStartDependencies) {
Expand Down
4 changes: 1 addition & 3 deletions src/plugins/region_map/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,7 @@ export class RegionMapPlugin implements Plugin<RegionMapPluginSetup, RegionMapPl

expressions.registerFunction(createRegionMapFn);

visualizations.createBaseVisualization(
createRegionMapTypeDefinition(visualizationDependencies)
);
visualizations.createBaseVisualization(createRegionMapTypeDefinition(visualizationDependencies));

return {
config,
Expand Down
4 changes: 1 addition & 3 deletions src/plugins/vis_type_table/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ export class TableVisPlugin implements Plugin<Promise<void>, void> {
{ expressions, visualizations }: TablePluginSetupDependencies
) {
expressions.registerFunction(createTableVisFn);
visualizations.createBaseVisualization(
getTableVisTypeDefinition(core, this.initializerContext)
);
visualizations.createBaseVisualization(getTableVisTypeDefinition(core, this.initializerContext));
}

public start(
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/vis_type_vega/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ import { VegaPlugin as Plugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext<ConfigSchema>) {
return new Plugin(initializerContext);
}

export { VegaExpressionFunctionDefinition } from './vega_fn';
export { VegaSpecExpressionFunctionDefinition } from './line_vega_spec_expression';
241 changes: 241 additions & 0 deletions src/plugins/vis_type_vega/public/line_vega_spec_expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/*
lezzago marked this conversation as resolved.
Show resolved Hide resolved
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Any modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

/*
* 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.
*/
lezzago marked this conversation as resolved.
Show resolved Hide resolved

import { cloneDeep } from 'lodash';
import { i18n } from '@osd/i18n';
import {
ExpressionFunctionDefinition,
OpenSearchDashboardsDatatable,
} from '../../expressions/public';
import { VegaVisualizationDependencies } from './plugin';

type Input = OpenSearchDashboardsDatatable;
type Output = Promise<string>;

interface Arguments {
visLayers: string | null;
visParams: string;
dimensions: string;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about keeping the arguments here the same as the vislib expression (or just add visLayers). Because visConfig already contains both the dimensions and visParams, and the visType can be used for selecting the type-specific mapping function (as Tyler suggests below).


export type VegaSpecExpressionFunctionDefinition = ExpressionFunctionDefinition<
'vega_spec',
lezzago marked this conversation as resolved.
Show resolved Hide resolved
Input,
Arguments,
Output
>;

const createSpecFromDatatable = (
datatable: OpenSearchDashboardsDatatable,
visParams: string,
dimensionsString: string
): object => {
// TODO: we can try to use VegaSpec type but it is currently very outdated, where many
// of the fields and sub-fields don't have other optional params that we want for customizing.
// For now, we make this more loosely-typed by just specifying it as a generic object.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make an issue to track this? It may be an issue I pick up and prioritize, because it will be a lot easier to make specs we're confident in if we can leverage typescript to help us.

const spec = {} as any;
const xAxis = datatable.columns[0];
lezzago marked this conversation as resolved.
Show resolved Hide resolved

const parseParams = JSON.parse(visParams);
const dimensions = JSON.parse(dimensionsString);
lezzago marked this conversation as resolved.
Show resolved Hide resolved
const legendPosition = parseParams.legendPosition;

// Get time range for the data in case there is only data for a small range so it will show the full time range
lezzago marked this conversation as resolved.
Show resolved Hide resolved
const startTime = {};
const xAxisId = xAxis.id.toString();
// @ts-ignore
startTime[xAxisId] = new Date(dimensions.x.params.bounds.min).valueOf();
const endTime = {};
// @ts-ignore
endTime[xAxisId] = new Date(dimensions.x.params.bounds.max).valueOf();
const updatedTable = datatable.rows.concat([startTime, endTime]);

// TODO: update this to v5 when available
spec.$schema = 'https://vega.github.io/schema/vega-lite/v4.json';
lezzago marked this conversation as resolved.
Show resolved Hide resolved
spec.data = {
values: updatedTable,
};
spec.config = {
view: {
stroke: null,
},
concat: {
spacing: 0,
},
// the circle timeline representing annotations
circle: {
color: 'blue',
},
// the vertical line when user hovers over an annotation circle
rule: {
color: 'red',
},
legend: {
orient: legendPosition,
},
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should extract some of these static values elsewhere.


// assuming the first column in the datatable represents the x-axis / the time-related field.
// need to confirm if that's always the case or not
spec.layer = [] as any[];

const valueAxis = {};
parseParams.valueAxes.forEach((yAxis: { id: { toString: () => string | number } }) => {
lezzago marked this conversation as resolved.
Show resolved Hide resolved
// @ts-ignore
valueAxis[yAxis.id.toString()] = yAxis;
});

if (datatable.rows.length > 0) {
datatable.columns.forEach((column, index) => {
if (index !== 0) {
const currentValueAxis =
// @ts-ignore
valueAxis[parseParams.seriesParams[index - 1].valueAxis.toString()];
let tooltip: Array<{ field: string; type: string; title: string }> = [];
if (parseParams.addTooltip) {
tooltip = [
{ field: xAxis.id, type: 'temporal', title: xAxis.name },
{ field: column.id, type: 'quantitative', title: column.name },
];
}
spec.layer.push({
lezzago marked this conversation as resolved.
Show resolved Hide resolved
mark: {
type: parseParams.seriesParams[index - 1].type,
interpolate: parseParams.seriesParams[index - 1].interpolate,
strokeWidth: parseParams.seriesParams[index - 1].lineWidth,
point: parseParams.seriesParams[index - 1].showCircles,
},
encoding: {
x: {
axis: {
title: xAxis.name,
grid: parseParams.grid.categoryLines,
},
field: xAxis.id,
type: 'temporal',
},
y: {
axis: {
title: currentValueAxis.title.text || column.name,
grid: parseParams.grid.valueAxis !== '',
orient: currentValueAxis.position,
labels: currentValueAxis.labels.show,
labelAngle: currentValueAxis.labels.rotate,
},
field: column.id,
type: 'quantitative',
},
tooltip,
color: {
datum: column.name,
},
},
});
}
});
}

if (parseParams.addTimeMarker) {
spec.transform = [
{
calculate: 'now()',
as: 'now_field',
},
];

spec.layer.push({
mark: 'rule',
encoding: {
x: {
type: 'temporal',
field: 'now_field',
},
color: {
value: 'red',
},
size: {
value: 1,
},
},
});
}

if (parseParams.thresholdLine.show as boolean) {
spec.layer.push({
mark: {
type: 'rule',
color: parseParams.thresholdLine.color,
},
encoding: {
y: {
datum: parseParams.thresholdLine.value,
},
},
});
}

return spec;
};

export const createVegaSpecFn = (
dependencies: VegaVisualizationDependencies
): VegaSpecExpressionFunctionDefinition => ({
name: 'vega_spec',
type: 'string',
inputTypes: ['opensearch_dashboards_datatable'],
help: i18n.translate('visTypeVega.function.help', {
defaultMessage: 'Construct vega spec',
}),
args: {
visLayers: {
types: ['string', 'null'],
default: '',
help: '',
},
visParams: {
types: ['string'],
default: '""',
help: '',
},
dimensions: {
types: ['string'],
default: '""',
help: '',
},
},
async fn(input, args, context) {
const table = cloneDeep(input);

// creating initial vega spec from table
const spec = createSpecFromDatatable(table, args.visParams, args.dimensions);

return JSON.stringify(spec);
},
});
2 changes: 2 additions & 0 deletions src/plugins/vis_type_vega/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import './index.scss';
import { ConfigSchema } from '../config';

import { getVegaInspectorView } from './vega_inspector';
import { createVegaSpecFn } from './line_vega_spec_expression';

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

expressions.registerFunction(() => createVegaFn(visualizationDependencies));
expressions.registerFunction(() => createVegaSpecFn(visualizationDependencies));

visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies));
}
Expand Down
16 changes: 9 additions & 7 deletions src/plugins/vis_type_vega/public/vega_fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ interface Arguments {

export type VisParams = Required<Arguments>;

export type VegaExpressionFunctionDefinition = ExpressionFunctionDefinition<
'vega',
Input,
Arguments,
Output,
ExecutionContext<unknown, VegaInspectorAdapters>
>;

interface RenderValue extends VisRenderValue {
visData: VegaParser;
visType: 'vega';
Expand All @@ -60,13 +68,7 @@ interface RenderValue extends VisRenderValue {

export const createVegaFn = (
dependencies: VegaVisualizationDependencies
): ExpressionFunctionDefinition<
'vega',
Input,
Arguments,
Output,
ExecutionContext<unknown, VegaInspectorAdapters>
> => ({
): VegaExpressionFunctionDefinition => ({
name: 'vega',
type: 'render',
inputTypes: ['opensearch_dashboards_context', 'null'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ export class VegaBaseView {
* Set global debug variable to simplify vega debugging in console. Show info message first time
*/
setDebugValues(view, spec, vlspec) {
this._parser.searchAPI.inspectorAdapters?.vega.bindInspectValues({
this._parser.searchAPI.inspectorAdapters?.vega?.bindInspectValues({
lezzago marked this conversation as resolved.
Show resolved Hide resolved
view,
spec: vlspec || spec,
});
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/vis_type_vislib/opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "opensearchDashboards",
"server": true,
"ui": true,
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "opensearchDashboardsLegacy"],
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "opensearchDashboardsLegacy", "visTypeVega"],
lezzago marked this conversation as resolved.
Show resolved Hide resolved
"optionalPlugins": ["visTypeXy"],
"requiredBundles": ["opensearchDashboardsUtils", "visDefaultEditor"]
}
2 changes: 2 additions & 0 deletions src/plugins/vis_type_vislib/public/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { createVislibVisController } from './vis_controller';
import { VisTypeVislibDependencies } from './plugin';
import { Rotates } from '../../charts/public';
import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public';
import { toExpressionAst } from './line_to_expression';

export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'line',
Expand All @@ -58,6 +59,7 @@ export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) =>
description: i18n.translate('visTypeVislib.line.lineDescription', {
defaultMessage: 'Emphasize trends',
}),
toExpressionAst,
lezzago marked this conversation as resolved.
Show resolved Hide resolved
visualization: createVislibVisController(deps),
getSupportedTriggers: () => {
return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
Expand Down
Loading