From 8a2cc697c786446ef1b5573b6490ad105a81fa09 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 30 Mar 2023 14:14:27 -0700 Subject: [PATCH] Dynamically update vega spec to show `VisLayer`s (#3145) * Dynamically update vega spec to show VisLayers Signed-off-by: Tyler Ohlsen * Clean up changes from rebase Signed-off-by: Tyler Ohlsen * Create and set augment vis loader in visualization start() Signed-off-by: Tyler Ohlsen * Fix test; add placeholder scheme Signed-off-by: Tyler Ohlsen * Fix null x axis NPE Signed-off-by: Tyler Ohlsen * remove adding timestamps that are out of bounds; add test cases for it Signed-off-by: Tyler Ohlsen * Remove unnecessary if block Signed-off-by: Tyler Ohlsen * Export PluginResource in vis_augmenter/public Signed-off-by: Tyler Ohlsen * Add comments Signed-off-by: Tyler Ohlsen * Fix conflict from rebasing Signed-off-by: Tyler Ohlsen * Make vega parser flags more fine-grained Signed-off-by: Tyler Ohlsen * Clean up few tests; simplify helper fn Signed-off-by: Tyler Ohlsen * Remove visTypeVega from visAugmenter reqd plugins; other minor cleanup Signed-off-by: Tyler Ohlsen --------- Signed-off-by: Tyler Ohlsen --- src/plugins/vis_augmenter/README.md | 2 +- src/plugins/vis_augmenter/public/constants.ts | 13 + src/plugins/vis_augmenter/public/index.ts | 19 +- .../vis_augmenter/public/test_constants.ts | 561 ++++++++++++++++++ .../vis_augmenter/public/vega/README.md | 1 + .../vis_augmenter/public/vega/helpers.test.ts | 444 ++++++++++++++ .../vis_augmenter/public/vega/helpers.ts | 342 +++++++++++ .../vis_augmenter/public/vega/index.ts | 6 + .../vis_type_vega/opensearch_dashboards.json | 9 +- .../vega_visualization.test.js.snap | 6 +- .../public/data_model/vega_parser.ts | 21 +- .../public/expressions/__mocks__/helpers.ts | 21 + .../public/expressions/__mocks__/index.ts | 6 + .../__snapshots__/helpers.test.js.snap | 7 + .../public/expressions/helpers.test.js | 183 ++++++ .../public/expressions/helpers.ts | 238 ++++++++ .../vis_type_vega/public/expressions/index.ts | 7 + .../expressions/line_vega_spec_fn.test.js | 200 ------- .../public/expressions/line_vega_spec_fn.ts | 272 +-------- src/plugins/vis_type_vega/public/index.ts | 3 +- .../public/vega_view/vega_base_view.js | 16 + .../public/vega_view/vega_view.js | 4 + .../public/line_to_expression.ts | 2 +- src/plugins/visualizations/public/plugin.ts | 14 +- 24 files changed, 1933 insertions(+), 464 deletions(-) create mode 100644 src/plugins/vis_augmenter/public/constants.ts create mode 100644 src/plugins/vis_augmenter/public/test_constants.ts create mode 100644 src/plugins/vis_augmenter/public/vega/README.md create mode 100644 src/plugins/vis_augmenter/public/vega/helpers.test.ts create mode 100644 src/plugins/vis_augmenter/public/vega/helpers.ts create mode 100644 src/plugins/vis_augmenter/public/vega/index.ts create mode 100644 src/plugins/vis_type_vega/public/expressions/__mocks__/helpers.ts create mode 100644 src/plugins/vis_type_vega/public/expressions/__mocks__/index.ts create mode 100644 src/plugins/vis_type_vega/public/expressions/__snapshots__/helpers.test.js.snap create mode 100644 src/plugins/vis_type_vega/public/expressions/helpers.test.js create mode 100644 src/plugins/vis_type_vega/public/expressions/helpers.ts create mode 100644 src/plugins/vis_type_vega/public/expressions/index.ts delete mode 100644 src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.test.js diff --git a/src/plugins/vis_augmenter/README.md b/src/plugins/vis_augmenter/README.md index fb9d4fbdcaa..4ebe2e4b1d4 100644 --- a/src/plugins/vis_augmenter/README.md +++ b/src/plugins/vis_augmenter/README.md @@ -1 +1 @@ -Contains interfaces and type definitions used for allowing external plugins to augment Visualizations. Registers the relevant saved object types and expression functions. +Contains interfaces, type definitions, helper functions, and services used for allowing external plugins to augment Visualizations. Registers the relevant saved object types and expression functions. diff --git a/src/plugins/vis_augmenter/public/constants.ts b/src/plugins/vis_augmenter/public/constants.ts new file mode 100644 index 00000000000..99b75a8de7e --- /dev/null +++ b/src/plugins/vis_augmenter/public/constants.ts @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const VIS_LAYER_COLUMN_TYPE = 'vis_layer'; +// TODO: replace with a value imported from OUI +export const EVENT_COLOR = 'red'; +export const HOVER_PARAM = 'HOVER'; +export const EVENT_MARK_SIZE = 100; +export const EVENT_MARK_SIZE_ENLARGED = 140; +export const EVENT_MARK_SHAPE = 'triangle-up'; +export const EVENT_TIMELINE_HEIGHT = 25; diff --git a/src/plugins/vis_augmenter/public/index.ts b/src/plugins/vis_augmenter/public/index.ts index 79ccbc183bd..cab6e6fef28 100644 --- a/src/plugins/vis_augmenter/public/index.ts +++ b/src/plugins/vis_augmenter/public/index.ts @@ -12,14 +12,19 @@ export function plugin(initializerContext: PluginInitializerContext) { export { VisAugmenterSetup, VisAugmenterStart }; export { - createSavedAugmentVisLoader, - createAugmentVisSavedObject, - SavedAugmentVisLoader, - SavedObjectOpenSearchDashboardsServicesWithAugmentVis, -} from './saved_augment_vis'; - -export { VisLayer, VisLayers, VisLayerTypes, VisLayerErrorTypes, VisLayerError } from './types'; + VisLayer, + VisLayers, + VisLayerTypes, + VisLayerErrorTypes, + VisLayerError, + PluginResource, + PointInTimeEvent, + PointInTimeEventsVisLayer, + isPointInTimeEventsVisLayer, +} from './types'; export * from './expressions'; export * from './utils'; +export * from './constants'; +export * from './vega'; export * from './saved_augment_vis'; diff --git a/src/plugins/vis_augmenter/public/test_constants.ts b/src/plugins/vis_augmenter/public/test_constants.ts new file mode 100644 index 00000000000..bc77e8ba803 --- /dev/null +++ b/src/plugins/vis_augmenter/public/test_constants.ts @@ -0,0 +1,561 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OpenSearchDashboardsDatatable } from '../../expressions/public'; +import { VIS_LAYER_COLUMN_TYPE, VisLayerTypes, HOVER_PARAM } from './'; + +const TEST_X_AXIS_ID = 'test-x-axis-id'; +const TEST_VALUE_AXIS_ID = 'test-value-axis-id'; +const TEST_X_AXIS_TITLE = 'time'; +const TEST_VALUE_AXIS_TITLE = 'avg value'; +const TEST_PLUGIN = 'test-plugin'; +const TEST_PLUGIN_RESOURCE_TYPE = 'test-resource-type'; +const TEST_PLUGIN_RESOURCE_ID = 'test-resource-id'; +const TEST_PLUGIN_RESOURCE_ID_2 = 'test-resource-id-2'; +const TEST_PLUGIN_RESOURCE_NAME = 'test-resource-name'; +const TEST_PLUGIN_RESOURCE_NAME_2 = 'test-resource-name-2'; +const TEST_PLUGIN_RESOURCE_PATH = `${TEST_PLUGIN}/${TEST_PLUGIN_RESOURCE_TYPE}/${TEST_PLUGIN_RESOURCE_ID}`; +const TEST_PLUGIN_RESOURCE_PATH_2 = `${TEST_PLUGIN}/${TEST_PLUGIN_RESOURCE_TYPE}/${TEST_PLUGIN_RESOURCE_ID_2}`; + +const TEST_VALUES_SINGLE_ROW_NO_VIS_LAYERS = [{ [TEST_X_AXIS_ID]: 0, [TEST_VALUE_AXIS_ID]: 5 }]; + +const TEST_VALUES_SINGLE_ROW_SINGLE_VIS_LAYER = [ + { [TEST_X_AXIS_ID]: 0, [TEST_VALUE_AXIS_ID]: 5, [TEST_PLUGIN_RESOURCE_ID]: 3 }, +]; + +const TEST_VALUES_ONLY_VIS_LAYERS = [ + { [TEST_X_AXIS_ID]: 0 }, + { [TEST_X_AXIS_ID]: 5, [TEST_PLUGIN_RESOURCE_ID]: 2 }, + { [TEST_X_AXIS_ID]: 10 }, + { [TEST_X_AXIS_ID]: 15 }, + { [TEST_X_AXIS_ID]: 20 }, + { [TEST_X_AXIS_ID]: 25 }, + { [TEST_X_AXIS_ID]: 30 }, + { [TEST_X_AXIS_ID]: 35, [TEST_PLUGIN_RESOURCE_ID]: 1 }, + { [TEST_X_AXIS_ID]: 40 }, + { [TEST_X_AXIS_ID]: 45 }, + { [TEST_X_AXIS_ID]: 50 }, +]; + +const TEST_VALUES_NO_VIS_LAYERS = [ + { [TEST_X_AXIS_ID]: 0, [TEST_VALUE_AXIS_ID]: 5 }, + { [TEST_X_AXIS_ID]: 5, [TEST_VALUE_AXIS_ID]: 10 }, + { [TEST_X_AXIS_ID]: 10, [TEST_VALUE_AXIS_ID]: 6 }, + { [TEST_X_AXIS_ID]: 15, [TEST_VALUE_AXIS_ID]: 4 }, + { [TEST_X_AXIS_ID]: 20, [TEST_VALUE_AXIS_ID]: 5 }, + { [TEST_X_AXIS_ID]: 25 }, + { [TEST_X_AXIS_ID]: 30 }, + { [TEST_X_AXIS_ID]: 35 }, + { [TEST_X_AXIS_ID]: 40 }, + { [TEST_X_AXIS_ID]: 45, [TEST_VALUE_AXIS_ID]: 3 }, + { [TEST_X_AXIS_ID]: 50, [TEST_VALUE_AXIS_ID]: 5 }, +]; + +const TEST_VALUES_SINGLE_VIS_LAYER = [ + { [TEST_X_AXIS_ID]: 0, [TEST_VALUE_AXIS_ID]: 5 }, + { [TEST_X_AXIS_ID]: 5, [TEST_VALUE_AXIS_ID]: 10, [TEST_PLUGIN_RESOURCE_ID]: 2 }, + { [TEST_X_AXIS_ID]: 10, [TEST_VALUE_AXIS_ID]: 6 }, + { [TEST_X_AXIS_ID]: 15, [TEST_VALUE_AXIS_ID]: 4 }, + { [TEST_X_AXIS_ID]: 20, [TEST_VALUE_AXIS_ID]: 5 }, + { [TEST_X_AXIS_ID]: 25 }, + { [TEST_X_AXIS_ID]: 30 }, + { [TEST_X_AXIS_ID]: 35, [TEST_PLUGIN_RESOURCE_ID]: 1 }, + { [TEST_X_AXIS_ID]: 40 }, + { [TEST_X_AXIS_ID]: 45, [TEST_VALUE_AXIS_ID]: 3 }, + { [TEST_X_AXIS_ID]: 50, [TEST_VALUE_AXIS_ID]: 5 }, +]; + +const TEST_VALUES_SINGLE_VIS_LAYER_ON_BOUNDS = [ + { [TEST_X_AXIS_ID]: 0, [TEST_VALUE_AXIS_ID]: 5, [TEST_PLUGIN_RESOURCE_ID]: 2 }, + { [TEST_X_AXIS_ID]: 5, [TEST_VALUE_AXIS_ID]: 10 }, + { [TEST_X_AXIS_ID]: 10, [TEST_VALUE_AXIS_ID]: 6 }, + { [TEST_X_AXIS_ID]: 15, [TEST_VALUE_AXIS_ID]: 4 }, + { [TEST_X_AXIS_ID]: 20, [TEST_VALUE_AXIS_ID]: 5 }, + { [TEST_X_AXIS_ID]: 25 }, + { [TEST_X_AXIS_ID]: 30 }, + { [TEST_X_AXIS_ID]: 35 }, + { [TEST_X_AXIS_ID]: 40 }, + { [TEST_X_AXIS_ID]: 45, [TEST_VALUE_AXIS_ID]: 3 }, + { [TEST_X_AXIS_ID]: 50, [TEST_VALUE_AXIS_ID]: 5, [TEST_PLUGIN_RESOURCE_ID]: 1 }, +]; + +const TEST_VALUES_MULTIPLE_VIS_LAYERS = [ + { [TEST_X_AXIS_ID]: 0, [TEST_VALUE_AXIS_ID]: 5 }, + { + [TEST_X_AXIS_ID]: 5, + [TEST_VALUE_AXIS_ID]: 10, + [TEST_PLUGIN_RESOURCE_ID]: 2, + [TEST_PLUGIN_RESOURCE_ID_2]: 1, + }, + { [TEST_X_AXIS_ID]: 10, [TEST_VALUE_AXIS_ID]: 6 }, + { [TEST_X_AXIS_ID]: 15, [TEST_VALUE_AXIS_ID]: 4, [TEST_PLUGIN_RESOURCE_ID_2]: 1 }, + { [TEST_X_AXIS_ID]: 20, [TEST_VALUE_AXIS_ID]: 5 }, + { [TEST_X_AXIS_ID]: 25 }, + { [TEST_X_AXIS_ID]: 30 }, + { [TEST_X_AXIS_ID]: 35, [TEST_PLUGIN_RESOURCE_ID]: 1 }, + { [TEST_X_AXIS_ID]: 40 }, + { [TEST_X_AXIS_ID]: 45, [TEST_VALUE_AXIS_ID]: 3 }, + { [TEST_X_AXIS_ID]: 50, [TEST_VALUE_AXIS_ID]: 5, [TEST_PLUGIN_RESOURCE_ID_2]: 2 }, +]; + +export const TEST_COLUMNS_NO_VIS_LAYERS = [ + { + id: TEST_X_AXIS_ID, + name: TEST_X_AXIS_TITLE, + }, + { + id: TEST_VALUE_AXIS_ID, + name: TEST_VALUE_AXIS_TITLE, + }, +]; + +export const TEST_COLUMNS_SINGLE_VIS_LAYER = [ + ...TEST_COLUMNS_NO_VIS_LAYERS, + { + id: TEST_PLUGIN_RESOURCE_ID, + name: TEST_PLUGIN_RESOURCE_NAME, + meta: { + type: VIS_LAYER_COLUMN_TYPE, + }, + }, +]; + +export const TEST_COLUMNS_MULTIPLE_VIS_LAYERS = [ + ...TEST_COLUMNS_SINGLE_VIS_LAYER, + { + id: TEST_PLUGIN_RESOURCE_ID_2, + name: TEST_PLUGIN_RESOURCE_NAME_2, + meta: { + type: VIS_LAYER_COLUMN_TYPE, + }, + }, +]; + +export const TEST_DATATABLE_SINGLE_ROW_NO_VIS_LAYERS = { + type: 'opensearch_dashboards_datatable', + columns: TEST_COLUMNS_NO_VIS_LAYERS, + rows: TEST_VALUES_SINGLE_ROW_NO_VIS_LAYERS, +} as OpenSearchDashboardsDatatable; + +export const TEST_DATATABLE_SINGLE_ROW_SINGLE_VIS_LAYER = { + type: 'opensearch_dashboards_datatable', + columns: TEST_COLUMNS_SINGLE_VIS_LAYER, + rows: TEST_VALUES_SINGLE_ROW_SINGLE_VIS_LAYER, +} as OpenSearchDashboardsDatatable; + +export const TEST_DATATABLE_ONLY_VIS_LAYERS = { + type: 'opensearch_dashboards_datatable', + columns: TEST_COLUMNS_SINGLE_VIS_LAYER, + rows: TEST_VALUES_ONLY_VIS_LAYERS, +} as OpenSearchDashboardsDatatable; + +export const TEST_DATATABLE_NO_VIS_LAYERS = { + type: 'opensearch_dashboards_datatable', + columns: TEST_COLUMNS_NO_VIS_LAYERS, + rows: TEST_VALUES_NO_VIS_LAYERS, +} as OpenSearchDashboardsDatatable; + +export const TEST_DATATABLE_SINGLE_VIS_LAYER_EMPTY = { + ...TEST_DATATABLE_NO_VIS_LAYERS, + columns: TEST_COLUMNS_SINGLE_VIS_LAYER, +} as OpenSearchDashboardsDatatable; + +export const TEST_DATATABLE_SINGLE_VIS_LAYER = { + type: 'opensearch_dashboards_datatable', + columns: TEST_COLUMNS_SINGLE_VIS_LAYER, + rows: TEST_VALUES_SINGLE_VIS_LAYER, +} as OpenSearchDashboardsDatatable; + +export const TEST_DATATABLE_SINGLE_VIS_LAYER_ON_BOUNDS = { + type: 'opensearch_dashboards_datatable', + columns: TEST_COLUMNS_SINGLE_VIS_LAYER, + rows: TEST_VALUES_SINGLE_VIS_LAYER_ON_BOUNDS, +} as OpenSearchDashboardsDatatable; + +export const TEST_DATATABLE_MULTIPLE_VIS_LAYERS = { + type: 'opensearch_dashboards_datatable', + columns: TEST_COLUMNS_MULTIPLE_VIS_LAYERS, + rows: TEST_VALUES_MULTIPLE_VIS_LAYERS, +} as OpenSearchDashboardsDatatable; + +const TEST_BASE_CONFIG = { + view: { stroke: null }, + concat: { spacing: 0 }, + legend: { orient: 'right' }, + kibana: { hideWarnings: true }, +}; + +const TEST_BASE_VIS_LAYER = { + mark: { type: 'line', interpolate: 'linear', strokeWidth: 2, point: true }, + encoding: { + x: { + axis: { title: TEST_X_AXIS_TITLE, grid: false }, + field: TEST_X_AXIS_ID, + type: 'temporal', + }, + y: { + axis: { + title: TEST_VALUE_AXIS_TITLE, + grid: '', + orient: 'left', + labels: true, + labelAngle: 0, + }, + field: TEST_VALUE_AXIS_ID, + type: 'quantitative', + }, + tooltip: [ + { field: TEST_X_AXIS_ID, type: 'temporal', title: TEST_VALUE_AXIS_TITLE }, + { field: TEST_VALUE_AXIS_ID, type: 'quantitative', title: TEST_VALUE_AXIS_TITLE }, + ], + color: { datum: TEST_VALUE_AXIS_TITLE }, + }, +}; + +export const TEST_SPEC_NO_VIS_LAYERS = { + $schema: 'https://vega.github.io/schema/vega-lite/v5.json', + data: { + values: TEST_VALUES_NO_VIS_LAYERS, + }, + config: TEST_BASE_CONFIG, + layer: [TEST_BASE_VIS_LAYER], +}; + +export const TEST_SPEC_SINGLE_VIS_LAYER = { + ...TEST_SPEC_NO_VIS_LAYERS, + data: { + ...TEST_SPEC_NO_VIS_LAYERS.data, + values: TEST_VALUES_SINGLE_VIS_LAYER, + }, +}; + +export const TEST_SPEC_MULTIPLE_VIS_LAYERS = { + ...TEST_SPEC_NO_VIS_LAYERS, + data: { + ...TEST_SPEC_NO_VIS_LAYERS.data, + values: TEST_VALUES_MULTIPLE_VIS_LAYERS, + }, +}; + +export const TEST_DIMENSIONS = { + x: { + params: { + interval: 5, + bounds: { + min: 0, + max: 50, + }, + }, + label: TEST_X_AXIS_TITLE, + }, +}; + +export const TEST_DIMENSIONS_SINGLE_ROW = { + x: { + params: { + interval: 5, + bounds: { + min: 0, + max: 0, + }, + }, + label: TEST_X_AXIS_TITLE, + }, +}; + +export const TEST_DIMENSIONS_INVALID_BOUNDS = { + x: { + params: { + interval: 5, + bounds: { + min: 50, + max: 0, + }, + }, + label: TEST_X_AXIS_TITLE, + }, +}; + +export const TEST_VIS_LAYERS_SINGLE = [ + { + originPlugin: TEST_PLUGIN, + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: TEST_PLUGIN_RESOURCE_TYPE, + id: TEST_PLUGIN_RESOURCE_ID, + name: TEST_PLUGIN_RESOURCE_NAME, + urlPath: TEST_PLUGIN_RESOURCE_PATH, + }, + events: [ + { + timestamp: 4, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID, + }, + }, + { + timestamp: 6, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID, + }, + }, + { + timestamp: 35, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID, + }, + }, + ], + }, +]; + +export const TEST_VIS_LAYERS_SINGLE_INVALID_BOUNDS = [ + { + originPlugin: TEST_PLUGIN, + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: TEST_PLUGIN_RESOURCE_TYPE, + id: TEST_PLUGIN_RESOURCE_ID, + name: TEST_PLUGIN_RESOURCE_NAME, + urlPath: TEST_PLUGIN_RESOURCE_PATH, + }, + events: [ + { + timestamp: -5, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID, + }, + }, + { + timestamp: -100, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID, + }, + }, + { + timestamp: 75, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID, + }, + }, + ], + }, +]; + +export const TEST_VIS_LAYERS_SINGLE_ON_BOUNDS = [ + { + originPlugin: TEST_PLUGIN, + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: TEST_PLUGIN_RESOURCE_TYPE, + id: TEST_PLUGIN_RESOURCE_ID, + name: TEST_PLUGIN_RESOURCE_NAME, + urlPath: TEST_PLUGIN_RESOURCE_PATH, + }, + events: [ + { + timestamp: 0, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID, + }, + }, + { + timestamp: 2, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID, + }, + }, + { + timestamp: 55, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID, + }, + }, + ], + }, +]; + +export const TEST_VIS_LAYERS_MULTIPLE = [ + ...TEST_VIS_LAYERS_SINGLE, + { + originPlugin: TEST_PLUGIN, + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: TEST_PLUGIN_RESOURCE_TYPE, + id: TEST_PLUGIN_RESOURCE_ID_2, + name: TEST_PLUGIN_RESOURCE_NAME_2, + urlPath: TEST_PLUGIN_RESOURCE_PATH_2, + }, + events: [ + { + timestamp: 5, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID_2, + }, + }, + { + timestamp: 15, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID_2, + }, + }, + { + timestamp: 49, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID_2, + }, + }, + { + timestamp: 50, + metadata: { + pluginResourceId: TEST_PLUGIN_RESOURCE_ID_2, + }, + }, + ], + }, +]; + +const TEST_RULE_LAYER_SINGLE_VIS_LAYER = { + mark: { type: 'rule', color: 'red', opacity: 1 }, + transform: [{ filter: `datum['${TEST_PLUGIN_RESOURCE_ID}'] > 0` }], + encoding: { + x: { field: TEST_X_AXIS_ID, type: 'temporal' }, + opacity: { value: 0, condition: { empty: false, param: HOVER_PARAM, value: 1 } }, + }, +}; + +const TEST_RULE_LAYER_MULTIPLE_VIS_LAYERS = { + ...TEST_RULE_LAYER_SINGLE_VIS_LAYER, + transform: [ + { + filter: `datum['${TEST_PLUGIN_RESOURCE_ID}'] > 0 || datum['${TEST_PLUGIN_RESOURCE_ID_2}'] > 0`, + }, + ], +}; + +const TEST_EVENTS_LAYER_SINGLE_VIS_LAYER = { + height: 25, + mark: { + type: 'point', + shape: 'triangle-up', + color: 'red', + filled: true, + opacity: 1, + }, + transform: [{ filter: `datum['${TEST_PLUGIN_RESOURCE_ID}'] > 0` }], + params: [{ name: HOVER_PARAM, select: { type: 'point', on: 'mouseover' } }], + encoding: { + x: { + axis: { + title: TEST_X_AXIS_TITLE, + grid: false, + ticks: true, + orient: 'bottom', + domain: true, + }, + field: TEST_X_AXIS_ID, + type: 'temporal', + scale: { + domain: [ + { + year: 2022, + month: 'December', + date: 1, + hours: 0, + minutes: 0, + seconds: 0, + milliseconds: 0, + }, + { + year: 2023, + month: 'March', + date: 2, + hours: 0, + minutes: 0, + seconds: 0, + milliseconds: 0, + }, + ], + }, + }, + size: { condition: { empty: false, param: HOVER_PARAM, value: 140 }, value: 100 }, + }, +}; + +const TEST_EVENTS_LAYER_MULTIPLE_VIS_LAYERS = { + ...TEST_EVENTS_LAYER_SINGLE_VIS_LAYER, + transform: [ + { + filter: `datum['${TEST_PLUGIN_RESOURCE_ID}'] > 0 || datum['${TEST_PLUGIN_RESOURCE_ID_2}'] > 0`, + }, + ], +}; + +export const TEST_RESULT_SPEC_SINGLE_VIS_LAYER = { + $schema: 'https://vega.github.io/schema/vega-lite/v5.json', + data: { + values: TEST_VALUES_SINGLE_VIS_LAYER, + }, + config: TEST_BASE_CONFIG, + vconcat: [ + { + layer: [ + { + ...TEST_BASE_VIS_LAYER, + encoding: { + ...TEST_BASE_VIS_LAYER.encoding, + x: { + ...TEST_BASE_VIS_LAYER.encoding.x, + axis: { + title: null, + grid: false, + labels: false, + }, + }, + }, + }, + TEST_RULE_LAYER_SINGLE_VIS_LAYER, + ], + }, + TEST_EVENTS_LAYER_SINGLE_VIS_LAYER, + ], +}; + +export const TEST_RESULT_SPEC_SINGLE_VIS_LAYER_EMPTY = { + ...TEST_RESULT_SPEC_SINGLE_VIS_LAYER, + data: { + values: TEST_VALUES_NO_VIS_LAYERS, + }, +}; + +export const TEST_RESULT_SPEC_MULTIPLE_VIS_LAYERS = { + $schema: 'https://vega.github.io/schema/vega-lite/v5.json', + data: { + values: TEST_VALUES_MULTIPLE_VIS_LAYERS, + }, + config: TEST_BASE_CONFIG, + vconcat: [ + { + layer: [ + { + ...TEST_BASE_VIS_LAYER, + encoding: { + ...TEST_BASE_VIS_LAYER.encoding, + x: { + ...TEST_BASE_VIS_LAYER.encoding.x, + axis: { + title: null, + grid: false, + labels: false, + }, + }, + }, + }, + TEST_RULE_LAYER_MULTIPLE_VIS_LAYERS, + ], + }, + TEST_EVENTS_LAYER_MULTIPLE_VIS_LAYERS, + ], +}; diff --git a/src/plugins/vis_augmenter/public/vega/README.md b/src/plugins/vis_augmenter/public/vega/README.md new file mode 100644 index 00000000000..fef45af1777 --- /dev/null +++ b/src/plugins/vis_augmenter/public/vega/README.md @@ -0,0 +1 @@ +Contains the helper functions that are optionally used when rendering vega charts that are eligible for rendering with VisLayers. diff --git a/src/plugins/vis_augmenter/public/vega/helpers.test.ts b/src/plugins/vis_augmenter/public/vega/helpers.test.ts new file mode 100644 index 00000000000..3f1fa58a4fc --- /dev/null +++ b/src/plugins/vis_augmenter/public/vega/helpers.test.ts @@ -0,0 +1,444 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { cloneDeep } from 'lodash'; +import { + OpenSearchDashboardsDatatable, + OpenSearchDashboardsDatatableColumn, +} from '../../../expressions/public'; +import { + enableVisLayersInSpecConfig, + isVisLayerColumn, + generateVisLayerFilterString, + addMissingRowsToTableBounds, + addPointInTimeEventsLayersToTable, + addPointInTimeEventsLayersToSpec, +} from './helpers'; +import { VIS_LAYER_COLUMN_TYPE, VisLayerTypes, PointInTimeEventsVisLayer, VisLayer } from '../'; +import { + TEST_DATATABLE_MULTIPLE_VIS_LAYERS, + TEST_DATATABLE_NO_VIS_LAYERS, + TEST_DATATABLE_ONLY_VIS_LAYERS, + TEST_DATATABLE_SINGLE_ROW_NO_VIS_LAYERS, + TEST_DATATABLE_SINGLE_ROW_SINGLE_VIS_LAYER, + TEST_DATATABLE_SINGLE_VIS_LAYER, + TEST_DATATABLE_SINGLE_VIS_LAYER_EMPTY, + TEST_DATATABLE_SINGLE_VIS_LAYER_ON_BOUNDS, + TEST_DIMENSIONS, + TEST_DIMENSIONS_INVALID_BOUNDS, + TEST_DIMENSIONS_SINGLE_ROW, + TEST_RESULT_SPEC_MULTIPLE_VIS_LAYERS, + TEST_RESULT_SPEC_SINGLE_VIS_LAYER, + TEST_RESULT_SPEC_SINGLE_VIS_LAYER_EMPTY, + TEST_SPEC_MULTIPLE_VIS_LAYERS, + TEST_SPEC_NO_VIS_LAYERS, + TEST_SPEC_SINGLE_VIS_LAYER, + TEST_VIS_LAYERS_MULTIPLE, + TEST_VIS_LAYERS_SINGLE, + TEST_VIS_LAYERS_SINGLE_INVALID_BOUNDS, + TEST_VIS_LAYERS_SINGLE_ON_BOUNDS, +} from '../test_constants'; + +describe('helpers', function () { + describe('enableVisLayersInSpecConfig()', function () { + const pointInTimeEventsVisLayer = { + type: VisLayerTypes.PointInTimeEvents, + originPlugin: 'test-plugin', + pluginResource: { + type: 'test-resource-type', + id: 'test-resource-id', + name: 'test-resource-name', + urlPath: 'test-resource-url-path', + }, + events: [ + { + timestamp: 1234, + metadata: { + pluginResourceId: 'test-resource-id', + }, + }, + ], + } as PointInTimeEventsVisLayer; + const invalidVisLayer = ({ + type: 'something-invalid', + originPlugin: 'test-plugin', + pluginResource: { + type: 'test-resource-type', + id: 'test-resource-id', + name: 'test-resource-name', + urlPath: 'test-resource-url-path', + }, + } as unknown) as VisLayer; + + it('updates config with just a valid Vislayer', function () { + const baseConfig = { + kibana: { + hideWarnings: true, + }, + }; + const updatedConfig = enableVisLayersInSpecConfig({ config: baseConfig }, [ + pointInTimeEventsVisLayer, + ]); + const expectedMap = new Map([ + [VisLayerTypes.PointInTimeEvents, true], + ]); + // @ts-ignore + baseConfig.kibana.visibleVisLayers = expectedMap; + expect(updatedConfig).toStrictEqual(baseConfig); + }); + it('updates config with a valid and invalid VisLayer', function () { + const baseConfig = { + kibana: { + hideWarnings: true, + }, + }; + const updatedConfig = enableVisLayersInSpecConfig({ config: baseConfig }, [ + pointInTimeEventsVisLayer, + invalidVisLayer, + ]); + const expectedMap = new Map([ + [VisLayerTypes.PointInTimeEvents, true], + ]); + // @ts-ignore + baseConfig.kibana.visibleVisLayers = expectedMap; + expect(updatedConfig).toStrictEqual(baseConfig); + }); + it('does not update config if no valid VisLayer', function () { + const baseConfig = { + kibana: { + hideWarnings: true, + }, + }; + const updatedConfig = enableVisLayersInSpecConfig({ config: baseConfig }, [invalidVisLayer]); + // @ts-ignore + baseConfig.kibana.visibleVisLayers = new Map(); + expect(updatedConfig).toStrictEqual(baseConfig); + }); + it('does not update config if empty VisLayer list', function () { + const baseConfig = { + kibana: { + hideWarnings: true, + }, + }; + const updatedConfig = enableVisLayersInSpecConfig({ config: baseConfig }, []); + // @ts-ignore + baseConfig.kibana.visibleVisLayers = new Map(); + expect(updatedConfig).toStrictEqual(baseConfig); + }); + }); + + describe('isVisLayerColumn()', function () { + it('return false for column with invalid type', function () { + const column = { + id: 'test-id', + name: 'test-name', + meta: { + type: 'invalid-type', + }, + } as OpenSearchDashboardsDatatableColumn; + expect(isVisLayerColumn(column)).toBe(false); + }); + it('return false for column with no meta field', function () { + const column = { + id: 'test-id', + name: 'test-name', + } as OpenSearchDashboardsDatatableColumn; + expect(isVisLayerColumn(column)).toBe(false); + }); + it('return true for column with valid type', function () { + const column = { + id: 'test-id', + name: 'test-name', + meta: { + type: VIS_LAYER_COLUMN_TYPE, + }, + } as OpenSearchDashboardsDatatableColumn; + expect(isVisLayerColumn(column)).toBe(true); + }); + }); + + describe('generateVisLayerFilterString()', function () { + it('empty array returns false', function () { + const visLayerColumnIds = [] as string[]; + const filterString = 'false'; + expect(generateVisLayerFilterString(visLayerColumnIds)).toStrictEqual(filterString); + }); + it('array with one value returns correct filter string', function () { + const visLayerColumnIds = ['test-id-1']; + const filterString = `datum['test-id-1'] > 0`; + expect(generateVisLayerFilterString(visLayerColumnIds)).toStrictEqual(filterString); + }); + it('array with multiple values returns correct filter string', function () { + const visLayerColumnIds = ['test-id-1', 'test-id-2']; + const filterString = `datum['test-id-1'] > 0 || datum['test-id-2'] > 0`; + expect(generateVisLayerFilterString(visLayerColumnIds)).toStrictEqual(filterString); + }); + }); + + describe('addMissingRowsToTableBounds()', function () { + const columnId = 'test-id'; + const columnName = 'test-name'; + const allRows = [ + { + [columnId]: 1, + }, + { + [columnId]: 2, + }, + { + [columnId]: 3, + }, + { + [columnId]: 4, + }, + { + [columnId]: 5, + }, + ]; + it('adds single row if start/end times are the same', function () { + const datatable = { + type: 'opensearch_dashboards_datatable', + columns: [ + { + id: columnId, + name: columnName, + }, + ], + rows: [], + } as OpenSearchDashboardsDatatable; + const dimensions = { + x: { + params: { + interval: 1, + bounds: { + min: 1, + max: 1, + }, + }, + label: columnName, + }, + }; + const result = addMissingRowsToTableBounds(datatable, dimensions); + const expectedTable = { + ...datatable, + rows: [allRows[0]], + }; + expect(result).toStrictEqual(expectedTable); + }); + it('adds all rows if there is none to begin with', function () { + const datatable = { + type: 'opensearch_dashboards_datatable', + columns: [ + { + id: columnId, + name: columnName, + }, + ], + rows: [], + } as OpenSearchDashboardsDatatable; + const dimensions = { + x: { + params: { + interval: 1, + bounds: { + min: 1, + max: 5, + }, + }, + label: columnName, + }, + }; + const result = addMissingRowsToTableBounds(datatable, dimensions); + const expectedTable = { + ...datatable, + rows: allRows, + }; + expect(result).toStrictEqual(expectedTable); + }); + it('fill rows at beginning', function () { + const missingRows = cloneDeep(allRows); + missingRows.shift(); + missingRows.shift(); + const datatable = { + type: 'opensearch_dashboards_datatable', + columns: [ + { + id: columnId, + name: columnName, + }, + ], + rows: missingRows, + } as OpenSearchDashboardsDatatable; + const dimensions = { + x: { + params: { + interval: 1, + bounds: { + min: 1, + max: 5, + }, + }, + label: columnName, + }, + }; + const result = addMissingRowsToTableBounds(datatable, dimensions); + const expectedTable = { + ...datatable, + rows: allRows, + }; + expect(result).toStrictEqual(expectedTable); + }); + it('fill rows at end', function () { + const missingRows = cloneDeep(allRows); + missingRows.pop(); + missingRows.pop(); + const datatable = { + type: 'opensearch_dashboards_datatable', + columns: [ + { + id: columnId, + name: columnName, + }, + ], + rows: missingRows, + } as OpenSearchDashboardsDatatable; + const dimensions = { + x: { + params: { + interval: 1, + bounds: { + min: 1, + max: 5, + }, + }, + label: columnName, + }, + }; + const result = addMissingRowsToTableBounds(datatable, dimensions); + const expectedTable = { + ...datatable, + rows: allRows, + }; + expect(result).toStrictEqual(expectedTable); + }); + }); + + describe('addPointInTimeEventsLayersToTable()', function () { + it('single vis layer is added correctly', function () { + expect( + addPointInTimeEventsLayersToTable( + TEST_DATATABLE_NO_VIS_LAYERS, + TEST_DIMENSIONS, + TEST_VIS_LAYERS_SINGLE + ) + ).toStrictEqual(TEST_DATATABLE_SINGLE_VIS_LAYER); + }); + it('multiple vis layers are added correctly', function () { + expect( + addPointInTimeEventsLayersToTable( + TEST_DATATABLE_NO_VIS_LAYERS, + TEST_DIMENSIONS, + TEST_VIS_LAYERS_MULTIPLE + ) + ).toStrictEqual(TEST_DATATABLE_MULTIPLE_VIS_LAYERS); + }); + it('invalid bounds adds no row data', function () { + expect( + addPointInTimeEventsLayersToTable( + { + ...TEST_DATATABLE_NO_VIS_LAYERS, + rows: [], + }, + TEST_DIMENSIONS_INVALID_BOUNDS, + TEST_VIS_LAYERS_SINGLE + ) + ).toStrictEqual({ + ...TEST_DATATABLE_SINGLE_VIS_LAYER_EMPTY, + rows: [], + }); + }); + it('vis layers with single row are added correctly', function () { + expect( + addPointInTimeEventsLayersToTable( + TEST_DATATABLE_SINGLE_ROW_NO_VIS_LAYERS, + TEST_DIMENSIONS_SINGLE_ROW, + TEST_VIS_LAYERS_SINGLE + ) + ).toStrictEqual(TEST_DATATABLE_SINGLE_ROW_SINGLE_VIS_LAYER); + }); + it('vis layers with no existing rows/data are added correctly', function () { + expect( + addPointInTimeEventsLayersToTable( + { + ...TEST_DATATABLE_NO_VIS_LAYERS, + rows: [], + }, + TEST_DIMENSIONS, + TEST_VIS_LAYERS_SINGLE + ) + ).toStrictEqual(TEST_DATATABLE_ONLY_VIS_LAYERS); + }); + it('vis layer with out-of-bounds timestamps are not added', function () { + expect( + addPointInTimeEventsLayersToTable( + TEST_DATATABLE_NO_VIS_LAYERS, + TEST_DIMENSIONS, + TEST_VIS_LAYERS_SINGLE_INVALID_BOUNDS + ) + ).toStrictEqual(TEST_DATATABLE_SINGLE_VIS_LAYER_EMPTY); + }); + it('vis layer with events on edge of bounds are added', function () { + expect( + addPointInTimeEventsLayersToTable( + TEST_DATATABLE_NO_VIS_LAYERS, + TEST_DIMENSIONS, + TEST_VIS_LAYERS_SINGLE_ON_BOUNDS + ) + ).toStrictEqual(TEST_DATATABLE_SINGLE_VIS_LAYER_ON_BOUNDS); + }); + }); + + describe('addPointInTimeEventsLayersToSpec()', function () { + it('spec with single time series produces correct spec', function () { + const expectedSpec = TEST_RESULT_SPEC_SINGLE_VIS_LAYER; + const returnSpec = addPointInTimeEventsLayersToSpec( + TEST_DATATABLE_SINGLE_VIS_LAYER, + TEST_DIMENSIONS, + TEST_SPEC_SINGLE_VIS_LAYER + ); + // deleting the scale fields since this contain generated + // fields based on timezone env it is run in + delete expectedSpec.vconcat[1].encoding.x.scale; + delete returnSpec.vconcat[1].encoding.x.scale; + expect(returnSpec).toEqual(expectedSpec); + }); + it('spec with multiple time series produces correct spec', function () { + const expectedSpec = TEST_RESULT_SPEC_MULTIPLE_VIS_LAYERS; + const returnSpec = addPointInTimeEventsLayersToSpec( + TEST_DATATABLE_MULTIPLE_VIS_LAYERS, + TEST_DIMENSIONS, + TEST_SPEC_MULTIPLE_VIS_LAYERS + ); + // deleting the scale fields since this contain generated + // fields based on timezone env it is run in + delete expectedSpec.vconcat[1].encoding.x.scale; + delete returnSpec.vconcat[1].encoding.x.scale; + expect(returnSpec).toEqual(expectedSpec); + }); + it('spec with vis layers with empty data produces correct spec', function () { + const expectedSpec = TEST_RESULT_SPEC_SINGLE_VIS_LAYER_EMPTY; + const returnSpec = addPointInTimeEventsLayersToSpec( + TEST_DATATABLE_SINGLE_VIS_LAYER_EMPTY, + TEST_DIMENSIONS, + TEST_SPEC_NO_VIS_LAYERS + ); + // deleting the scale fields since this contain generated + // fields based on timezone env it is run in + delete expectedSpec.vconcat[1].encoding.x.scale; + delete returnSpec.vconcat[1].encoding.x.scale; + expect(returnSpec).toEqual(expectedSpec); + }); + }); +}); diff --git a/src/plugins/vis_augmenter/public/vega/helpers.ts b/src/plugins/vis_augmenter/public/vega/helpers.ts new file mode 100644 index 00000000000..670a9c7590b --- /dev/null +++ b/src/plugins/vis_augmenter/public/vega/helpers.ts @@ -0,0 +1,342 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import moment from 'moment'; +import { cloneDeep, isEmpty, get } from 'lodash'; +import { + OpenSearchDashboardsDatatable, + OpenSearchDashboardsDatatableColumn, +} from '../../../expressions/public'; +import { + PointInTimeEvent, + PointInTimeEventsVisLayer, + isPointInTimeEventsVisLayer, + VIS_LAYER_COLUMN_TYPE, + EVENT_COLOR, + EVENT_MARK_SIZE, + EVENT_MARK_SIZE_ENLARGED, + EVENT_MARK_SHAPE, + EVENT_TIMELINE_HEIGHT, + HOVER_PARAM, + VisLayer, + VisLayers, + VisLayerTypes, +} from '../'; + +export const enableVisLayersInSpecConfig = (spec: object, visLayers: VisLayers): {} => { + const config = get(spec, 'config', { kibana: {} }); + const visibleVisLayers = new Map(); + + // Currently only support PointInTimeEventsVisLayers. Set the flag to true + // if there are any + const pointInTimeEventsVisLayers = visLayers.filter((visLayer: VisLayer) => + isPointInTimeEventsVisLayer(visLayer) + ) as PointInTimeEventsVisLayer[]; + if (!isEmpty(pointInTimeEventsVisLayers)) { + visibleVisLayers.set(VisLayerTypes.PointInTimeEvents, true); + } + return { + ...config, + kibana: { + ...config.kibana, + visibleVisLayers, + }, + }; +}; + +// 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 = ( + dimensions: any, + columns: OpenSearchDashboardsDatatableColumn[] +): string => { + return columns.filter((column) => column.name === dimensions.x.label)[0].id; +}; + +export const isVisLayerColumn = (column: OpenSearchDashboardsDatatableColumn): boolean => { + return column.meta?.type === VIS_LAYER_COLUMN_TYPE; +}; + +/** + * For temporal domain ranges, there is a bug when passing timestamps in vega lite + * that is still present in the current libraries we are using when developing in a + * dev env. See https://github.com/vega/vega-lite/issues/6060 for bug details. + * So, we convert to a vega-lite Date Time object and pass that instead. + * See https://vega.github.io/vega-lite/docs/datetime.html for details on Date Time. + */ +const convertToDateTimeObj = (timestamp: number): any => { + const momentObj = moment(timestamp); + return { + year: Number(momentObj.format('YYYY')), + month: momentObj.format('MMMM'), + date: momentObj.date(), + hours: momentObj.hours(), + minutes: momentObj.minutes(), + seconds: momentObj.seconds(), + milliseconds: momentObj.milliseconds(), + }; +}; + +export const generateVisLayerFilterString = (visLayerColumnIds: string[]): string => { + if (!isEmpty(visLayerColumnIds)) { + const filterString = visLayerColumnIds.map( + (visLayerColumnId) => `datum['${visLayerColumnId}'] > 0` + ); + return filterString.join(' || '); + } else { + // if there is no VisLayers to display, then filter out everything by always returning false + return 'false'; + } +}; + +/** + * By default, the source datatable will not include rows with empty data. + * For handling events that may belong in missing buckets that are not yet + * created, we need to create them. For more details, see description in + * https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3145 + * + * Note that this may add buckets with start/end times out of the chart bounds. + * This is the current default behavior of histogram aggregations with intervals, + * in order for the bucket keys to have "clean" timestamp keys (e.g., 1/1 @ 12AM). + * For more details, see + * https://opensearch.org/docs/latest/opensearch/bucket-agg/#histogram-date_histogram + * + * Also note this is only adding empty buckets at the beginning/end of a table. We are + * not taking into account missing buckets within source datapoints. Because of this + * limitation, it is possible that charted events may not be put into the most precise + * bucket based on their raw event timestamps, if there is missing / sparse source data. + */ +export const addMissingRowsToTableBounds = ( + datatable: OpenSearchDashboardsDatatable, + dimensions: any +): OpenSearchDashboardsDatatable => { + const augmentedTable = cloneDeep(datatable); + const intervalMillis = moment.duration(dimensions.x.params.interval).asMilliseconds(); + const xAxisId = getXAxisId(dimensions, augmentedTable.columns); + const chartStartTime = new Date(dimensions.x.params.bounds.min).valueOf(); + const chartEndTime = new Date(dimensions.x.params.bounds.max).valueOf(); + + if (!isEmpty(augmentedTable.rows)) { + const dataStartTime = augmentedTable.rows[0][xAxisId] as number; + const dataEndTime = augmentedTable.rows[augmentedTable.rows.length - 1][xAxisId] as number; + + let curStartTime = dataStartTime; + while (curStartTime > chartStartTime) { + curStartTime -= intervalMillis; + augmentedTable.rows.unshift({ + [xAxisId]: curStartTime, + }); + } + + let curEndTime = dataEndTime; + while (curEndTime < chartEndTime) { + curEndTime += intervalMillis; + augmentedTable.rows.push({ + [xAxisId]: curEndTime, + }); + } + } else { + // if there's no existing rows, create them all + let curTime = chartStartTime; + while (curTime <= chartEndTime) { + augmentedTable.rows.push({ + [xAxisId]: curTime, + }); + curTime += intervalMillis; + } + } + return augmentedTable; +}; + +/** + * Adding events into the correct x-axis key (the time bucket) + * based on the table. As of now only results from + * PointInTimeEventsVisLayers are supported + */ +export const addPointInTimeEventsLayersToTable = ( + datatable: OpenSearchDashboardsDatatable, + dimensions: any, + visLayers: PointInTimeEventsVisLayer[] +): OpenSearchDashboardsDatatable => { + const augmentedTable = addMissingRowsToTableBounds(datatable, dimensions); + const xAxisId = getXAxisId(dimensions, augmentedTable.columns); + + if (isEmpty(visLayers)) return augmentedTable; + + visLayers.every((visLayer: PointInTimeEventsVisLayer) => { + const visLayerColumnId = `${visLayer.pluginResource.id}`; + const visLayerColumnName = `${visLayer.pluginResource.name}`; + augmentedTable.columns.push({ + id: visLayerColumnId, + name: visLayerColumnName, + meta: { + type: VIS_LAYER_COLUMN_TYPE, + }, + }); + + if (augmentedTable.rows.length === 0) { + return false; + } + + // if only one row / one datapoint, put all events into this bucket + if (augmentedTable.rows.length === 1) { + augmentedTable.rows[0] = { + ...augmentedTable.rows[0], + [visLayerColumnId]: visLayer.events.length, + }; + return false; + } + + // Bin the timestamps to the closest x-axis key, adding + // an entry for this vis layer ID. Sorting the timestamps first + // so that we will only search a particular row value once. + // There could be some optimizations, such as binary search + dynamically + // changing the bounds, but performance benefits would be very minimal + // if any, given the upper bounds limit on n already due to chart constraints. + let rowIndex = 0; + const minVal = augmentedTable.rows[0][xAxisId] as number; + const maxVal = + (augmentedTable.rows[augmentedTable.rows.length - 1][xAxisId] as number) + + moment.duration(dimensions.x.params.interval).asMilliseconds(); + const sortedTimestamps = visLayer.events + .map((event: PointInTimeEvent) => event.timestamp) + .filter((timestamp: number) => timestamp >= minVal && timestamp <= maxVal) + .sort((n1: number, n2: number) => n1 - n2) as number[]; + + sortedTimestamps.forEach((timestamp) => { + while (rowIndex < augmentedTable.rows.length - 1) { + const smallerVal = augmentedTable.rows[rowIndex][xAxisId] as number; + const higherVal = augmentedTable.rows[rowIndex + 1][xAxisId] as number; + let rowIndexToInsert: number; + + // timestamp is on the left bounds of the chart + if (timestamp === smallerVal) { + rowIndexToInsert = rowIndex; + + // timestamp is in between the right 2 buckets. determine which one it is closer to + } else if (timestamp <= higherVal) { + const smallerValDiff = Math.abs(timestamp - smallerVal); + const higherValDiff = Math.abs(timestamp - higherVal); + rowIndexToInsert = smallerValDiff <= higherValDiff ? rowIndex : rowIndex + 1; + } + + // timestamp is on the right bounds of the chart + else if (rowIndex + 1 === augmentedTable.rows.length - 1) { + rowIndexToInsert = rowIndex + 1; + // timestamp is still too small; traverse to next bucket + } else { + rowIndex += 1; + continue; + } + + // inserting the value. increment if the mapping/property already exists + augmentedTable.rows[rowIndexToInsert][visLayerColumnId] = + (get(augmentedTable.rows[rowIndexToInsert], visLayerColumnId, 0) as number) + 1; + break; + } + }); + + return true; + }); + return augmentedTable; +}; + +/** + * Updating the vega lite spec to include layers and marks related to + * PointInTimeEventsVisLayers. It is assumed the datatable has already been + * augmented with columns and row data containing the vis layers. + */ +export const addPointInTimeEventsLayersToSpec = ( + datatable: OpenSearchDashboardsDatatable, + dimensions: any, + spec: object +): object => { + const newSpec = cloneDeep(spec) as any; + + const xAxisId = getXAxisId(dimensions, datatable.columns); + const xAxisTitle = dimensions.x.label.replaceAll('"', ''); + const bucketStartTime = convertToDateTimeObj(datatable.rows[0][xAxisId] as number); + const bucketEndTime = convertToDateTimeObj( + datatable.rows[datatable.rows.length - 1][xAxisId] as number + ); + const visLayerColumns = datatable.columns.filter((column: OpenSearchDashboardsDatatableColumn) => + isVisLayerColumn(column) + ); + const visLayerColumnIds = visLayerColumns.map((column) => column.id); + + // Hide x axes text on existing chart so they are only visible on the event chart + newSpec.layer.forEach((dataSeries: any) => { + if (get(dataSeries, 'encoding.x.axis', null) !== null) { + dataSeries.encoding.x.axis = { + ...dataSeries.encoding.x.axis, + labels: false, + title: null, + }; + } + }); + + // Add a rule to the existing layer for showing lines on the chart if a dot is hovered on + newSpec.layer.push({ + mark: { + type: 'rule', + color: EVENT_COLOR, + opacity: 1, + }, + transform: [{ filter: generateVisLayerFilterString(visLayerColumnIds) }], + encoding: { + x: { + field: xAxisId, + type: 'temporal', + }, + opacity: { + value: 0, + condition: { empty: false, param: HOVER_PARAM, value: 1 }, + }, + }, + }); + + // Nesting layer into a vconcat field so we can append event chart. + newSpec.vconcat = [] as any[]; + newSpec.vconcat.push({ + layer: newSpec.layer, + }); + delete newSpec.layer; + + // Adding the event timeline chart + newSpec.vconcat.push({ + height: EVENT_TIMELINE_HEIGHT, + mark: { + type: 'point', + shape: EVENT_MARK_SHAPE, + color: EVENT_COLOR, + filled: true, + opacity: 1, + }, + transform: [{ filter: generateVisLayerFilterString(visLayerColumnIds) }], + params: [{ name: HOVER_PARAM, select: { type: 'point', on: 'mouseover' } }], + encoding: { + x: { + axis: { + title: xAxisTitle, + grid: false, + ticks: true, + orient: 'bottom', + domain: true, + }, + field: xAxisId, + type: 'temporal', + scale: { + domain: [bucketStartTime, bucketEndTime], + }, + }, + size: { + condition: { empty: false, param: HOVER_PARAM, value: EVENT_MARK_SIZE_ENLARGED }, + value: EVENT_MARK_SIZE, + }, + }, + }); + + return newSpec; +}; diff --git a/src/plugins/vis_augmenter/public/vega/index.ts b/src/plugins/vis_augmenter/public/vega/index.ts new file mode 100644 index 00000000000..0e8ad44d2bd --- /dev/null +++ b/src/plugins/vis_augmenter/public/vega/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './helpers'; diff --git a/src/plugins/vis_type_vega/opensearch_dashboards.json b/src/plugins/vis_type_vega/opensearch_dashboards.json index ca4d7020c2f..17aee4a9723 100644 --- a/src/plugins/vis_type_vega/opensearch_dashboards.json +++ b/src/plugins/vis_type_vega/opensearch_dashboards.json @@ -4,6 +4,11 @@ "server": true, "ui": true, "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions", "inspector"], - "optionalPlugins": ["home","usageCollection"], - "requiredBundles": ["opensearchDashboardsUtils", "opensearchDashboardsReact", "visDefaultEditor"] + "optionalPlugins": ["home", "usageCollection"], + "requiredBundles": [ + "opensearchDashboardsUtils", + "opensearchDashboardsReact", + "visDefaultEditor", + "visAugmenter" + ] } diff --git a/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap b/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap index 7ba343a02f2..973d539e475 100644 --- a/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap +++ b/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap @@ -2,8 +2,8 @@ exports[`VegaVisualizations VegaVisualization - basics should show vega blank rectangle on top of a map (vegamap) 1`] = `"
"`; -exports[`VegaVisualizations VegaVisualization - basics should show vega graph (may fail in dev env) 1`] = `"
"`; +exports[`VegaVisualizations VegaVisualization - basics should show vega graph (may fail in dev env) 1`] = `"
  • Cannot read property 'get' of undefined
"`; -exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 1`] = `"
  • \\"width\\" and \\"height\\" params are ignored because \\"autosize\\" is enabled. Set \\"autosize\\": \\"none\\" to disable
"`; +exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 1`] = `"
  • Cannot read property 'get' of undefined
"`; -exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 2`] = `"
  • \\"width\\" and \\"height\\" params are ignored because \\"autosize\\" is enabled. Set \\"autosize\\": \\"none\\" to disable
"`; +exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 2`] = `"
  • Cannot read property 'get' of undefined
"`; diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 64ed96f7e3e..4de808506f3 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -44,6 +44,7 @@ import { UrlParser } from './url_parser'; import { SearchAPI } from './search_api'; import { TimeCache } from './time_cache'; import { IServiceSettings } from '../../../maps_legacy/public'; +import { VisLayerTypes } from '../../../vis_augmenter/public'; import { Bool, Data, @@ -92,6 +93,7 @@ export class VegaParser { getServiceSettings: () => Promise; filters: Bool; timeCache: TimeCache; + visibleVisLayers: Map; constructor( spec: VegaSpec | string, @@ -102,6 +104,7 @@ export class VegaParser { ) { this.spec = spec as VegaSpec; this.hideWarnings = false; + this.visibleVisLayers = new Map(); this.error = undefined; this.warnings = []; @@ -158,6 +161,7 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never this._config = this._parseConfig(); this.hideWarnings = !!this._config.hideWarnings; + this.visibleVisLayers = this._config.visibleVisLayers; this.useMap = this._config.type === 'map'; this.renderer = this._config.renderer === 'svg' ? 'svg' : 'canvas'; this.tooltips = this._parseTooltips(); @@ -190,6 +194,17 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never contains: 'padding', }; + // If we are showing PointInTimeEventsVisLayers, it means we are showing a base vis + event vis. + // Because this will be using a vconcat spec, we can autosize the width + // via fit-x. Note the regular 'fit' (to autosize width + height) does not work here. + // See limitations: https://vega.github.io/vega-lite/docs/size.html#limitations + const showPointInTimeEvents = + this.visibleVisLayers.get(VisLayerTypes.PointInTimeEvents) === true; + const showPointInTimeEventsAutosize = { + type: 'fit-x', + contains: 'padding', + }; + let autosize = this.spec.autosize; let useResize = true; @@ -224,6 +239,10 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never autosize = defaultAutosize; } + if (showPointInTimeEvents) { + autosize = showPointInTimeEventsAutosize; + } + if ( useResize && ((this.spec.width && this.spec.width !== 'container') || @@ -243,7 +262,7 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never ); } - if (useResize) { + if (useResize && !showPointInTimeEvents) { this.spec.width = 'container'; this.spec.height = 'container'; } diff --git a/src/plugins/vis_type_vega/public/expressions/__mocks__/helpers.ts b/src/plugins/vis_type_vega/public/expressions/__mocks__/helpers.ts new file mode 100644 index 00000000000..0dd50913d5f --- /dev/null +++ b/src/plugins/vis_type_vega/public/expressions/__mocks__/helpers.ts @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const complexDatatable = + '{"type":"opensearch_dashboards_datatable","rows":[{"col-0-2":1672214400000,"col-1-1":44,"col-2-3":60.9375},{"col-0-2":1672300800000,"col-1-1":150,"col-2-3":82.5},{"col-0-2":1672387200000,"col-1-1":154,"col-2-3":79.5},{"col-0-2":1672473600000,"col-1-1":144,"col-2-3":75.875},{"col-0-2":1672560000000,"col-1-1":133,"col-2-3":259.25},{"col-0-2":1672646400000,"col-1-1":149,"col-2-3":90},{"col-0-2":1672732800000,"col-1-1":152,"col-2-3":79.0625},{"col-0-2":1672819200000,"col-1-1":144,"col-2-3":82.5},{"col-0-2":1672905600000,"col-1-1":166,"col-2-3":85.25},{"col-0-2":1672992000000,"col-1-1":151,"col-2-3":92},{"col-0-2":1673078400000,"col-1-1":143,"col-2-3":90.75},{"col-0-2":1673164800000,"col-1-1":148,"col-2-3":92},{"col-0-2":1673251200000,"col-1-1":146,"col-2-3":83.25},{"col-0-2":1673337600000,"col-1-1":137,"col-2-3":98},{"col-0-2":1673424000000,"col-1-1":152,"col-2-3":83.6875},{"col-0-2":1673510400000,"col-1-1":152,"col-2-3":83.6875},{"col-0-2":1673596800000,"col-1-1":151,"col-2-3":87.4375},{"col-0-2":1673683200000,"col-1-1":157,"col-2-3":63.75},{"col-0-2":1673769600000,"col-1-1":151,"col-2-3":81.5625},{"col-0-2":1673856000000,"col-1-1":152,"col-2-3":100.6875},{"col-0-2":1673942400000,"col-1-1":142,"col-2-3":98},{"col-0-2":1674028800000,"col-1-1":151,"col-2-3":100.8125},{"col-0-2":1674115200000,"col-1-1":163,"col-2-3":83.6875},{"col-0-2":1674201600000,"col-1-1":156,"col-2-3":85.8125},{"col-0-2":1674288000000,"col-1-1":153,"col-2-3":98},{"col-0-2":1674374400000,"col-1-1":162,"col-2-3":75.9375},{"col-0-2":1674460800000,"col-1-1":152,"col-2-3":113.375},{"col-0-2":1674547200000,"col-1-1":159,"col-2-3":73.625},{"col-0-2":1674633600000,"col-1-1":165,"col-2-3":72.8125},{"col-0-2":1674720000000,"col-1-1":153,"col-2-3":113.375},{"col-0-2":1674806400000,"col-1-1":149,"col-2-3":82.5},{"col-0-2":1674892800000,"col-1-1":94,"col-2-3":54}],"columns":[{"id":"col-0-2","name":"order_date per day","meta":{"type":"date_histogram","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{"field":"order_date","timeRange":{"from":"now-90d","to":"now"},"useNormalizedOpenSearchInterval":true,"scaleMetricValues":false,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}}},{"id":"col-1-1","name":"Count","meta":{"type":"count","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{}}},{"id":"col-2-3","name":"Max products.min_price","meta":{"type":"max","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{"field":"products.min_price"}}}]}'; +export const complexVisParams = + '{"addLegend":true,"addTimeMarker":true,"addTooltip":true,"categoryAxes":[{"id":"CategoryAxis-1","labels":{"filter":true,"show":true,"truncate":100},"position":"bottom","scale":{"type":"linear"},"show":true,"style":{},"title":{},"type":"category"}],"grid":{"categoryLines":false,"valueAxis":"ValueAxis-1"},"labels":{},"legendPosition":"bottom","seriesParams":[{"data":{"id":"1","label":"Count"},"drawLinesBetweenPoints":true,"interpolate":"linear","lineWidth":2,"mode":"normal","show":true,"showCircles":true,"type":"line","valueAxis":"ValueAxis-1"},{"data":{"id":"3","label":"Max products.min_price"},"drawLinesBetweenPoints":true,"interpolate":"linear","lineWidth":2,"mode":"normal","show":true,"showCircles":true,"type":"line","valueAxis":"ValueAxis-1"}],"thresholdLine":{"color":"#E7664C","show":true,"style":"dashed","value":100,"width":1},"times":[],"type":"line","valueAxes":[{"id":"ValueAxis-1","labels":{"filter":false,"rotate":75,"show":true,"truncate":100},"name":"RightAxis-1","position":"right","scale":{"mode":"normal","type":"linear"},"show":true,"style":{},"title":{"text":"Count"},"type":"value"}]}'; +export const complexDimensions = + '{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD"}},"params":{"date":true,"interval":"P1D","intervalESValue":1,"intervalESUnit":"d","format":"YYYY-MM-DD","bounds":{"min":"2022-11-19T03:26:04.730Z","max":"2023-02-17T03:26:04.730Z"}},"label":"order_date per day","aggType":"date_histogram"},"y":[{"accessor":1,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"},{"accessor":2,"format":{"id":"number","params":{"parsedUrl":{"origin":"http://localhost:5603","pathname":"/rao/app/visualize","basePath":"/rao"}}},"params":{},"label":"Max products.min_price","aggType":"max"}]}'; + +export const noXAxisDimensions = + '{"x":null,"y":[{"accessor":0,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"}]}'; + +export const simpleDatatable = + '{"type":"opensearch_dashboards_datatable","rows":[{"col-0-2":1672214400000,"col-1-1":44},{"col-0-2":1672300800000,"col-1-1":150},{"col-0-2":1672387200000,"col-1-1":154},{"col-0-2":1672473600000,"col-1-1":144},{"col-0-2":1672560000000,"col-1-1":133},{"col-0-2":1672646400000,"col-1-1":149},{"col-0-2":1672732800000,"col-1-1":152},{"col-0-2":1672819200000,"col-1-1":144},{"col-0-2":1672905600000,"col-1-1":166},{"col-0-2":1672992000000,"col-1-1":151},{"col-0-2":1673078400000,"col-1-1":143},{"col-0-2":1673164800000,"col-1-1":148},{"col-0-2":1673251200000,"col-1-1":146},{"col-0-2":1673337600000,"col-1-1":137},{"col-0-2":1673424000000,"col-1-1":152},{"col-0-2":1673510400000,"col-1-1":152},{"col-0-2":1673596800000,"col-1-1":151},{"col-0-2":1673683200000,"col-1-1":157},{"col-0-2":1673769600000,"col-1-1":151},{"col-0-2":1673856000000,"col-1-1":152},{"col-0-2":1673942400000,"col-1-1":142},{"col-0-2":1674028800000,"col-1-1":151},{"col-0-2":1674115200000,"col-1-1":163},{"col-0-2":1674201600000,"col-1-1":156},{"col-0-2":1674288000000,"col-1-1":153},{"col-0-2":1674374400000,"col-1-1":162},{"col-0-2":1674460800000,"col-1-1":152},{"col-0-2":1674547200000,"col-1-1":159},{"col-0-2":1674633600000,"col-1-1":165},{"col-0-2":1674720000000,"col-1-1":153},{"col-0-2":1674806400000,"col-1-1":149},{"col-0-2":1674892800000,"col-1-1":94}],"columns":[{"id":"col-0-2","name":"order_date per day","meta":{"type":"date_histogram","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{"field":"order_date","timeRange":{"from":"now-90d","to":"now"},"useNormalizedOpenSearchInterval":true,"scaleMetricValues":false,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}}},{"id":"col-1-1","name":"Count","meta":{"type":"count","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{}}}]}'; +export const simpleVisParams = + '{"addLegend":true,"addTimeMarker":false,"addTooltip":true,"categoryAxes":[{"id":"CategoryAxis-1","labels":{"filter":true,"show":true,"truncate":100},"position":"bottom","scale":{"type":"linear"},"show":true,"style":{},"title":{},"type":"category"}],"grid":{"categoryLines":false},"labels":{},"legendPosition":"right","seriesParams":[{"data":{"id":"1","label":"Count"},"drawLinesBetweenPoints":true,"interpolate":"linear","lineWidth":2,"mode":"normal","show":true,"showCircles":true,"type":"line","valueAxis":"ValueAxis-1"}],"thresholdLine":{"color":"#E7664C","show":false,"style":"full","value":10,"width":1},"times":[],"type":"line","valueAxes":[{"id":"ValueAxis-1","labels":{"filter":false,"rotate":0,"show":true,"truncate":100},"name":"LeftAxis-1","position":"left","scale":{"mode":"normal","type":"linear"},"show":true,"style":{},"title":{"text":"Count"},"type":"value"}]}'; +export const simpleDimensions = + '{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD"}},"params":{"date":true,"interval":"P1D","intervalESValue":1,"intervalESUnit":"d","format":"YYYY-MM-DD","bounds":{"min":"2022-11-18T00:14:09.617Z","max":"2023-02-16T00:14:09.617Z"}},"label":"order_date per day","aggType":"date_histogram"},"y":[{"accessor":1,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"}]}'; diff --git a/src/plugins/vis_type_vega/public/expressions/__mocks__/index.ts b/src/plugins/vis_type_vega/public/expressions/__mocks__/index.ts new file mode 100644 index 00000000000..0e8ad44d2bd --- /dev/null +++ b/src/plugins/vis_type_vega/public/expressions/__mocks__/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './helpers'; diff --git a/src/plugins/vis_type_vega/public/expressions/__snapshots__/helpers.test.js.snap b/src/plugins/vis_type_vega/public/expressions/__snapshots__/helpers.test.js.snap new file mode 100644 index 00000000000..69af7eba10b --- /dev/null +++ b/src/plugins/vis_type_vega/public/expressions/__snapshots__/helpers.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`helpers createSpecFromDatatable() build complicated line chart" 1`] = `"{\\"$schema\\":\\"https://vega.github.io/schema/vega-lite/v5.json\\",\\"data\\":{\\"values\\":[{\\"col-0-2\\":1672214400000,\\"col-1-1\\":44,\\"col-2-3\\":60.9375},{\\"col-0-2\\":1672300800000,\\"col-1-1\\":150,\\"col-2-3\\":82.5},{\\"col-0-2\\":1672387200000,\\"col-1-1\\":154,\\"col-2-3\\":79.5},{\\"col-0-2\\":1672473600000,\\"col-1-1\\":144,\\"col-2-3\\":75.875},{\\"col-0-2\\":1672560000000,\\"col-1-1\\":133,\\"col-2-3\\":259.25},{\\"col-0-2\\":1672646400000,\\"col-1-1\\":149,\\"col-2-3\\":90},{\\"col-0-2\\":1672732800000,\\"col-1-1\\":152,\\"col-2-3\\":79.0625},{\\"col-0-2\\":1672819200000,\\"col-1-1\\":144,\\"col-2-3\\":82.5},{\\"col-0-2\\":1672905600000,\\"col-1-1\\":166,\\"col-2-3\\":85.25},{\\"col-0-2\\":1672992000000,\\"col-1-1\\":151,\\"col-2-3\\":92},{\\"col-0-2\\":1673078400000,\\"col-1-1\\":143,\\"col-2-3\\":90.75},{\\"col-0-2\\":1673164800000,\\"col-1-1\\":148,\\"col-2-3\\":92},{\\"col-0-2\\":1673251200000,\\"col-1-1\\":146,\\"col-2-3\\":83.25},{\\"col-0-2\\":1673337600000,\\"col-1-1\\":137,\\"col-2-3\\":98},{\\"col-0-2\\":1673424000000,\\"col-1-1\\":152,\\"col-2-3\\":83.6875},{\\"col-0-2\\":1673510400000,\\"col-1-1\\":152,\\"col-2-3\\":83.6875},{\\"col-0-2\\":1673596800000,\\"col-1-1\\":151,\\"col-2-3\\":87.4375},{\\"col-0-2\\":1673683200000,\\"col-1-1\\":157,\\"col-2-3\\":63.75},{\\"col-0-2\\":1673769600000,\\"col-1-1\\":151,\\"col-2-3\\":81.5625},{\\"col-0-2\\":1673856000000,\\"col-1-1\\":152,\\"col-2-3\\":100.6875},{\\"col-0-2\\":1673942400000,\\"col-1-1\\":142,\\"col-2-3\\":98},{\\"col-0-2\\":1674028800000,\\"col-1-1\\":151,\\"col-2-3\\":100.8125},{\\"col-0-2\\":1674115200000,\\"col-1-1\\":163,\\"col-2-3\\":83.6875},{\\"col-0-2\\":1674201600000,\\"col-1-1\\":156,\\"col-2-3\\":85.8125},{\\"col-0-2\\":1674288000000,\\"col-1-1\\":153,\\"col-2-3\\":98},{\\"col-0-2\\":1674374400000,\\"col-1-1\\":162,\\"col-2-3\\":75.9375},{\\"col-0-2\\":1674460800000,\\"col-1-1\\":152,\\"col-2-3\\":113.375},{\\"col-0-2\\":1674547200000,\\"col-1-1\\":159,\\"col-2-3\\":73.625},{\\"col-0-2\\":1674633600000,\\"col-1-1\\":165,\\"col-2-3\\":72.8125},{\\"col-0-2\\":1674720000000,\\"col-1-1\\":153,\\"col-2-3\\":113.375},{\\"col-0-2\\":1674806400000,\\"col-1-1\\":149,\\"col-2-3\\":82.5},{\\"col-0-2\\":1674892800000,\\"col-1-1\\":94,\\"col-2-3\\":54}]},\\"config\\":{\\"view\\":{\\"stroke\\":null},\\"concat\\":{\\"spacing\\":0},\\"legend\\":{\\"orient\\":\\"bottom\\"},\\"kibana\\":{\\"hideWarnings\\":true}},\\"layer\\":[{\\"mark\\":{\\"type\\":\\"line\\",\\"interpolate\\":\\"linear\\",\\"strokeWidth\\":2,\\"point\\":true},\\"encoding\\":{\\"x\\":{\\"axis\\":{\\"title\\":\\"order_date per day\\",\\"grid\\":false},\\"field\\":\\"col-0-2\\",\\"type\\":\\"temporal\\"},\\"y\\":{\\"axis\\":{\\"title\\":\\"Count\\",\\"grid\\":\\"ValueAxis-1\\",\\"orient\\":\\"right\\",\\"labels\\":true,\\"labelAngle\\":75},\\"field\\":\\"col-1-1\\",\\"type\\":\\"quantitative\\"},\\"tooltip\\":[{\\"field\\":\\"col-0-2\\",\\"type\\":\\"temporal\\",\\"title\\":\\"order_date per day\\"},{\\"field\\":\\"col-1-1\\",\\"type\\":\\"quantitative\\",\\"title\\":\\"Count\\"}],\\"color\\":{\\"datum\\":\\"Count\\"}}},{\\"mark\\":{\\"type\\":\\"line\\",\\"interpolate\\":\\"linear\\",\\"strokeWidth\\":2,\\"point\\":true},\\"encoding\\":{\\"x\\":{\\"axis\\":{\\"title\\":\\"order_date per day\\",\\"grid\\":false},\\"field\\":\\"col-0-2\\",\\"type\\":\\"temporal\\"},\\"y\\":{\\"axis\\":{\\"title\\":\\"Count\\",\\"grid\\":\\"ValueAxis-1\\",\\"orient\\":\\"right\\",\\"labels\\":true,\\"labelAngle\\":75},\\"field\\":\\"col-2-3\\",\\"type\\":\\"quantitative\\"},\\"tooltip\\":[{\\"field\\":\\"col-0-2\\",\\"type\\":\\"temporal\\",\\"title\\":\\"order_date per day\\"},{\\"field\\":\\"col-2-3\\",\\"type\\":\\"quantitative\\",\\"title\\":\\"Max products.min_price\\"}],\\"color\\":{\\"datum\\":\\"Max products.min_price\\"}}},{\\"mark\\":\\"rule\\",\\"encoding\\":{\\"x\\":{\\"type\\":\\"temporal\\",\\"field\\":\\"now_field\\"},\\"color\\":{\\"value\\":\\"red\\"},\\"size\\":{\\"value\\":1}}},{\\"mark\\":{\\"type\\":\\"rule\\",\\"color\\":\\"#E7664C\\",\\"strokeDash\\":[8,8]},\\"encoding\\":{\\"y\\":{\\"datum\\":100}}}],\\"transform\\":[{\\"calculate\\":\\"now()\\",\\"as\\":\\"now_field\\"}]}"`; + +exports[`helpers createSpecFromDatatable() build empty chart if no x-axis is defined" 1`] = `"{\\"$schema\\":\\"https://vega.github.io/schema/vega-lite/v5.json\\",\\"data\\":{\\"values\\":[{\\"col-0-2\\":1672214400000,\\"col-1-1\\":44},{\\"col-0-2\\":1672300800000,\\"col-1-1\\":150},{\\"col-0-2\\":1672387200000,\\"col-1-1\\":154},{\\"col-0-2\\":1672473600000,\\"col-1-1\\":144},{\\"col-0-2\\":1672560000000,\\"col-1-1\\":133},{\\"col-0-2\\":1672646400000,\\"col-1-1\\":149},{\\"col-0-2\\":1672732800000,\\"col-1-1\\":152},{\\"col-0-2\\":1672819200000,\\"col-1-1\\":144},{\\"col-0-2\\":1672905600000,\\"col-1-1\\":166},{\\"col-0-2\\":1672992000000,\\"col-1-1\\":151},{\\"col-0-2\\":1673078400000,\\"col-1-1\\":143},{\\"col-0-2\\":1673164800000,\\"col-1-1\\":148},{\\"col-0-2\\":1673251200000,\\"col-1-1\\":146},{\\"col-0-2\\":1673337600000,\\"col-1-1\\":137},{\\"col-0-2\\":1673424000000,\\"col-1-1\\":152},{\\"col-0-2\\":1673510400000,\\"col-1-1\\":152},{\\"col-0-2\\":1673596800000,\\"col-1-1\\":151},{\\"col-0-2\\":1673683200000,\\"col-1-1\\":157},{\\"col-0-2\\":1673769600000,\\"col-1-1\\":151},{\\"col-0-2\\":1673856000000,\\"col-1-1\\":152},{\\"col-0-2\\":1673942400000,\\"col-1-1\\":142},{\\"col-0-2\\":1674028800000,\\"col-1-1\\":151},{\\"col-0-2\\":1674115200000,\\"col-1-1\\":163},{\\"col-0-2\\":1674201600000,\\"col-1-1\\":156},{\\"col-0-2\\":1674288000000,\\"col-1-1\\":153},{\\"col-0-2\\":1674374400000,\\"col-1-1\\":162},{\\"col-0-2\\":1674460800000,\\"col-1-1\\":152},{\\"col-0-2\\":1674547200000,\\"col-1-1\\":159},{\\"col-0-2\\":1674633600000,\\"col-1-1\\":165},{\\"col-0-2\\":1674720000000,\\"col-1-1\\":153},{\\"col-0-2\\":1674806400000,\\"col-1-1\\":149},{\\"col-0-2\\":1674892800000,\\"col-1-1\\":94}]},\\"config\\":{\\"view\\":{\\"stroke\\":null},\\"concat\\":{\\"spacing\\":0},\\"legend\\":{\\"orient\\":\\"right\\"},\\"kibana\\":{\\"hideWarnings\\":true}},\\"layer\\":[]}"`; + +exports[`helpers createSpecFromDatatable() build simple line chart" 1`] = `"{\\"$schema\\":\\"https://vega.github.io/schema/vega-lite/v5.json\\",\\"data\\":{\\"values\\":[{\\"col-0-2\\":1672214400000,\\"col-1-1\\":44},{\\"col-0-2\\":1672300800000,\\"col-1-1\\":150},{\\"col-0-2\\":1672387200000,\\"col-1-1\\":154},{\\"col-0-2\\":1672473600000,\\"col-1-1\\":144},{\\"col-0-2\\":1672560000000,\\"col-1-1\\":133},{\\"col-0-2\\":1672646400000,\\"col-1-1\\":149},{\\"col-0-2\\":1672732800000,\\"col-1-1\\":152},{\\"col-0-2\\":1672819200000,\\"col-1-1\\":144},{\\"col-0-2\\":1672905600000,\\"col-1-1\\":166},{\\"col-0-2\\":1672992000000,\\"col-1-1\\":151},{\\"col-0-2\\":1673078400000,\\"col-1-1\\":143},{\\"col-0-2\\":1673164800000,\\"col-1-1\\":148},{\\"col-0-2\\":1673251200000,\\"col-1-1\\":146},{\\"col-0-2\\":1673337600000,\\"col-1-1\\":137},{\\"col-0-2\\":1673424000000,\\"col-1-1\\":152},{\\"col-0-2\\":1673510400000,\\"col-1-1\\":152},{\\"col-0-2\\":1673596800000,\\"col-1-1\\":151},{\\"col-0-2\\":1673683200000,\\"col-1-1\\":157},{\\"col-0-2\\":1673769600000,\\"col-1-1\\":151},{\\"col-0-2\\":1673856000000,\\"col-1-1\\":152},{\\"col-0-2\\":1673942400000,\\"col-1-1\\":142},{\\"col-0-2\\":1674028800000,\\"col-1-1\\":151},{\\"col-0-2\\":1674115200000,\\"col-1-1\\":163},{\\"col-0-2\\":1674201600000,\\"col-1-1\\":156},{\\"col-0-2\\":1674288000000,\\"col-1-1\\":153},{\\"col-0-2\\":1674374400000,\\"col-1-1\\":162},{\\"col-0-2\\":1674460800000,\\"col-1-1\\":152},{\\"col-0-2\\":1674547200000,\\"col-1-1\\":159},{\\"col-0-2\\":1674633600000,\\"col-1-1\\":165},{\\"col-0-2\\":1674720000000,\\"col-1-1\\":153},{\\"col-0-2\\":1674806400000,\\"col-1-1\\":149},{\\"col-0-2\\":1674892800000,\\"col-1-1\\":94}]},\\"config\\":{\\"view\\":{\\"stroke\\":null},\\"concat\\":{\\"spacing\\":0},\\"legend\\":{\\"orient\\":\\"right\\"},\\"kibana\\":{\\"hideWarnings\\":true}},\\"layer\\":[{\\"mark\\":{\\"type\\":\\"line\\",\\"interpolate\\":\\"linear\\",\\"strokeWidth\\":2,\\"point\\":true},\\"encoding\\":{\\"x\\":{\\"axis\\":{\\"title\\":\\"order_date per day\\",\\"grid\\":false},\\"field\\":\\"col-0-2\\",\\"type\\":\\"temporal\\"},\\"y\\":{\\"axis\\":{\\"title\\":\\"Count\\",\\"orient\\":\\"left\\",\\"labels\\":true,\\"labelAngle\\":0},\\"field\\":\\"col-1-1\\",\\"type\\":\\"quantitative\\"},\\"tooltip\\":[{\\"field\\":\\"col-0-2\\",\\"type\\":\\"temporal\\",\\"title\\":\\"order_date per day\\"},{\\"field\\":\\"col-1-1\\",\\"type\\":\\"quantitative\\",\\"title\\":\\"Count\\"}],\\"color\\":{\\"datum\\":\\"Count\\"}}}]}"`; diff --git a/src/plugins/vis_type_vega/public/expressions/helpers.test.js b/src/plugins/vis_type_vega/public/expressions/helpers.test.js new file mode 100644 index 00000000000..f09b1b8db3d --- /dev/null +++ b/src/plugins/vis_type_vega/public/expressions/helpers.test.js @@ -0,0 +1,183 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + buildLayerMark, + buildXAxis, + buildYAxis, + cleanString, + createSpecFromDatatable, + formatDatatable, + setupConfig, +} from './helpers'; +import { + complexDatatable, + complexVisParams, + complexDimensions, + simpleDatatable, + simpleVisParams, + simpleDimensions, + noXAxisDimensions, +} from './__mocks__'; + +describe('helpers', function () { + describe('cleanString()', function () { + it('string should not contain "', function () { + const dirtyString = '"someString"'; + expect(cleanString(dirtyString)).toBe('someString'); + }); + }); + + describe('setupConfig()', function () { + it('check all legend positions', function () { + const baseConfig = { + view: { + stroke: null, + }, + concat: { + spacing: 0, + }, + legend: { + orient: null, + }, + kibana: { + hideWarnings: true, + }, + }; + const positions = ['top', 'right', 'left', 'bottom']; + positions.forEach((position) => { + const visParams = { legendPosition: position }; + baseConfig.legend.orient = position; + expect(setupConfig(visParams)).toStrictEqual(baseConfig); + }); + }); + }); + + describe('buildLayerMark()', function () { + const types = ['line', 'area', 'histogram']; + const interpolates = ['linear', 'cardinal', 'step-after']; + const strokeWidths = [-1, 0, 1, 2, 3, 4]; + const showCircles = [false, true]; + + it('check each mark possible value', function () { + const mark = { + type: null, + interpolate: null, + strokeWidth: null, + point: null, + }; + types.forEach((type) => { + mark.type = type; + interpolates.forEach((interpolate) => { + mark.interpolate = interpolate; + strokeWidths.forEach((strokeWidth) => { + mark.strokeWidth = strokeWidth; + showCircles.forEach((showCircle) => { + mark.point = showCircle; + const param = { + type: type, + interpolate: interpolate, + lineWidth: strokeWidth, + showCircles: showCircle, + }; + expect(buildLayerMark(param)).toStrictEqual(mark); + }); + }); + }); + }); + }); + }); + + describe('buildXAxis()', function () { + it('build different XAxis', function () { + const xAxisTitle = 'someTitle'; + const xAxisId = 'someId'; + [true, false].forEach((enableGrid) => { + const visParams = { grid: { categoryLines: enableGrid } }; + const vegaXAxis = { + axis: { + title: xAxisTitle, + grid: enableGrid, + }, + field: xAxisId, + type: 'temporal', + }; + expect(buildXAxis(xAxisTitle, xAxisId, visParams)).toStrictEqual(vegaXAxis); + }); + }); + }); + + describe('buildYAxis()', function () { + it('build different YAxis', function () { + const valueAxis = { + id: 'someId', + labels: { + rotate: 75, + show: false, + }, + position: 'left', + title: { + text: 'someText', + }, + }; + const column = { name: 'columnName', id: 'columnId' }; + const visParams = { grid: { valueAxis: true } }; + const vegaYAxis = { + axis: { + title: 'someText', + grid: true, + orient: 'left', + labels: false, + labelAngle: 75, + }, + field: 'columnId', + type: 'quantitative', + }; + expect(buildYAxis(column, valueAxis, visParams)).toStrictEqual(vegaYAxis); + + valueAxis.title.text = '""'; + vegaYAxis.axis.title = 'columnName'; + expect(buildYAxis(column, valueAxis, visParams)).toStrictEqual(vegaYAxis); + }); + }); + + describe('createSpecFromDatatable()', function () { + it('build simple line chart"', function () { + expect( + JSON.stringify( + createSpecFromDatatable( + formatDatatable(JSON.parse(simpleDatatable)), + JSON.parse(simpleVisParams), + JSON.parse(simpleDimensions) + ) + ) + ).toMatchSnapshot(); + }); + + it('build empty chart if no x-axis is defined"', function () { + expect( + JSON.stringify( + createSpecFromDatatable( + formatDatatable(JSON.parse(simpleDatatable)), + JSON.parse(simpleVisParams), + JSON.parse(noXAxisDimensions) + ) + ) + ).toMatchSnapshot(); + }); + + it('build complicated line chart"', function () { + expect( + JSON.stringify( + createSpecFromDatatable( + formatDatatable(JSON.parse(complexDatatable)), + JSON.parse(complexVisParams), + JSON.parse(complexDimensions) + ) + ) + ).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/vis_type_vega/public/expressions/helpers.ts b/src/plugins/vis_type_vega/public/expressions/helpers.ts new file mode 100644 index 00000000000..ca31367bf11 --- /dev/null +++ b/src/plugins/vis_type_vega/public/expressions/helpers.ts @@ -0,0 +1,238 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + OpenSearchDashboardsDatatable, + OpenSearchDashboardsDatatableColumn, +} from '../../../expressions/public'; +import { VislibDimensions, VisParams } from '../../../visualizations/public'; +import { isVisLayerColumn } from '../../../vis_augmenter/public'; + +// TODO: move this to the visualization plugin that has VisParams once all of these parameters have been better defined +interface ValueAxis { + id: string; + labels: { + filter: boolean; + rotate: number; + show: boolean; + truncate: number; + }; + name: string; + position: string; + scale: { + mode: string; + type: string; + }; + show: true; + style: any; + title: { + text: string; + }; + type: string; +} + +// 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 +const getXAxisId = (dimensions: any, columns: OpenSearchDashboardsDatatableColumn[]): string => { + return columns.filter((column) => column.name === dimensions.x.label)[0].id; +}; + +export const cleanString = (rawString: string): string => { + return rawString.replaceAll('"', ''); +}; + +export const formatDatatable = ( + datatable: OpenSearchDashboardsDatatable +): OpenSearchDashboardsDatatable => { + datatable.columns.forEach((column) => { + // clean quotation marks from names in columns + column.name = cleanString(column.name); + }); + return datatable; +}; + +export const setupConfig = (visParams: VisParams) => { + const legendPosition = visParams.legendPosition; + return { + view: { + stroke: null, + }, + concat: { + spacing: 0, + }, + legend: { + orient: legendPosition, + }, + // This is parsed in the VegaParser and hides unnecessary warnings. + // For example, 'infinite extent' warnings that cover the chart + // when there is empty data for a time series + kibana: { + hideWarnings: true, + }, + }; +}; + +export const buildLayerMark = (seriesParams: { + type: string; + interpolate: string; + lineWidth: number; + showCircles: boolean; +}) => { + return { + // Possible types are: line, area, histogram. The eligibility checker will + // prevent area and histogram (though area works in vega-lite) + type: seriesParams.type, + // Possible types: linear, cardinal, step-after. All of these types work in vega-lite + interpolate: seriesParams.interpolate, + // The possible values is any number, which matches what vega-lite supports + strokeWidth: seriesParams.lineWidth, + // this corresponds to showing the dots in the visbuilder for each data point + point: seriesParams.showCircles, + }; +}; + +export const buildXAxis = (xAxisTitle: string, xAxisId: string, visParams: VisParams) => { + return { + axis: { + title: xAxisTitle, + grid: visParams.grid.categoryLines, + }, + field: xAxisId, + // Right now, the line charts can only set the x-axis value to be a date attribute, so + // this should always be of type temporal + type: 'temporal', + }; +}; + +export const buildYAxis = ( + column: OpenSearchDashboardsDatatableColumn, + valueAxis: ValueAxis, + visParams: VisParams +) => { + return { + axis: { + title: cleanString(valueAxis.title.text) || column.name, + grid: visParams.grid.valueAxis, + orient: valueAxis.position, + labels: valueAxis.labels.show, + labelAngle: valueAxis.labels.rotate, + }, + field: column.id, + type: 'quantitative', + }; +}; + +const isXAxisColumn = (column: OpenSearchDashboardsDatatableColumn): boolean => { + return column.meta?.aggConfigParams?.interval !== undefined; +}; + +export const createSpecFromDatatable = ( + datatable: OpenSearchDashboardsDatatable, + visParams: VisParams, + dimensions: VislibDimensions +): 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. + const spec = {} as any; + + spec.$schema = 'https://vega.github.io/schema/vega-lite/v5.json'; + spec.data = { + values: datatable.rows, + }; + spec.config = setupConfig(visParams); + + // Get the valueAxes data and generate a map to easily fetch the different valueAxes data + const valueAxis = new Map(); + visParams?.valueAxes?.forEach((yAxis: ValueAxis) => { + valueAxis.set(yAxis.id, yAxis); + }); + + spec.layer = [] as any[]; + + if (datatable.rows.length > 0 && dimensions.x !== null) { + const xAxisId = getXAxisId(dimensions, datatable.columns); + const xAxisTitle = cleanString(dimensions.x.label); + let seriesParamSkipCount = 0; + datatable.columns.forEach((column, index) => { + // Don't add a layer for x axis column + if (isXAxisColumn(column)) { + seriesParamSkipCount++; + // Don't add a layer for vis layer column + } else if (!isVisLayerColumn(column)) { + const currentSeriesParams = visParams.seriesParams[index - seriesParamSkipCount]; + const currentValueAxis = valueAxis.get(currentSeriesParams.valueAxis.toString()); + let tooltip: Array<{ field: string; type: string; title: string }> = []; + if (visParams.addTooltip) { + tooltip = [ + { field: xAxisId, type: 'temporal', title: xAxisTitle }, + { field: column.id, type: 'quantitative', title: column.name }, + ]; + } + spec.layer.push({ + mark: buildLayerMark(currentSeriesParams), + encoding: { + x: buildXAxis(xAxisTitle, xAxisId, visParams), + y: buildYAxis(column, currentValueAxis, visParams), + tooltip, + color: { + // This ensures all the different metrics have their own distinct and unique color + datum: column.name, + }, + }, + }); + } + }); + } + + if (visParams.addTimeMarker) { + spec.transform = [ + { + calculate: 'now()', + as: 'now_field', + }, + ]; + + spec.layer.push({ + mark: 'rule', + encoding: { + x: { + type: 'temporal', + field: 'now_field', + }, + // The time marker on vislib is red, so keeping this consistent + color: { + value: 'red', + }, + size: { + value: 1, + }, + }, + }); + } + + if (visParams.thresholdLine.show as boolean) { + const layer = { + mark: { + type: 'rule', + color: visParams.thresholdLine.color, + strokeDash: [1, 0], + }, + encoding: { + y: { + datum: visParams.thresholdLine.value, + }, + }, + }; + + // Can only support making a threshold line with full or dashed style, but not dot-dashed + // due to vega-lite limitations + if (visParams.thresholdLine.style !== 'full') { + layer.mark.strokeDash = [8, 8]; + } + spec.layer.push(layer); + } + return spec; +}; diff --git a/src/plugins/vis_type_vega/public/expressions/index.ts b/src/plugins/vis_type_vega/public/expressions/index.ts new file mode 100644 index 00000000000..dce44f56c47 --- /dev/null +++ b/src/plugins/vis_type_vega/public/expressions/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { LineVegaSpecExpressionFunctionDefinition } from './line_vega_spec_fn'; +export { VegaExpressionFunctionDefinition } from './vega_fn'; diff --git a/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.test.js b/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.test.js deleted file mode 100644 index 2260c46b841..00000000000 --- a/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.test.js +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - buildLayerMark, - buildXAxis, - buildYAxis, - cleanString, - createSpecFromDatatable, - formatDataTable, - setupConfig, -} from './line_vega_spec_fn'; - -describe('cleanString()', function () { - it('string should not contain "', function () { - const dirtyString = '"someString"'; - expect(cleanString(dirtyString)).toBe('someString'); - }); -}); - -describe('setupConfig()', function () { - it('check all legend positions', function () { - const baseConfig = { - view: { - stroke: null, - }, - concat: { - spacing: 0, - }, - legend: { - orient: null, - }, - }; - const positions = ['top', 'right', 'left', 'bottom']; - positions.forEach((position) => { - const visParams = { legendPosition: position }; - baseConfig.legend.orient = position; - expect(setupConfig(visParams)).toStrictEqual(baseConfig); - }); - }); -}); - -describe('buildLayerMark()', function () { - const types = ['line', 'area', 'histogram']; - const interpolates = ['linear', 'cardinal', 'step-after']; - const strokeWidths = [-1, 0, 1, 2, 3, 4]; - const showCircles = [false, true]; - - it('check each mark possible value', function () { - const mark = { - type: null, - interpolate: null, - strokeWidth: null, - point: null, - }; - types.forEach((type) => { - mark.type = type; - interpolates.forEach((interpolate) => { - mark.interpolate = interpolate; - strokeWidths.forEach((strokeWidth) => { - mark.strokeWidth = strokeWidth; - showCircles.forEach((showCircle) => { - mark.point = showCircle; - const param = { - type: type, - interpolate: interpolate, - lineWidth: strokeWidth, - showCircles: showCircle, - }; - expect(buildLayerMark(param)).toStrictEqual(mark); - }); - }); - }); - }); - }); -}); - -describe('buildXAxis()', function () { - it('build different XAxis', function () { - const xAxisTitle = 'someTitle'; - const xAxisId = 'someId'; - const startTime = 1676596400; - const endTime = 1676796400; - [true, false].forEach((enableGrid) => { - const visParams = { grid: { categoryLines: enableGrid } }; - const vegaXAxis = { - axis: { - title: xAxisTitle, - grid: enableGrid, - }, - field: xAxisId, - type: 'temporal', - scale: { - domain: [startTime, endTime], - }, - }; - expect(buildXAxis(xAxisTitle, xAxisId, startTime, endTime, visParams)).toStrictEqual( - vegaXAxis - ); - }); - }); -}); - -describe('buildYAxis()', function () { - it('build different YAxis', function () { - const valueAxis = { - id: 'someId', - labels: { - rotate: 75, - show: false, - }, - position: 'left', - title: { - text: 'someText', - }, - }; - const column = { name: 'columnName', id: 'columnId' }; - const visParams = { grid: { valueAxis: true } }; - const vegaYAxis = { - axis: { - title: 'someText', - grid: true, - orient: 'left', - labels: false, - labelAngle: 75, - }, - field: 'columnId', - type: 'quantitative', - }; - expect(buildYAxis(column, valueAxis, visParams)).toStrictEqual(vegaYAxis); - - valueAxis.title.text = '""'; - vegaYAxis.axis.title = 'columnName'; - expect(buildYAxis(column, valueAxis, visParams)).toStrictEqual(vegaYAxis); - }); -}); - -describe('createSpecFromDatatable()', function () { - it('build simple line chart"', function () { - const datatable = - '{"type":"opensearch_dashboards_datatable","rows":[{"col-0-2":1672214400000,"col-1-1":44},{"col-0-2":1672300800000,"col-1-1":150},{"col-0-2":1672387200000,"col-1-1":154},{"col-0-2":1672473600000,"col-1-1":144},{"col-0-2":1672560000000,"col-1-1":133},{"col-0-2":1672646400000,"col-1-1":149},{"col-0-2":1672732800000,"col-1-1":152},{"col-0-2":1672819200000,"col-1-1":144},{"col-0-2":1672905600000,"col-1-1":166},{"col-0-2":1672992000000,"col-1-1":151},{"col-0-2":1673078400000,"col-1-1":143},{"col-0-2":1673164800000,"col-1-1":148},{"col-0-2":1673251200000,"col-1-1":146},{"col-0-2":1673337600000,"col-1-1":137},{"col-0-2":1673424000000,"col-1-1":152},{"col-0-2":1673510400000,"col-1-1":152},{"col-0-2":1673596800000,"col-1-1":151},{"col-0-2":1673683200000,"col-1-1":157},{"col-0-2":1673769600000,"col-1-1":151},{"col-0-2":1673856000000,"col-1-1":152},{"col-0-2":1673942400000,"col-1-1":142},{"col-0-2":1674028800000,"col-1-1":151},{"col-0-2":1674115200000,"col-1-1":163},{"col-0-2":1674201600000,"col-1-1":156},{"col-0-2":1674288000000,"col-1-1":153},{"col-0-2":1674374400000,"col-1-1":162},{"col-0-2":1674460800000,"col-1-1":152},{"col-0-2":1674547200000,"col-1-1":159},{"col-0-2":1674633600000,"col-1-1":165},{"col-0-2":1674720000000,"col-1-1":153},{"col-0-2":1674806400000,"col-1-1":149},{"col-0-2":1674892800000,"col-1-1":94}],"columns":[{"id":"col-0-2","name":"order_date per day","meta":{"type":"date_histogram","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{"field":"order_date","timeRange":{"from":"now-90d","to":"now"},"useNormalizedOpenSearchInterval":true,"scaleMetricValues":false,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}}},{"id":"col-1-1","name":"Count","meta":{"type":"count","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{}}}]}'; - const visParams = - '{"addLegend":true,"addTimeMarker":false,"addTooltip":true,"categoryAxes":[{"id":"CategoryAxis-1","labels":{"filter":true,"show":true,"truncate":100},"position":"bottom","scale":{"type":"linear"},"show":true,"style":{},"title":{},"type":"category"}],"grid":{"categoryLines":false},"labels":{},"legendPosition":"right","seriesParams":[{"data":{"id":"1","label":"Count"},"drawLinesBetweenPoints":true,"interpolate":"linear","lineWidth":2,"mode":"normal","show":true,"showCircles":true,"type":"line","valueAxis":"ValueAxis-1"}],"thresholdLine":{"color":"#E7664C","show":false,"style":"full","value":10,"width":1},"times":[],"type":"line","valueAxes":[{"id":"ValueAxis-1","labels":{"filter":false,"rotate":0,"show":true,"truncate":100},"name":"LeftAxis-1","position":"left","scale":{"mode":"normal","type":"linear"},"show":true,"style":{},"title":{"text":"Count"},"type":"value"}]}'; - const dimensions = - '{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD"}},"params":{"date":true,"interval":"P1D","intervalESValue":1,"intervalESUnit":"d","format":"YYYY-MM-DD","bounds":{"min":"2022-11-18T00:14:09.617Z","max":"2023-02-16T00:14:09.617Z"}},"label":"order_date per day","aggType":"date_histogram"},"y":[{"accessor":1,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"}]}'; - const spec = - '{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","data":{"values":[{"col-0-2":1672214400000,"col-1-1":44},{"col-0-2":1672300800000,"col-1-1":150},{"col-0-2":1672387200000,"col-1-1":154},{"col-0-2":1672473600000,"col-1-1":144},{"col-0-2":1672560000000,"col-1-1":133},{"col-0-2":1672646400000,"col-1-1":149},{"col-0-2":1672732800000,"col-1-1":152},{"col-0-2":1672819200000,"col-1-1":144},{"col-0-2":1672905600000,"col-1-1":166},{"col-0-2":1672992000000,"col-1-1":151},{"col-0-2":1673078400000,"col-1-1":143},{"col-0-2":1673164800000,"col-1-1":148},{"col-0-2":1673251200000,"col-1-1":146},{"col-0-2":1673337600000,"col-1-1":137},{"col-0-2":1673424000000,"col-1-1":152},{"col-0-2":1673510400000,"col-1-1":152},{"col-0-2":1673596800000,"col-1-1":151},{"col-0-2":1673683200000,"col-1-1":157},{"col-0-2":1673769600000,"col-1-1":151},{"col-0-2":1673856000000,"col-1-1":152},{"col-0-2":1673942400000,"col-1-1":142},{"col-0-2":1674028800000,"col-1-1":151},{"col-0-2":1674115200000,"col-1-1":163},{"col-0-2":1674201600000,"col-1-1":156},{"col-0-2":1674288000000,"col-1-1":153},{"col-0-2":1674374400000,"col-1-1":162},{"col-0-2":1674460800000,"col-1-1":152},{"col-0-2":1674547200000,"col-1-1":159},{"col-0-2":1674633600000,"col-1-1":165},{"col-0-2":1674720000000,"col-1-1":153},{"col-0-2":1674806400000,"col-1-1":149},{"col-0-2":1674892800000,"col-1-1":94}]},"config":{"view":{"stroke":null},"concat":{"spacing":0},"legend":{"orient":"right"}},"layer":[{"mark":{"type":"line","interpolate":"linear","strokeWidth":2,"point":true},"encoding":{"x":{"axis":{"title":"order_date per day","grid":false},"field":"col-0-2","type":"temporal","scale":{"domain":[1668730449617,1676506449617]}},"y":{"axis":{"title":"Count","orient":"left","labels":true,"labelAngle":0},"field":"col-1-1","type":"quantitative"},"tooltip":[{"field":"col-0-2","type":"temporal","title":"order_date per day"},{"field":"col-1-1","type":"quantitative","title":"Count"}],"color":{"datum":"Count"}}}]}'; - expect( - JSON.stringify( - createSpecFromDatatable( - formatDataTable(JSON.parse(datatable)), - JSON.parse(visParams), - JSON.parse(dimensions) - ) - ) - ).toBe(spec); - }); - - it('build empty chart if no x-axis is defined"', function () { - const datatable = - '{"type":"opensearch_dashboards_datatable","rows":[{"col-0-1":4675}],"columns":[{"id":"col-0-1","name":"Count","meta":{"type":"count","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{}}}]}'; - const visParams = - '{"type":"line","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":true,"type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"lineWidth":2,"interpolate":"linear","showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{},"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#E7664C"}}'; - const dimensions = - '{"x":null,"y":[{"accessor":0,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"}]}'; - const spec = - '{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","data":{"values":[{"col-0-1":4675}]},"config":{"view":{"stroke":null},"concat":{"spacing":0},"legend":{"orient":"right"}},"layer":[]}'; - expect( - JSON.stringify( - createSpecFromDatatable( - formatDataTable(JSON.parse(datatable)), - JSON.parse(visParams), - JSON.parse(dimensions) - ) - ) - ).toBe(spec); - }); - - it('build complicated line chart"', function () { - const datatable = - '{"type":"opensearch_dashboards_datatable","rows":[{"col-0-2":1672214400000,"col-1-1":44,"col-2-3":60.9375},{"col-0-2":1672300800000,"col-1-1":150,"col-2-3":82.5},{"col-0-2":1672387200000,"col-1-1":154,"col-2-3":79.5},{"col-0-2":1672473600000,"col-1-1":144,"col-2-3":75.875},{"col-0-2":1672560000000,"col-1-1":133,"col-2-3":259.25},{"col-0-2":1672646400000,"col-1-1":149,"col-2-3":90},{"col-0-2":1672732800000,"col-1-1":152,"col-2-3":79.0625},{"col-0-2":1672819200000,"col-1-1":144,"col-2-3":82.5},{"col-0-2":1672905600000,"col-1-1":166,"col-2-3":85.25},{"col-0-2":1672992000000,"col-1-1":151,"col-2-3":92},{"col-0-2":1673078400000,"col-1-1":143,"col-2-3":90.75},{"col-0-2":1673164800000,"col-1-1":148,"col-2-3":92},{"col-0-2":1673251200000,"col-1-1":146,"col-2-3":83.25},{"col-0-2":1673337600000,"col-1-1":137,"col-2-3":98},{"col-0-2":1673424000000,"col-1-1":152,"col-2-3":83.6875},{"col-0-2":1673510400000,"col-1-1":152,"col-2-3":83.6875},{"col-0-2":1673596800000,"col-1-1":151,"col-2-3":87.4375},{"col-0-2":1673683200000,"col-1-1":157,"col-2-3":63.75},{"col-0-2":1673769600000,"col-1-1":151,"col-2-3":81.5625},{"col-0-2":1673856000000,"col-1-1":152,"col-2-3":100.6875},{"col-0-2":1673942400000,"col-1-1":142,"col-2-3":98},{"col-0-2":1674028800000,"col-1-1":151,"col-2-3":100.8125},{"col-0-2":1674115200000,"col-1-1":163,"col-2-3":83.6875},{"col-0-2":1674201600000,"col-1-1":156,"col-2-3":85.8125},{"col-0-2":1674288000000,"col-1-1":153,"col-2-3":98},{"col-0-2":1674374400000,"col-1-1":162,"col-2-3":75.9375},{"col-0-2":1674460800000,"col-1-1":152,"col-2-3":113.375},{"col-0-2":1674547200000,"col-1-1":159,"col-2-3":73.625},{"col-0-2":1674633600000,"col-1-1":165,"col-2-3":72.8125},{"col-0-2":1674720000000,"col-1-1":153,"col-2-3":113.375},{"col-0-2":1674806400000,"col-1-1":149,"col-2-3":82.5},{"col-0-2":1674892800000,"col-1-1":94,"col-2-3":54}],"columns":[{"id":"col-0-2","name":"order_date per day","meta":{"type":"date_histogram","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{"field":"order_date","timeRange":{"from":"now-90d","to":"now"},"useNormalizedOpenSearchInterval":true,"scaleMetricValues":false,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}}},{"id":"col-1-1","name":"Count","meta":{"type":"count","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{}}},{"id":"col-2-3","name":"Max products.min_price","meta":{"type":"max","indexPatternId":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","aggConfigParams":{"field":"products.min_price"}}}]}'; - const visParams = - '{"addLegend":true,"addTimeMarker":true,"addTooltip":true,"categoryAxes":[{"id":"CategoryAxis-1","labels":{"filter":true,"show":true,"truncate":100},"position":"bottom","scale":{"type":"linear"},"show":true,"style":{},"title":{},"type":"category"}],"grid":{"categoryLines":false,"valueAxis":"ValueAxis-1"},"labels":{},"legendPosition":"bottom","seriesParams":[{"data":{"id":"1","label":"Count"},"drawLinesBetweenPoints":true,"interpolate":"linear","lineWidth":2,"mode":"normal","show":true,"showCircles":true,"type":"line","valueAxis":"ValueAxis-1"},{"data":{"id":"3","label":"Max products.min_price"},"drawLinesBetweenPoints":true,"interpolate":"linear","lineWidth":2,"mode":"normal","show":true,"showCircles":true,"type":"line","valueAxis":"ValueAxis-1"}],"thresholdLine":{"color":"#E7664C","show":true,"style":"dashed","value":100,"width":1},"times":[],"type":"line","valueAxes":[{"id":"ValueAxis-1","labels":{"filter":false,"rotate":75,"show":true,"truncate":100},"name":"RightAxis-1","position":"right","scale":{"mode":"normal","type":"linear"},"show":true,"style":{},"title":{"text":"Count"},"type":"value"}]}'; - const dimensions = - '{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD"}},"params":{"date":true,"interval":"P1D","intervalESValue":1,"intervalESUnit":"d","format":"YYYY-MM-DD","bounds":{"min":"2022-11-19T03:26:04.730Z","max":"2023-02-17T03:26:04.730Z"}},"label":"order_date per day","aggType":"date_histogram"},"y":[{"accessor":1,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"},{"accessor":2,"format":{"id":"number","params":{"parsedUrl":{"origin":"http://localhost:5603","pathname":"/rao/app/visualize","basePath":"/rao"}}},"params":{},"label":"Max products.min_price","aggType":"max"}]}'; - const spec = - '{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","data":{"values":[{"col-0-2":1672214400000,"col-1-1":44,"col-2-3":60.9375},{"col-0-2":1672300800000,"col-1-1":150,"col-2-3":82.5},{"col-0-2":1672387200000,"col-1-1":154,"col-2-3":79.5},{"col-0-2":1672473600000,"col-1-1":144,"col-2-3":75.875},{"col-0-2":1672560000000,"col-1-1":133,"col-2-3":259.25},{"col-0-2":1672646400000,"col-1-1":149,"col-2-3":90},{"col-0-2":1672732800000,"col-1-1":152,"col-2-3":79.0625},{"col-0-2":1672819200000,"col-1-1":144,"col-2-3":82.5},{"col-0-2":1672905600000,"col-1-1":166,"col-2-3":85.25},{"col-0-2":1672992000000,"col-1-1":151,"col-2-3":92},{"col-0-2":1673078400000,"col-1-1":143,"col-2-3":90.75},{"col-0-2":1673164800000,"col-1-1":148,"col-2-3":92},{"col-0-2":1673251200000,"col-1-1":146,"col-2-3":83.25},{"col-0-2":1673337600000,"col-1-1":137,"col-2-3":98},{"col-0-2":1673424000000,"col-1-1":152,"col-2-3":83.6875},{"col-0-2":1673510400000,"col-1-1":152,"col-2-3":83.6875},{"col-0-2":1673596800000,"col-1-1":151,"col-2-3":87.4375},{"col-0-2":1673683200000,"col-1-1":157,"col-2-3":63.75},{"col-0-2":1673769600000,"col-1-1":151,"col-2-3":81.5625},{"col-0-2":1673856000000,"col-1-1":152,"col-2-3":100.6875},{"col-0-2":1673942400000,"col-1-1":142,"col-2-3":98},{"col-0-2":1674028800000,"col-1-1":151,"col-2-3":100.8125},{"col-0-2":1674115200000,"col-1-1":163,"col-2-3":83.6875},{"col-0-2":1674201600000,"col-1-1":156,"col-2-3":85.8125},{"col-0-2":1674288000000,"col-1-1":153,"col-2-3":98},{"col-0-2":1674374400000,"col-1-1":162,"col-2-3":75.9375},{"col-0-2":1674460800000,"col-1-1":152,"col-2-3":113.375},{"col-0-2":1674547200000,"col-1-1":159,"col-2-3":73.625},{"col-0-2":1674633600000,"col-1-1":165,"col-2-3":72.8125},{"col-0-2":1674720000000,"col-1-1":153,"col-2-3":113.375},{"col-0-2":1674806400000,"col-1-1":149,"col-2-3":82.5},{"col-0-2":1674892800000,"col-1-1":94,"col-2-3":54}]},"config":{"view":{"stroke":null},"concat":{"spacing":0},"legend":{"orient":"bottom"}},"layer":[{"mark":{"type":"line","interpolate":"linear","strokeWidth":2,"point":true},"encoding":{"x":{"axis":{"title":"order_date per day","grid":false},"field":"col-0-2","type":"temporal","scale":{"domain":[1668828364730,1676604364730]}},"y":{"axis":{"title":"Count","grid":"ValueAxis-1","orient":"right","labels":true,"labelAngle":75},"field":"col-1-1","type":"quantitative"},"tooltip":[{"field":"col-0-2","type":"temporal","title":"order_date per day"},{"field":"col-1-1","type":"quantitative","title":"Count"}],"color":{"datum":"Count"}}},{"mark":{"type":"line","interpolate":"linear","strokeWidth":2,"point":true},"encoding":{"x":{"axis":{"title":"order_date per day","grid":false},"field":"col-0-2","type":"temporal","scale":{"domain":[1668828364730,1676604364730]}},"y":{"axis":{"title":"Count","grid":"ValueAxis-1","orient":"right","labels":true,"labelAngle":75},"field":"col-2-3","type":"quantitative"},"tooltip":[{"field":"col-0-2","type":"temporal","title":"order_date per day"},{"field":"col-2-3","type":"quantitative","title":"Max products.min_price"}],"color":{"datum":"Max products.min_price"}}},{"mark":"rule","encoding":{"x":{"type":"temporal","field":"now_field"},"color":{"value":"red"},"size":{"value":1}}},{"mark":{"type":"rule","color":"#E7664C","strokeDash":[8,8]},"encoding":{"y":{"datum":100}}}],"transform":[{"calculate":"now()","as":"now_field"}]}'; - expect( - JSON.stringify( - createSpecFromDatatable( - formatDataTable(JSON.parse(datatable)), - JSON.parse(visParams), - JSON.parse(dimensions) - ) - ) - ).toBe(spec); - }); -}); diff --git a/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.ts b/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.ts index b0f440e5964..499310c106d 100644 --- a/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.ts +++ b/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.ts @@ -3,15 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, isEmpty } from 'lodash'; import { i18n } from '@osd/i18n'; import { ExpressionFunctionDefinition, OpenSearchDashboardsDatatable, - OpenSearchDashboardsDatatableColumn, } from '../../../expressions/public'; -import { VegaVisualizationDependencies } from '../plugin'; import { VislibDimensions, VisParams } from '../../../visualizations/public'; +import { + VisLayer, + VisLayers, + PointInTimeEventsVisLayer, + isPointInTimeEventsVisLayer, + addPointInTimeEventsLayersToTable, + addPointInTimeEventsLayersToSpec, + enableVisLayersInSpecConfig, +} from '../../../vis_augmenter/public'; +import { formatDatatable, createSpecFromDatatable } from './helpers'; +import { VegaVisualizationDependencies } from '../plugin'; type Input = OpenSearchDashboardsDatatable; type Output = Promise; @@ -29,236 +38,6 @@ export type LineVegaSpecExpressionFunctionDefinition = ExpressionFunctionDefinit Output >; -// TODO: move this to the visualization plugin that has VisParams once all of these parameters have been better defined -interface ValueAxis { - id: string; - labels: { - filter: boolean; - rotate: number; - show: boolean; - truncate: number; - }; - name: string; - position: string; - scale: { - mode: string; - type: string; - }; - show: true; - style: any; - title: { - text: string; - }; - type: string; -} - -// 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 -const getXAxisId = (dimensions: any, columns: OpenSearchDashboardsDatatableColumn[]): string => { - return columns.filter((column) => column.name === dimensions.x.label)[0].id; -}; - -export const cleanString = (rawString: string): string => { - return rawString.replaceAll('"', ''); -}; - -export const formatDataTable = ( - datatable: OpenSearchDashboardsDatatable -): OpenSearchDashboardsDatatable => { - datatable.columns.forEach((column) => { - // clean quotation marks from names in columns - column.name = cleanString(column.name); - }); - return datatable; -}; - -export const setupConfig = (visParams: VisParams) => { - const legendPosition = visParams.legendPosition; - return { - view: { - stroke: null, - }, - concat: { - spacing: 0, - }, - legend: { - orient: legendPosition, - }, - }; -}; - -export const buildLayerMark = (seriesParams: { - type: string; - interpolate: string; - lineWidth: number; - showCircles: boolean; -}) => { - return { - // Possible types are: line, area, histogram. The eligibility checker will - // prevent area and histogram (though area works in vega-lite) - type: seriesParams.type, - // Possible types: linear, cardinal, step-after. All of these types work in vega-lite - interpolate: seriesParams.interpolate, - // The possible values is any number, which matches what vega-lite supports - strokeWidth: seriesParams.lineWidth, - // this corresponds to showing the dots in the visbuilder for each data point - point: seriesParams.showCircles, - }; -}; - -export const buildXAxis = ( - xAxisTitle: string, - xAxisId: string, - startTime: number, - endTime: number, - visParams: VisParams -) => { - return { - axis: { - title: xAxisTitle, - grid: visParams.grid.categoryLines, - }, - field: xAxisId, - // Right now, the line charts can only set the x-axis value to be a date attribute, so - // this should always be of type temporal - type: 'temporal', - scale: { - domain: [startTime, endTime], - }, - }; -}; - -export const buildYAxis = ( - column: OpenSearchDashboardsDatatableColumn, - valueAxis: ValueAxis, - visParams: VisParams -) => { - return { - axis: { - title: cleanString(valueAxis.title.text) || column.name, - grid: visParams.grid.valueAxis, - orient: valueAxis.position, - labels: valueAxis.labels.show, - labelAngle: valueAxis.labels.rotate, - }, - field: column.id, - type: 'quantitative', - }; -}; - -export const createSpecFromDatatable = ( - datatable: OpenSearchDashboardsDatatable, - visParams: VisParams, - dimensions: VislibDimensions -): 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. - const spec = {} as any; - - spec.$schema = 'https://vega.github.io/schema/vega-lite/v5.json'; - spec.data = { - values: datatable.rows, - }; - spec.config = setupConfig(visParams); - - // Get the valueAxes data and generate a map to easily fetch the different valueAxes data - const valueAxis = new Map(); - visParams?.valueAxes?.forEach((yAxis: ValueAxis) => { - valueAxis.set(yAxis.id, yAxis); - }); - - spec.layer = [] as any[]; - - if (datatable.rows.length > 0 && dimensions.x !== null) { - const xAxisId = getXAxisId(dimensions, datatable.columns); - const xAxisTitle = cleanString(dimensions.x.label); - // get x-axis bounds for the chart - const startTime = new Date(dimensions.x.params.bounds.min).valueOf(); - const endTime = new Date(dimensions.x.params.bounds.max).valueOf(); - let skip = 0; - datatable.columns.forEach((column, index) => { - // Check if it's not xAxis column data - if (column.meta?.aggConfigParams?.interval !== undefined) { - skip++; - } else { - const currentSeriesParams = visParams.seriesParams[index - skip]; - const currentValueAxis = valueAxis.get(currentSeriesParams.valueAxis.toString()); - let tooltip: Array<{ field: string; type: string; title: string }> = []; - if (visParams.addTooltip) { - tooltip = [ - { field: xAxisId, type: 'temporal', title: xAxisTitle }, - { field: column.id, type: 'quantitative', title: column.name }, - ]; - } - spec.layer.push({ - mark: buildLayerMark(currentSeriesParams), - encoding: { - x: buildXAxis(xAxisTitle, xAxisId, startTime, endTime, visParams), - y: buildYAxis(column, currentValueAxis, visParams), - tooltip, - color: { - // This ensures all the different metrics have their own distinct and unique color - datum: column.name, - }, - }, - }); - } - }); - } - - if (visParams.addTimeMarker) { - spec.transform = [ - { - calculate: 'now()', - as: 'now_field', - }, - ]; - - spec.layer.push({ - mark: 'rule', - encoding: { - x: { - type: 'temporal', - field: 'now_field', - }, - // The time marker on vislib is red, so keeping this consistent - color: { - value: 'red', - }, - size: { - value: 1, - }, - }, - }); - } - - if (visParams.thresholdLine.show as boolean) { - const layer = { - mark: { - type: 'rule', - color: visParams.thresholdLine.color, - strokeDash: [1, 0], - }, - encoding: { - y: { - datum: visParams.thresholdLine.value, - }, - }, - }; - - // Can only support making a threshold line with full or dashed style, but not dot-dashed - // due to vega-lite limitations - if (visParams.thresholdLine.style !== 'full') { - layer.mark.strokeDash = [8, 8]; - } - - spec.layer.push(layer); - } - - return spec; -}; - export const createLineVegaSpecFn = ( dependencies: VegaVisualizationDependencies ): LineVegaSpecExpressionFunctionDefinition => ({ @@ -286,14 +65,27 @@ export const createLineVegaSpecFn = ( }, }, async fn(input, args, context) { - const table = cloneDeep(input); + let table = formatDatatable(cloneDeep(input)); + + const visParams = JSON.parse(args.visParams) as VisParams; + const dimensions = JSON.parse(args.dimensions) as VislibDimensions; + const allVisLayers = (args.visLayers ? JSON.parse(args.visLayers) : []) as VisLayers; + + // currently only supporting PointInTimeEventsVisLayer type + const pointInTimeEventsVisLayers = allVisLayers.filter((visLayer: VisLayer) => + isPointInTimeEventsVisLayer(visLayer) + ) as PointInTimeEventsVisLayer[]; - // creating initial vega spec from table - const spec = createSpecFromDatatable( - formatDataTable(table), - JSON.parse(args.visParams), - JSON.parse(args.dimensions) - ); + if (!isEmpty(pointInTimeEventsVisLayers) && dimensions.x !== null) { + table = addPointInTimeEventsLayersToTable(table, dimensions, pointInTimeEventsVisLayers); + } + + let spec = createSpecFromDatatable(table, visParams, dimensions); + + if (!isEmpty(pointInTimeEventsVisLayers) && dimensions.x !== null) { + spec = addPointInTimeEventsLayersToSpec(table, dimensions, spec); + spec.config = enableVisLayersInSpecConfig(spec, pointInTimeEventsVisLayers); + } return JSON.stringify(spec); }, }); diff --git a/src/plugins/vis_type_vega/public/index.ts b/src/plugins/vis_type_vega/public/index.ts index ed0c794837d..da9b8b396db 100644 --- a/src/plugins/vis_type_vega/public/index.ts +++ b/src/plugins/vis_type_vega/public/index.ts @@ -36,5 +36,4 @@ export function plugin(initializerContext: PluginInitializerContext { const vegaSpecFn = buildExpressionFunction( 'line_vega_spec', { - visLayers: JSON.stringify([]), + visLayers: JSON.stringify(params.visLayers), visParams: JSON.stringify(vis.params), dimensions: JSON.stringify(dimensions), } diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 48f1d830634..ec0fc098189 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -54,13 +54,13 @@ import { setExpressions, setUiActions, setSavedVisualizationsLoader, + setSavedAugmentVisLoader, setTimeFilter, setAggs, setChrome, setOverlays, setSavedSearchLoader, setEmbeddable, - setSavedAugmentVisLoader, } from './services'; import { VISUALIZE_EMBEDDABLE_TYPE, @@ -195,30 +195,30 @@ export class VisualizationsPlugin setAggs(data.search.aggs); setOverlays(core.overlays); setChrome(core.chrome); - const savedAugmentVisLoader = createSavedAugmentVisLoader({ + const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, search: data.search, chrome: core.chrome, overlays: core.overlays, + visualizationTypes: types, }); - setSavedAugmentVisLoader(savedAugmentVisLoader); - const savedVisualizationsLoader = createSavedVisLoader({ + setSavedVisualizationsLoader(savedVisualizationsLoader); + const savedSearchLoader = createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, search: data.search, chrome: core.chrome, overlays: core.overlays, - visualizationTypes: types, }); - setSavedVisualizationsLoader(savedVisualizationsLoader); - const savedSearchLoader = createSavedSearchesLoader({ + const savedAugmentVisLoader = createSavedAugmentVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, search: data.search, chrome: core.chrome, overlays: core.overlays, }); + setSavedAugmentVisLoader(savedAugmentVisLoader); setSavedSearchLoader(savedSearchLoader); return { ...types,