Skip to content

Commit

Permalink
[Feature anywhere] Add annotation click handler (#3777)
Browse files Browse the repository at this point in the history
* added interaction handling
* added framework for invoking flyout, tooltip
* using signals to listen for events on point annotation

Signed-off-by: Amardeepsingh Siglani <[email protected]>
  • Loading branch information
amsiglan authored May 22, 2023
1 parent 2cc88f3 commit e7e4b2a
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/plugins/ui_actions/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export {
visualizeFieldTrigger,
VISUALIZE_GEO_FIELD_TRIGGER,
visualizeGeoFieldTrigger,
EXTERNAL_ACTION_TRIGGER,
externalActionTrigger,
} from './triggers';
export {
TriggerContextMapping,
Expand Down
44 changes: 44 additions & 0 deletions src/plugins/ui_actions/public/triggers/external_action_trigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.
*/

import { i18n } from '@osd/i18n';
import { Trigger } from '.';

export const EXTERNAL_ACTION_TRIGGER = 'EXTERNAL_ACTION_TRIGGER';
export const externalActionTrigger: Trigger<'EXTERNAL_ACTION_TRIGGER'> = {
id: EXTERNAL_ACTION_TRIGGER,
title: i18n.translate('uiActions.triggers.externalActionTitle', {
defaultMessage: 'Single click',
}),
description: i18n.translate('uiActions.triggers.externalActionDescription', {
defaultMessage:
'A data point click on the visualization used to trigger external action like show flyout, etc.',
}),
};
1 change: 1 addition & 0 deletions src/plugins/ui_actions/public/triggers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ export * from './value_click_trigger';
export * from './apply_filter_trigger';
export * from './visualize_field_trigger';
export * from './visualize_geo_field_trigger';
export * from './external_action_trigger';
export * from './default_trigger';
8 changes: 7 additions & 1 deletion src/plugins/vis_augmenter/public/test_constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { OpenSearchDashboardsDatatable } from '../../expressions/public';
import { VIS_LAYER_COLUMN_TYPE, VisLayerTypes, HOVER_PARAM } from './';
import { VisAnnotationType } from './vega/constants';

const TEST_X_AXIS_ID = 'test-x-axis-id';
const TEST_X_AXIS_ID_DIRTY = 'test.x.axis.id';
Expand Down Expand Up @@ -489,8 +490,12 @@ const TEST_EVENTS_LAYER_SINGLE_VIS_LAYER = {
color: 'red',
filled: true,
opacity: 1,
style: [`${VisAnnotationType.POINT_IN_TIME_ANNOTATION}`],
},
transform: [{ filter: `datum['${TEST_PLUGIN_RESOURCE_ID}'] > 0` }],
transform: [
{ filter: `datum['${TEST_PLUGIN_RESOURCE_ID}'] > 0` },
{ calculate: `'${VisAnnotationType.POINT_IN_TIME_ANNOTATION}'`, as: 'annotationType' },
],
params: [{ name: HOVER_PARAM, select: { type: 'point', on: 'mouseover' } }],
encoding: {
x: {
Expand Down Expand Up @@ -536,6 +541,7 @@ const TEST_EVENTS_LAYER_MULTIPLE_VIS_LAYERS = {
{
filter: `datum['${TEST_PLUGIN_RESOURCE_ID}'] > 0 || datum['${TEST_PLUGIN_RESOURCE_ID_2}'] > 0`,
},
{ calculate: `'${VisAnnotationType.POINT_IN_TIME_ANNOTATION}'`, as: 'annotationType' },
],
};

Expand Down
8 changes: 8 additions & 0 deletions src/plugins/vis_augmenter/public/vega/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export enum VisAnnotationType {
POINT_IN_TIME_ANNOTATION = 'POINT_IN_TIME_ANNOTATION',
}
38 changes: 37 additions & 1 deletion src/plugins/vis_augmenter/public/vega/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import moment from 'moment';
import { cloneDeep, isEmpty, get } from 'lodash';
import { Item } from 'vega';
import {
OpenSearchDashboardsDatatable,
OpenSearchDashboardsDatatableColumn,
Expand All @@ -24,6 +25,7 @@ import {
VisLayers,
VisLayerTypes,
} from '../';
import { VisAnnotationType } from './constants';

// Given any visLayers, create a map to indicate which VisLayer types are present.
// Convert to an array since ES6 Maps cannot be stringified.
Expand All @@ -48,6 +50,30 @@ export const enableVisLayersInSpecConfig = (spec: object, visLayers: VisLayers):
};
};

/**
* Adds the signals which vega will use to trigger required events on the point in time annotation marks
*/
export const addVisEventSignalsToSpecConfig = (spec: object) => {
const config = get(spec, 'config', { kibana: {} });
const signals = {
...(config.kibana.signals || {}),
[`${VisAnnotationType.POINT_IN_TIME_ANNOTATION}`]: [
{
name: 'PointInTimeAnnotationVisEvent',
on: [{ events: 'click', update: 'opensearchDashboardsVisEventTriggered(event, datum)' }],
},
],
};

return {
...config,
kibana: {
...config.kibana,
signals,
},
};
};

// Get the first xaxis field as only 1 setup of X Axis will be supported and
// there won't be support for split series and split chart
export const getXAxisId = (
Expand Down Expand Up @@ -313,8 +339,14 @@ export const addPointInTimeEventsLayersToSpec = (
color: EVENT_COLOR,
filled: true,
opacity: 1,
// This style is only used to locate this mark when trying to add signals in the compiled vega spec.
// @see @method vega_parser._compileVegaLite
style: [`${VisAnnotationType.POINT_IN_TIME_ANNOTATION}`],
},
transform: [{ filter: generateVisLayerFilterString(visLayerColumnIds) }],
transform: [
{ filter: generateVisLayerFilterString(visLayerColumnIds) },
{ calculate: `'${VisAnnotationType.POINT_IN_TIME_ANNOTATION}'`, as: 'annotationType' },
],
params: [{ name: HOVER_PARAM, select: { type: 'point', on: 'mouseover' } }],
encoding: {
x: {
Expand All @@ -340,3 +372,7 @@ export const addPointInTimeEventsLayersToSpec = (

return newSpec;
};

export const isPointInTimeAnnotation = (item?: Item | null) => {
return item?.datum?.annotationType === VisAnnotationType.POINT_IN_TIME_ANNOTATION;
};
2 changes: 2 additions & 0 deletions src/plugins/vis_type_vega/public/data_model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { SearchResponse, SearchParams } from 'elasticsearch';
import { Filter } from 'src/plugins/data/public';
import { DslQuery } from 'src/plugins/data/common';
import { VisLayerTypes } from 'src/plugins/vis_augmenter/public';
import { Signal } from 'vega';
import { OpenSearchQueryParser } from './opensearch_query_parser';
import { EmsFileParser } from './ems_file_parser';
import { UrlParser } from './url_parser';
Expand Down Expand Up @@ -115,6 +116,7 @@ export interface OpenSearchDashboards {
type: string;
renderer: Renderer;
visibleVisLayers?: Map<VisLayerTypes, boolean>;
signals?: { [markId: string]: Signal[] };
}

export interface VegaSpec {
Expand Down
47 changes: 47 additions & 0 deletions src/plugins/vis_type_vega/public/data_model/vega_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { euiPaletteColorBlind } from '@elastic/eui';
import { euiThemeVars } from '@osd/ui-shared-deps/theme';
import { i18n } from '@osd/i18n';
// @ts-ignore
import { Signal } from 'vega';
import { vega, vegaLite } from '../lib/vega';
import { OpenSearchQueryParser } from './opensearch_query_parser';
import { Utils } from './utils';
Expand Down Expand Up @@ -323,6 +324,52 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never
delete this.spec.autosize;
}
}

if (this._config?.signals) {
Object.entries(this._config?.signals).forEach(([markId, signals]: [string, any]) => {
const mark = this.getMarkWithStyle(this.spec.marks, markId);

if (mark) {
signals.forEach((signal: Signal) => {
signal.on?.forEach((eventObj) => {
// We are prepending mark name here so that the signals only listens to the events on
// the elements related to this mark
eventObj.events = `@${mark.name}:${eventObj.events}`;
});
});
this.spec.signals = (this.spec.signals || []).concat(signals);
}
});
}
}

/**
* This method recursively looks for a mark that includes the given style.
* Returns undefined if it doesn't find it.
*/
getMarkWithStyle(marks: any[], style: string): any {
if (!marks) {
return undefined;
}

if (Array.isArray(marks)) {
const markWithStyle = marks.find((mark) => {
return mark.style?.includes(style);
});

if (markWithStyle) {
return markWithStyle;
}

for (let i = 0; i < marks.length; i++) {
const res = this.getMarkWithStyle(marks[i].marks, style);
if (res) {
return res;
}
}

return undefined;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
addPointInTimeEventsLayersToTable,
addPointInTimeEventsLayersToSpec,
enableVisLayersInSpecConfig,
addVisEventSignalsToSpecConfig,
} from '../../../vis_augmenter/public';
import { formatDatatable, createSpecFromDatatable } from './helpers';
import { VegaVisualizationDependencies } from '../plugin';
Expand Down Expand Up @@ -85,6 +86,7 @@ export const createLineVegaSpecFn = (
if (!isEmpty(pointInTimeEventsVisLayers) && dimensions.x !== null) {
spec = addPointInTimeEventsLayersToSpec(table, dimensions, spec);
spec.config = enableVisLayersInSpecConfig(spec, pointInTimeEventsVisLayers);
spec.config = addVisEventSignalsToSpecConfig(spec);
}
return JSON.stringify(spec);
},
Expand Down
14 changes: 14 additions & 0 deletions src/plugins/vis_type_vega/public/vega_view/vega_base_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const vegaFunctions = {
opensearchDashboardsRemoveFilter: 'removeFilterHandler',
opensearchDashboardsRemoveAllFilters: 'removeAllFiltersHandler',
opensearchDashboardsSetTimeFilter: 'setTimeFilterHandler',
opensearchDashboardsVisEventTriggered: 'triggerExternalActionHandler',
};

for (const funcName of Object.keys(vegaFunctions)) {
Expand All @@ -76,6 +77,7 @@ export class VegaBaseView {
this._serviceSettings = opts.serviceSettings;
this._filterManager = opts.filterManager;
this._applyFilter = opts.applyFilter;
this._triggerExternalAction = opts.externalAction;
this._timefilter = opts.timefilter;
this._view = null;
this._vegaViewConfig = null;
Expand Down Expand Up @@ -343,6 +345,18 @@ export class VegaBaseView {
this._applyFilter({ filters: [filter] });
}

/**
* This method is triggered using signal expression in vega-spec via @see opensearchDashboardsVisEventTriggered
* @param {import('vega').ScenegraphEvent} event Event triggered by the underlying vega visualization.
* @param {import('vega').Item} datum Data associated with the element on which the event was triggered.
*/
triggerExternalActionHandler(event, datum) {
this._triggerExternalAction({
event,
item: datum,
});
}

/**
* @param {object} query Query DSL snippet, as used in the query DSL editor
* @param {string} [index] as defined in OpenSearch Dashboards, or default if missing
Expand Down
1 change: 1 addition & 0 deletions src/plugins/vis_type_vega/public/vega_visualization.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const createVegaVisualization = ({ getServiceSettings }) =>
serviceSettings,
filterManager,
timefilter,
externalAction: this._vis.API.events.externalAction,
};

if (vegaParser.useMap) {
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/visualizations/public/embeddable/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,19 @@ import {
APPLY_FILTER_TRIGGER,
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
EXTERNAL_ACTION_TRIGGER,
} from '../../../ui_actions/public';

export interface VisEventToTrigger {
['applyFilter']: typeof APPLY_FILTER_TRIGGER;
['brush']: typeof SELECT_RANGE_TRIGGER;
['filter']: typeof VALUE_CLICK_TRIGGER;
['externalAction']: typeof EXTERNAL_ACTION_TRIGGER;
}

export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = {
applyFilter: APPLY_FILTER_TRIGGER,
brush: SELECT_RANGE_TRIGGER,
filter: VALUE_CLICK_TRIGGER,
externalAction: EXTERNAL_ACTION_TRIGGER,
};
5 changes: 5 additions & 0 deletions src/plugins/visualizations/public/expressions/vis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface ExprVisAPIEvents {
filter: (data: any) => void;
brush: (data: any) => void;
applyFilter: (data: any) => void;
externalAction: (data: any) => void;
}

export interface ExprVisAPI {
Expand Down Expand Up @@ -99,6 +100,10 @@ export class ExprVis extends EventEmitter {
if (!this.eventsSubject) return;
this.eventsSubject.next({ name: 'applyFilter', data });
},
externalAction: (data: any) => {
if (!this.eventsSubject) return;
this.eventsSubject.next({ name: 'externalAction', data });
},
},
};
}
Expand Down

0 comments on commit e7e4b2a

Please sign in to comment.