From edef1366b5e255cf8c72593516114e1c056fcb67 Mon Sep 17 00:00:00 2001 From: Irina Kuzmina Date: Mon, 13 Nov 2023 18:10:54 +0200 Subject: [PATCH] Add support chart-chart filtering in wizard (#170) * Add support chart-chart filtering in wizard * fix point selection * prepare actionParams data for scatter and pie visualizations * add custom data for treemap visualization * fix mock for unit tests * fix treemap * code review fix(1) * move some parameters from a point to a series * remove unused flag * code review fix(2) * code review fix(3) * code review fix(3.1) --- .../WizardChartChartFilteringAvailable.ts | 10 + .../url/distincts/build-distincts-body.ts | 4 +- .../modes/charts/plugins/datalens/config.ts | 23 +- .../datalens/preparers/bar-x/highcharts.ts | 1 - .../datalens/preparers/bar-x/prepareBarX.ts | 35 +- .../line/__tests__/mocks/bar.mock.ts | 26 +- .../plugins/datalens/preparers/line/index.ts | 52 ++- .../preparers/pie/__tests__/mocks/pie.mock.ts | 6 +- .../datalens/preparers/pie/preparePie.ts | 15 + .../scatter/__tests__/mocks/scatter.mock.ts | 18 +- .../preparers/scatter/prepareScatter.ts | 56 ++- .../plugins/datalens/preparers/treemap.ts | 20 +- .../plugins/datalens/preparers/types.ts | 1 + src/shared/modules/helpers.ts | 19 +- src/shared/types/chartkit/dl-chartkit.ts | 13 + src/shared/types/feature.ts | 1 + src/shared/types/widget.ts | 2 +- src/ui/components/Widgets/Chart/Chart.tsx | 3 +- src/ui/components/Widgets/Chart/types.ts | 1 + src/ui/constants/common.ts | 1 + .../helpers/action-params-handlers.ts | 341 ++++++++++++++++++ .../ChartKit/helpers/apply-hc-handlers.ts | 233 +----------- .../ChartKit/helpers/chartkitAdapter.ts | 5 +- .../ChartKit/helpers/helpers.test.ts | 32 +- .../ChartKit/helpers/types.ts | 6 + .../ChartKit/helpers/utils.test.ts | 37 ++ .../ChartKit/helpers/utils.ts | 45 +++ src/ui/libs/DatalensChartkit/types/widget.ts | 15 +- .../dash/containers/Dialogs/Widget/Widget.tsx | 15 +- .../preview/components/Preview/Preview.tsx | 3 +- src/ui/utils/utils.ts | 1 + 31 files changed, 739 insertions(+), 301 deletions(-) create mode 100644 src/server/components/features/features-list/WizardChartChartFilteringAvailable.ts create mode 100644 src/ui/libs/DatalensChartkit/ChartKit/helpers/action-params-handlers.ts create mode 100644 src/ui/libs/DatalensChartkit/ChartKit/helpers/types.ts create mode 100644 src/ui/libs/DatalensChartkit/ChartKit/helpers/utils.test.ts create mode 100644 src/ui/libs/DatalensChartkit/ChartKit/helpers/utils.ts diff --git a/src/server/components/features/features-list/WizardChartChartFilteringAvailable.ts b/src/server/components/features/features-list/WizardChartChartFilteringAvailable.ts new file mode 100644 index 0000000000..c8ab719c2b --- /dev/null +++ b/src/server/components/features/features-list/WizardChartChartFilteringAvailable.ts @@ -0,0 +1,10 @@ +import {Feature} from '../../../../shared'; +import {createFeatureConfig} from '../utils'; + +export default createFeatureConfig({ + name: Feature.WizardChartChartFilteringAvailable, + state: { + development: false, + production: false, + }, +}); diff --git a/src/server/modes/charts/plugins/control/url/distincts/build-distincts-body.ts b/src/server/modes/charts/plugins/control/url/distincts/build-distincts-body.ts index 55affb9a32..1a8957ee5f 100644 --- a/src/server/modes/charts/plugins/control/url/distincts/build-distincts-body.ts +++ b/src/server/modes/charts/plugins/control/url/distincts/build-distincts-body.ts @@ -152,7 +152,7 @@ export const getDistinctsRequestBody = ({ ); operation = valuesWithOperations.find((item) => item && item.operation)?.operation; - if (values.length === 1 && values[0].startsWith('__relative')) { + if (values.length === 1 && String(values[0]).startsWith('__relative')) { const resolvedRelative = resolveRelativeDate(values[0]); if (resolvedRelative) { @@ -160,7 +160,7 @@ export const getDistinctsRequestBody = ({ } } - if (values.length === 1 && values[0].startsWith('__interval')) { + if (values.length === 1 && String(values[0]).startsWith('__interval')) { const resolvedInterval = ChartEditor.resolveInterval(values[0]); if (resolvedInterval) { diff --git a/src/server/modes/charts/plugins/datalens/config.ts b/src/server/modes/charts/plugins/datalens/config.ts index b263ebd646..affef523dd 100644 --- a/src/server/modes/charts/plugins/datalens/config.ts +++ b/src/server/modes/charts/plugins/datalens/config.ts @@ -3,12 +3,15 @@ import type {HighchartsWidgetData} from '@gravity-ui/chartkit/highcharts'; import { ChartkitHandlers, DEFAULT_CHART_LINES_LIMIT, + DashWidgetConfig, Feature, GraphTooltipLine, + GraphWidgetEventScope, PlaceholderId, ServerChartsConfig, ServerCommonSharedExtraSettings, StringParams, + WidgetEvent, WizardVisualizationId, getIsNavigatorEnabled, isEnabledServerFeature, @@ -61,6 +64,9 @@ type GraphConfig = BaseConfig & navigatorSettings?: ServerCommonSharedExtraSettings['navigatorSettings']; calcClosestPointManually?: boolean; enableGPTInsights?: ServerCommonSharedExtraSettings['enableGPTInsights']; + events?: { + click?: WidgetEvent | WidgetEvent[]; + }; }; type TableConfig = BaseConfig & { @@ -83,16 +89,25 @@ export type Config = GraphConfig | TableConfig | MetricConfig; // eslint-disable-next-line complexity export default ( ...options: [ - {shared: ServerChartsConfig; params: StringParams} | ServerChartsConfig, + ( + | { + shared: ServerChartsConfig; + params: StringParams; + widgetConfig?: DashWidgetConfig['widgetConfig']; + } + | ServerChartsConfig + ), StringParams, ] ) => { let shared; let params: StringParams; + let widgetConfig: DashWidgetConfig['widgetConfig']; if ('shared' in options[0]) { shared = options[0].shared; params = options[0].params as StringParams; + widgetConfig = options[0].widgetConfig; } else { shared = options[0]; params = options[1]; @@ -217,6 +232,12 @@ export default ( config.calcClosestPointManually = true; } + if (widgetConfig?.actionParams?.enable) { + config.events = { + click: [{handler: {type: 'setActionParams'}, scope: 'point'}], + }; + } + log('CONFIG:'); log(config); diff --git a/src/server/modes/charts/plugins/datalens/preparers/bar-x/highcharts.ts b/src/server/modes/charts/plugins/datalens/preparers/bar-x/highcharts.ts index a28ee496b2..a1d69ac01f 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/bar-x/highcharts.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/bar-x/highcharts.ts @@ -49,7 +49,6 @@ export function prepareHighchartsBarX(args: PrepareFunctionArgs) { segments, } = args; const {data, order} = resultData; - const preparedData = prepareBarX(args); const {graphs} = preparedData; diff --git a/src/server/modes/charts/plugins/datalens/preparers/bar-x/prepareBarX.ts b/src/server/modes/charts/plugins/datalens/preparers/bar-x/prepareBarX.ts index cd9442d23e..9e99bece23 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/bar-x/prepareBarX.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/bar-x/prepareBarX.ts @@ -9,6 +9,7 @@ import { getAxisMode, getFakeTitleOrTitle, isDateField, + isDimensionField, isMeasureField, isMeasureValue, isPercentVisualization, @@ -54,9 +55,11 @@ export function prepareBarX(args: PrepareFunctionArgs) { segments, layerChartMeta, usedColors, + ChartEditor, } = args; const {data, order} = resultData; - + const widgetConfig = ChartEditor.getWidgetConfig(); + const isActionParamsEnable = widgetConfig?.actionParams?.enable; const xPlaceholder = placeholders.find((p) => p.id === PlaceholderId.X); const xPlaceholderSettings = xPlaceholder?.settings; const x: ServerField | undefined = xPlaceholder?.items[0]; @@ -370,6 +373,19 @@ export function prepareBarX(args: PrepareFunctionArgs) { point.label = pointLabel; } + if (isActionParamsEnable) { + const actionParams: Record = {}; + + if (x && isDimensionField(x)) { + actionParams[x.guid] = category; + } + + point.custom = { + ...point.custom, + actionParams, + }; + } + return point; }) .filter((point) => point !== null), @@ -409,6 +425,23 @@ export function prepareBarX(args: PrepareFunctionArgs) { graph.custom = customSeriesData; + if (isActionParamsEnable) { + const actionParams: Record = {}; + + if (x2) { + actionParams[x2.guid] = line.stack; + } + + if (isDimensionField(colorItem)) { + actionParams[colorItem.guid] = line.colorValue; + } + + graph.custom = { + ...graph.custom, + actionParams, + }; + } + graphs.push(graph); }); }); diff --git a/src/server/modes/charts/plugins/datalens/preparers/line/__tests__/mocks/bar.mock.ts b/src/server/modes/charts/plugins/datalens/preparers/line/__tests__/mocks/bar.mock.ts index 39d42ddb79..b16cda1083 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/line/__tests__/mocks/bar.mock.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/line/__tests__/mocks/bar.mock.ts @@ -1,4 +1,14 @@ +const chartEditorMock = { + getLang: () => { + return 'en'; + }, + updateHighchartsConfig: () => {}, + updateConfig: () => {}, + getWidgetConfig: () => {}, +}; + export const calculatedTitle = { + ChartEditor: chartEditorMock, placeholders: [ { items: [ @@ -51,10 +61,13 @@ export const calculatedTitle = { }, visualizationId: 'column', datasets: [], - shared: {}, + shared: { + visualization: {id: 'bar'}, + }, }; export const regularTitle = { + ChartEditor: chartEditorMock, placeholders: [ { items: [ @@ -98,17 +111,14 @@ export const regularTitle = { }, visualizationId: 'column', datasets: [], - shared: {}, + shared: { + visualization: {id: 'bar'}, + }, }; // /wizard/foizv5h101la9-multidatasetnyy-chart-s-odinakovymi-pokazatelyami export const preparingDataForFieldsWithSameTitlesFromDifferentDatasets = { - ChartEditor: { - getLang: () => { - return 'en'; - }, - updateHighchartsConfig: () => {}, - }, + ChartEditor: chartEditorMock, placeholders: [ { allowedTypes: {}, diff --git a/src/server/modes/charts/plugins/datalens/preparers/line/index.ts b/src/server/modes/charts/plugins/datalens/preparers/line/index.ts index f3f7df9244..3d838a69f5 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/line/index.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/line/index.ts @@ -74,7 +74,8 @@ function prepareLine({ usedColors, }: PrepareFunctionArgs) { const {data, order} = resultData; - + const widgetConfig = ChartEditor.getWidgetConfig(); + const isActionParamsEnable = widgetConfig?.actionParams?.enable; const xPlaceholder = placeholders[0]; const xPlaceholderSettings = xPlaceholder.settings; const x: ServerField | undefined = placeholders[0].items[0]; @@ -405,11 +406,24 @@ function prepareLine({ } const pointLabel = innerLabels && innerLabels[category]; + point.label = pointLabel === undefined ? '' : pointLabel; + + if (isActionParamsEnable) { + const actionParams: Record = {}; + + if (isDimensionField(x)) { + actionParams[x.guid] = point.x; + } + + const [yField] = ySectionItems || []; + if (isDimensionField(yField)) { + actionParams[yField.guid] = point.y; + } - if (pointLabel !== undefined) { - point.label = pointLabel; - } else { - point.label = ''; + point.custom = { + ...point.custom, + actionParams, + }; } return point; @@ -452,6 +466,34 @@ function prepareLine({ graph.custom = customSeriesData; + if (isActionParamsEnable) { + const actionParams: Record = {}; + + // bar-x only + if (x2 && isDimensionField(x2)) { + actionParams[x2.guid] = line.stack; + } + + // bar-y only + const [, yField2] = ySectionItems || []; + if (isDimensionField(yField2)) { + actionParams[yField2.guid] = line.stack; + } + + if (isDimensionField(colorItem)) { + actionParams[colorItem.guid] = line.colorValue; + } + + if (isDimensionField(shapeItem)) { + actionParams[shapeItem.guid] = line.shapeValue; + } + + graph.custom = { + ...graph.custom, + actionParams, + }; + } + graphs.push(graph); }); }); diff --git a/src/server/modes/charts/plugins/datalens/preparers/pie/__tests__/mocks/pie.mock.ts b/src/server/modes/charts/plugins/datalens/preparers/pie/__tests__/mocks/pie.mock.ts index 8906861d51..01c9eef925 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/pie/__tests__/mocks/pie.mock.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/pie/__tests__/mocks/pie.mock.ts @@ -1,4 +1,4 @@ -import {DATASET_FIELD_TYPES} from '../../../../../../../../../shared'; +import {DATASET_FIELD_TYPES, IChartEditor} from '../../../../../../../../../shared'; const DATASET_ID = 'j43msj9o23ge9'; @@ -183,7 +183,9 @@ export const SET_WITH_MEASURE_TEXT_AND_MEASURE = { }; export const PREPARE_FUNCTION_ARGS = { - ChartEditor: undefined, + ChartEditor: { + getWidgetConfig: () => {}, + } as IChartEditor, datasets: [], fields: [], shapes: [], diff --git a/src/server/modes/charts/plugins/datalens/preparers/pie/preparePie.ts b/src/server/modes/charts/plugins/datalens/preparers/pie/preparePie.ts index 56a703a855..9a741b2b94 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/pie/preparePie.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/pie/preparePie.ts @@ -4,6 +4,7 @@ import { ExtendedSeriesLineOptions, MINIMUM_FRACTION_DIGITS, isDateField, + isDimensionField, isNumberField, } from '../../../../../../../shared'; import {ChartColorsConfig} from '../../js/helpers/colors'; @@ -105,8 +106,10 @@ export function preparePie({ colorsConfig, idToTitle, idToDataType, + ChartEditor, }: PrepareFunctionArgs) { const {data, order, totals} = resultData; + const widgetConfig = ChartEditor.getWidgetConfig(); const groupedData: Record = {}; const labelsData: Record = {}; @@ -256,6 +259,18 @@ export function preparePie({ colorValue: colorKey || name || color.title, }; + if (widgetConfig?.actionParams?.enable) { + const actionParams: Record = {}; + + if (isDimensionField(color)) { + actionParams[color.guid] = key; + } + + point.custom = { + actionParams, + }; + } + if (labelsLength) { if (isNumericalDataType(lDataType!) || label.title === 'Measure Values') { // CLOUDSUPPORT-52785 - the logic below is a bypass of the problem that once manifested itself diff --git a/src/server/modes/charts/plugins/datalens/preparers/scatter/__tests__/mocks/scatter.mock.ts b/src/server/modes/charts/plugins/datalens/preparers/scatter/__tests__/mocks/scatter.mock.ts index e94404e4ca..edcd041149 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/scatter/__tests__/mocks/scatter.mock.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/scatter/__tests__/mocks/scatter.mock.ts @@ -1,4 +1,10 @@ -import {DATASET_FIELD_TYPES, ServerColor, ServerShape} from '../../../../../../../../../shared'; +import { + DATASET_FIELD_TYPES, + IChartEditor, + ServerColor, + ServerField, + ServerShape, +} from '../../../../../../../../../shared'; const DATASET_ID = 'j43msj9o23ge9'; @@ -7,14 +13,14 @@ const X_FIELD = { title: 'XField', guid: 'cddd9cad-52a2-4232-8898-ade9a972c864', data_type: DATASET_FIELD_TYPES.GENERICDATETIME, -}; +} as ServerField; const Y_FIELD = { datasetId: DATASET_ID, title: 'YField', guid: 'a6b94410-e219-11e9-a279-0b30c0a74ab7', data_type: DATASET_FIELD_TYPES.FLOAT, -}; +} as ServerField; export const COLOR_FIELD = { datasetId: DATASET_ID, @@ -31,7 +37,9 @@ export const SHAPE_FIELD = { } as ServerShape; export const PREPARE_FUNCTION_ARGS = { - ChartEditor: undefined, + ChartEditor: { + getWidgetConfig: () => {}, + } as IChartEditor, colors: [], colorsConfig: {loadedColorPalettes: {}, colors: ['blue', 'red', 'orange'], gradientColors: []}, datasets: [], @@ -43,7 +51,7 @@ export const PREPARE_FUNCTION_ARGS = { [Y_FIELD.guid]: Y_FIELD.data_type, [COLOR_FIELD.guid]: COLOR_FIELD.data_type, [SHAPE_FIELD.guid]: SHAPE_FIELD.data_type, - }, + } as Record, idToTitle: { [X_FIELD.guid]: X_FIELD.title, [Y_FIELD.guid]: Y_FIELD.title, diff --git a/src/server/modes/charts/plugins/datalens/preparers/scatter/prepareScatter.ts b/src/server/modes/charts/plugins/datalens/preparers/scatter/prepareScatter.ts index 90075f9a9d..5c0da51146 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/scatter/prepareScatter.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/scatter/prepareScatter.ts @@ -9,6 +9,7 @@ import { ServerField, getFormatOptions, isDateField, + isDimensionField, isEnabledServerFeature, } from '../../../../../../../shared'; import {registry} from '../../../../../../registry'; @@ -68,8 +69,10 @@ export function prepareScatter(options: PrepareFunctionArgs): PrepareScatterResu idToDataType, shapes, shapesConfig, + ChartEditor, } = options; - + const widgetConfig = ChartEditor.getWidgetConfig(); + const isActionParamsEnable = widgetConfig?.actionParams?.enable; const {data, order} = resultData; const x = placeholders[0].items[0]; @@ -132,6 +135,7 @@ export function prepareScatter(options: PrepareFunctionArgs): PrepareScatterResu const xi = findIndexInOrder(order, x, xTitle); const xValueRaw: string | null | undefined = values[xi]; let xValue: string | number | Date; + let zValueRaw: string | null | undefined; const point: ScatterPoint = {}; if (xValueRaw === null || xValueRaw === undefined) { @@ -234,17 +238,21 @@ export function prepareScatter(options: PrepareFunctionArgs): PrepareScatterResu if (z) { const zTitle = idToTitle[z.guid]; const zi = findIndexInOrder(order, z, zTitle); - let zValue = values[zi]; + zValueRaw = values[zi]; + let formattedZValue = zValueRaw; if (isNumericalDataType(z.data_type) && z.formatting) { - zValue = chartKitFormatNumberWrapper(Number(zValue), { + formattedZValue = chartKitFormatNumberWrapper(Number(formattedZValue), { lang: 'ru', ...z.formatting, }); } - const name = zValue && shouldEscapeUserValue ? escape(zValue as string) : zValue; - point.name = name || ''; + if (formattedZValue && shouldEscapeUserValue) { + formattedZValue = escape(formattedZValue as string); + } + + point.name = formattedZValue || ''; } else { delete point.name; keys.delete('x'); @@ -321,6 +329,27 @@ export function prepareScatter(options: PrepareFunctionArgs): PrepareScatterResu point.sLabel = shapeValue; } + if (isActionParamsEnable) { + const actionParams: Record = {}; + + if (isDimensionField(x)) { + actionParams[x.guid] = xValueRaw; + } + + if (isDimensionField(y)) { + actionParams[y.guid] = yValueRaw; + } + + if (isDimensionField(z)) { + actionParams[z.guid] = zValueRaw; + } + + point.custom = { + ...point.custom, + actionParams, + }; + } + points.push(point); }); @@ -362,6 +391,23 @@ export function prepareScatter(options: PrepareFunctionArgs): PrepareScatterResu graphs.forEach((graph) => { graph.keys = Array.from(keys); + + if (isActionParamsEnable) { + const actionParams: Record = {}; + + if (isDimensionField(color)) { + actionParams[color.guid] = graph.data?.[0]?.colorValue; + } + + if (isDimensionField(shape)) { + actionParams[shape.guid] = graph.data?.[0]?.shapeValue; + } + + graph.custom = { + ...graph.custom, + actionParams, + }; + } }); return { diff --git a/src/server/modes/charts/plugins/datalens/preparers/treemap.ts b/src/server/modes/charts/plugins/datalens/preparers/treemap.ts index a73acaf712..8e4ceecd2b 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/treemap.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/treemap.ts @@ -27,6 +27,8 @@ type TreemapItem = { label?: string; value?: number; drillDownFilterValue?: string | null; + color?: string; + custom?: object; }; function prepareTreemap({ @@ -106,22 +108,22 @@ function prepareTreemap({ const i = findIndexInOrder(order, item, actualTitle); + const rawValue = values[i]; let value: string | null; if (isDateField({data_type: dTypes[level]})) { value = formatDate({ valueType: dTypes[level], - value: values[i], + value: rawValue, format: item.format, }); } else if (isNumericalDataType(dTypes[level]) && item.formatting) { - value = chartKitFormatNumberWrapper(values[i] as unknown as number, { + value = chartKitFormatNumberWrapper(rawValue as unknown as number, { lang: 'ru', ...item.formatting, }); } else { - value = - values[i] && shouldEscapeUserValue ? escape(values[i] as string) : values[i]; + value = rawValue && shouldEscapeUserValue ? escape(rawValue as string) : rawValue; } const treemapId = @@ -204,14 +206,14 @@ function prepareTreemap({ } treemap = treemap.map((obj) => { + const item = {...obj}; + const color = colorData[obj.id]; if (color) { - return { - ...obj, - color: color.backgroundColor, - }; + item.color = color.backgroundColor; } - return obj; + + return item; }); } diff --git a/src/server/modes/charts/plugins/datalens/preparers/types.ts b/src/server/modes/charts/plugins/datalens/preparers/types.ts index 5afb5391e6..6b269f96ae 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/types.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/types.ts @@ -87,4 +87,5 @@ export type PiePoint = { colorGuid: string; colorValue: string | number; label?: string | number | null; + custom?: object; }; diff --git a/src/shared/modules/helpers.ts b/src/shared/modules/helpers.ts index 850b61bee3..d5523e9069 100644 --- a/src/shared/modules/helpers.ts +++ b/src/shared/modules/helpers.ts @@ -66,13 +66,6 @@ export function decodeURISafe(uri: string) { return decodeURI(uri.replace(/%(?![0-9a-fA-F][0-9a-fA-F]+)/g, '%25')); } -export function encodeURISafe(uri: string) { - if (!uri) { - return uri; - } - return encodeURI(decodeURISafe(uri)); -} - type PrepareFilterValuesArgs = { values: string[]; }; @@ -187,8 +180,8 @@ export const isParameter = (field: Partial | Partial) => { return field.calc_mode === 'parameter'; }; -export const isDimensionField = (field: Partial | Partial) => { - return field.type === DatasetFieldType.Dimension && field.calc_mode !== 'parameter'; +export const isDimensionField = (field: Partial | Partial | undefined) => { + return field?.type === DatasetFieldType.Dimension && !isParameter(field); }; export const isMeasureField = (field: Partial | Partial | undefined) => { @@ -199,10 +192,6 @@ export const isPseudoField = (field: Partial | Partial | und return field?.type === DatasetFieldType.Pseudo; }; -export const isTreeField = (field: {data_type: string}) => { - return isTreeDataType(field.data_type); -}; - export const isTreeDataType = (data_type: string) => { return ( data_type === DATASET_FIELD_TYPES.TREE_STR || @@ -211,6 +200,10 @@ export const isTreeDataType = (data_type: string) => { ); }; +export const isTreeField = (field: {data_type: string}) => { + return isTreeDataType(field.data_type); +}; + export const transformParamsToUrlParams = (widgetParams: StringParams) => { return Object.keys(widgetParams).reduce((acc: [string, string][], paramName: string) => { const paramValue = widgetParams[paramName]; diff --git a/src/shared/types/chartkit/dl-chartkit.ts b/src/shared/types/chartkit/dl-chartkit.ts index faf7522a1e..5c977ff77d 100644 --- a/src/shared/types/chartkit/dl-chartkit.ts +++ b/src/shared/types/chartkit/dl-chartkit.ts @@ -6,3 +6,16 @@ export type ChartsInsightsItem = { message: string; locator: string; }; + +type SetActionParamsEventHandler = { + type: 'setActionParams'; +}; + +export type WidgetEventHandler = SetActionParamsEventHandler; + +export type GraphWidgetEventScope = 'point' | 'series'; + +export type WidgetEvent = { + handler: WidgetEventHandler | WidgetEventHandler[]; + scope?: T; +}; diff --git a/src/shared/types/feature.ts b/src/shared/types/feature.ts index 29d45c5907..51d8dbe7f3 100644 --- a/src/shared/types/feature.ts +++ b/src/shared/types/feature.ts @@ -91,6 +91,7 @@ export enum Feature { AddDemoWorkbook = 'AddDemoWorkbook', SaveDashWithFakeEntry = 'SaveDashWithFakeEntry', CopyEntriesToWorkbook = 'CopyEntriesToWorkbook', + WizardChartChartFilteringAvailable = 'WizardChartChartFilteringAvailable', } export type FeatureConfig = Record; diff --git a/src/shared/types/widget.ts b/src/shared/types/widget.ts index dd35e9a4f8..eae2f13e2f 100644 --- a/src/shared/types/widget.ts +++ b/src/shared/types/widget.ts @@ -40,4 +40,4 @@ export enum WidgetKind { Markdown = 'markdown', } -export type WidgetType = LegacyEditorType | EditorType; +export type WidgetType = LegacyEditorType | EditorType | WizardType; diff --git a/src/ui/components/Widgets/Chart/Chart.tsx b/src/ui/components/Widgets/Chart/Chart.tsx index afaaf53a72..e980edcbaa 100644 --- a/src/ui/components/Widgets/Chart/Chart.tsx +++ b/src/ui/components/Widgets/Chart/Chart.tsx @@ -65,6 +65,7 @@ export const Chart = (props: ChartNoWidgetProps) => { ignoreUsedParams, onInnerParamsChanged, disableChartLoader, + actionParamsEnabled, } = props; const innerParamsRef = React.useRef(null); @@ -170,7 +171,7 @@ export const Chart = (props: ChartNoWidgetProps) => { ignoreUsedParams, clearedOuterParams, onInnerParamsChanged, - enableActionParams: true, + enableActionParams: actionParamsEnabled, }); /** diff --git a/src/ui/components/Widgets/Chart/types.ts b/src/ui/components/Widgets/Chart/types.ts index ab5e2ea623..4e103606ef 100644 --- a/src/ui/components/Widgets/Chart/types.ts +++ b/src/ui/components/Widgets/Chart/types.ts @@ -103,6 +103,7 @@ type ChartKitBaseWrapperProps = ChartsProps & { skipReload?: boolean; renderPluginLoader?: () => React.ReactNode; + actionParamsEnabled?: boolean; }; export type ChartWidgetProviderPropsWithRefProps = Omit< diff --git a/src/ui/constants/common.ts b/src/ui/constants/common.ts index c896be90f8..e5106c136b 100644 --- a/src/ui/constants/common.ts +++ b/src/ui/constants/common.ts @@ -255,6 +255,7 @@ export const URL_OPTIONS = { EMBEDDED: '_embedded', NO_CONTROLS: '_no_controls', LANGUAGE: '_lang', + ACTION_PARAMS_ENABLED: '_action_params', }; export const DLS_SUBJECT = { diff --git a/src/ui/libs/DatalensChartkit/ChartKit/helpers/action-params-handlers.ts b/src/ui/libs/DatalensChartkit/ChartKit/helpers/action-params-handlers.ts new file mode 100644 index 0000000000..c4efdb38fc --- /dev/null +++ b/src/ui/libs/DatalensChartkit/ChartKit/helpers/action-params-handlers.ts @@ -0,0 +1,341 @@ +import type {Highcharts} from '@gravity-ui/chartkit/highcharts'; +import {transformParamsToActionParams} from '@gravity-ui/dashkit'; +import type {Point, PointOptionsObject} from 'highcharts'; +import cloneDeep from 'lodash/cloneDeep'; +import get from 'lodash/get'; +import isEqual from 'lodash/isEqual'; +import merge from 'lodash/merge'; +import uniq from 'lodash/uniq'; + +import type {GraphWidgetEventScope, StringParams} from '../../../../../shared'; +import type {ChartKitAdapterProps} from '../types'; + +import { + ActionParams, + ActionParamsValue, + extractHcTypeFromSeries, + getPointActionParams, + isPointSelected, +} from './utils'; + +const Opacity = { + SELECTED: '1', + UNSELECTED: '0.5', +}; + +const setSeriesOpacity = (seriesItem: Highcharts.Series) => { + const opacity = seriesItem.selected ? Opacity.SELECTED : Opacity.UNSELECTED; + seriesItem.update({opacity, selected: seriesItem.selected}); +}; + +function setPointSelectState(point: Point, selected: boolean) { + const type = extractHcTypeFromSeries(point.series); + const opacity = selected ? Opacity.SELECTED : Opacity.UNSELECTED; + + switch (type) { + case 'scatter': { + const prevMarkerOptions = get(point, 'marker'); + const markerOptions = merge({}, prevMarkerOptions, { + states: {normal: {opacity}}, + }); + point.update( + { + marker: markerOptions, + } as unknown as PointOptionsObject, + false, + ); + break; + } + case 'line': + case 'area': { + point.update( + { + marker: { + enabled: selected, + fillOpacity: opacity, + states: {normal: {opacity}}, + }, + } as unknown as PointOptionsObject, + false, + ); + break; + } + default: { + point.update({opacity} as unknown as PointOptionsObject, false); + } + } +} + +function setSeriesSelectState(series: Highcharts.Series, selected: boolean) { + const type = extractHcTypeFromSeries(series); + const opacity = selected ? Opacity.SELECTED : Opacity.UNSELECTED; + + switch (type) { + case 'area': + case 'line': { + series.update({opacity}, false); + break; + } + default: { + break; + } + } +} + +function isEmptyParam(paramValue: ActionParamsValue) { + return paramValue === ''; +} + +function addParams(params: ActionParams, addition: ActionParams = {}) { + const result = cloneDeep(params); + + return Object.entries(addition).reduce((acc, [key, value]) => { + if (!(key in acc)) { + acc[key] = []; + } + + if (!Array.isArray(acc[key])) { + acc[key] = [acc[key] as ActionParamsValue]; + } + + if ((acc[key] as ActionParamsValue[]).every(isEmptyParam)) { + acc[key] = []; + } + + acc[key] = uniq((acc[key] as ActionParamsValue[]).concat(value as string)); + return acc; + }, result); +} + +function subtractParameters(params: ActionParams, sub: ActionParams = {}) { + const result = cloneDeep(params); + return Object.entries(sub).reduce((acc, [key, value]) => { + const paramValue = acc[key]; + if (Array.isArray(paramValue)) { + const exclude = Array.isArray(value) ? value : [value]; + acc[key] = paramValue.filter((item) => !exclude.includes(item)); + } else { + delete acc[key]; + } + + return acc; + }, result); +} + +const applyParamToParams = (args: { + params: StringParams; + key: string; + value: string; + selected: boolean; +}) => { + const {params, key, value, selected} = args; + const isArray = Array.isArray(params[key]); + const isString = typeof params[key] === 'string'; + + if (selected) { + if (isArray && !params[key].includes(value)) { + (params[key] as string[]).push(value); + } else if (isString && params[key] !== value) { + params[key] = [params[key] as string, value]; + } + } else if (isArray && params[key].includes(value)) { + params[key] = (params[key] as string[]).filter((iteratedValue) => iteratedValue !== value); + } else if (isString && params[key] === value) { + params[key] = []; + } +}; + +export const setPointActionParamToParams = (args: { + actionParams: unknown; + params: StringParams; + selected: boolean; +}) => { + const {actionParams, params, selected} = args; + + if (!actionParams || typeof actionParams !== 'object') { + return; + } + + Object.entries(actionParams).forEach(([key, values]) => { + if (!params[key]) { + params[key] = []; + } + + if (Array.isArray(values)) { + values.forEach((value) => applyParamToParams({params, key, value, selected})); + } else { + applyParamToParams({params, key, value: values, selected}); + } + }); +}; + +function seriesToParams(series: Highcharts.Series[]) { + return series.reduce((params, seriesItem) => { + const points = seriesItem.getPointsCollection(); + const actionParams = points.map(getPointActionParams); + + if (!Array.isArray(actionParams)) { + return params; + } + + actionParams.forEach((actionParam: unknown) => { + setPointActionParamToParams({ + actionParams: actionParam, + params, + selected: seriesItem.selected, + }); + }); + + return params; + }, {}); +} + +export const handleChartLoadingForActionParams = (args: { + clickScope: GraphWidgetEventScope; + series: Highcharts.Series[]; + actionParams?: ActionParams; +}) => { + const {clickScope, series, actionParams = {}} = args; + + if (!Object.keys(actionParams).length) { + return; + } + + switch (clickScope) { + case 'point': { + const chartPoints = series.reduce( + (acc, s) => acc.concat(s.getPointsCollection()), + [] as Point[], + ); + + const hasSomePointSelected = chartPoints.some((p) => isPointSelected(p, actionParams)); + if (hasSomePointSelected) { + series.forEach((s) => { + const hasAnySelectedPoints = s.getPointsCollection().reduce((acc, p) => { + const pointSelected = isPointSelected(p, actionParams); + setPointSelectState(p, pointSelected); + return acc || pointSelected; + }, false); + setSeriesSelectState(s, hasAnySelectedPoints); + }); + series[0]?.chart.redraw(); + } + + break; + } + case 'series': { + const hasSelectedSeries = series.some((s) => s.selected); + + if (hasSelectedSeries) { + series.forEach(setSeriesOpacity); + } + + break; + } + } +}; + +export function handleSeriesClickForActionParams(args: { + chart: Highcharts.Chart; + clickScope: GraphWidgetEventScope; + event: Highcharts.SeriesClickEventObject; + onChange?: ChartKitAdapterProps['onChange']; + actionParams: ActionParams; +}) { + const {chart, clickScope, event, onChange, actionParams: prevActionParams} = args; + const multiSelect = Boolean(event.metaKey); + let newActionParams: ActionParams = prevActionParams; + + switch (clickScope) { + case 'point': { + const currentPoint = event.point; + const currentPointParams = getPointActionParams(currentPoint); + const chartPoints = chart.series.reduce( + (acc, s) => acc.concat(s.getPointsCollection()), + [] as Point[], + ); + const hasSomePointSelected = chartPoints.some((p) => + isPointSelected(p, prevActionParams), + ); + + if (hasSomePointSelected) { + if (isPointSelected(currentPoint, prevActionParams)) { + chartPoints.forEach((p) => { + if (isPointSelected(p, currentPointParams)) { + setPointSelectState(p, false); + } + }); + + newActionParams = subtractParameters(newActionParams, currentPointParams); + + if (multiSelect) { + chartPoints.forEach((p) => { + if (isPointSelected(p, newActionParams)) { + const pointParams = getPointActionParams(p); + newActionParams = addParams(newActionParams, pointParams); + } + }); + } + } else { + if (!multiSelect) { + // should remove the selection from the previously selected points + chartPoints.forEach((p) => { + if (isPointSelected(p, prevActionParams)) { + const pointParams = getPointActionParams(p); + newActionParams = subtractParameters(newActionParams, pointParams); + + setPointSelectState(p, false); + } + }); + } + + setPointSelectState(currentPoint, true); + newActionParams = addParams(newActionParams, currentPointParams); + } + } else { + newActionParams = addParams(newActionParams, currentPointParams); + chartPoints.forEach((p) => { + if (!isPointSelected(p, newActionParams)) { + setPointSelectState(p, false); + } + }); + } + + break; + } + case 'series': { + event.point.series.select(); + + if (!event.metaKey) { + chart.series + .filter((s) => s.name !== event.point.series.name) + .forEach((s) => { + if (s.selected) { + s.select(); + } + }); + } + + chart.series.forEach(setSeriesOpacity); + const params = seriesToParams(chart.series); + newActionParams = transformParamsToActionParams(params); + + break; + } + } + + if (isEqual(prevActionParams, newActionParams)) { + return; + } + + const params = transformParamsToActionParams(newActionParams as StringParams); + onChange?.( + { + type: 'PARAMS_CHANGED', + data: {params}, + }, + {forceUpdate: true}, + true, + true, + ); +} diff --git a/src/ui/libs/DatalensChartkit/ChartKit/helpers/apply-hc-handlers.ts b/src/ui/libs/DatalensChartkit/ChartKit/helpers/apply-hc-handlers.ts index 7e92ef2274..1f6a7e6c83 100644 --- a/src/ui/libs/DatalensChartkit/ChartKit/helpers/apply-hc-handlers.ts +++ b/src/ui/libs/DatalensChartkit/ChartKit/helpers/apply-hc-handlers.ts @@ -1,224 +1,21 @@ import type {Highcharts} from '@gravity-ui/chartkit/highcharts'; -import {transformParamsToActionParams} from '@gravity-ui/dashkit'; +import {pickActionParamsFromParams} from '@gravity-ui/dashkit'; import {wrap} from 'highcharts'; import get from 'lodash/get'; import has from 'lodash/has'; import merge from 'lodash/merge'; import set from 'lodash/set'; -import type {StringParams} from 'shared'; -import type {GraphWidget, GraphWidgetEventScope, WidgetEventHandler} from '../../types'; +import type {GraphWidgetEventScope} from '../../../../../shared'; +import type {GraphWidget} from '../../types'; import type {ChartKitAdapterProps} from '../types'; -type ShapedAction = { - type: WidgetEventHandler['type']; - scope?: GraphWidgetEventScope; -}; - -const Opacity = { - SELECTED: '1', - UNSELECTED: '0.5', -}; - -export const extractHcTypeFromData = (data?: GraphWidget) => { - return data?.libraryConfig.chart?.type; -}; - -const extractHcTypeFromPoint = (point: Highcharts.Point) => { - return point.series.chart.options.chart?.type; -}; - -const isStringParam = (data: unknown): data is StringParams => { - if (typeof data !== 'object' || Array.isArray(data) || data === null) { - return false; - } - - const entries = Object.entries(data); - - if (entries.length !== 1) { - return false; - } - - const value = entries[0][1]; - - return Array.isArray(value) || typeof value === 'string'; -}; - -const applyParamToParams = (args: { - params: StringParams; - key: string; - value: string; - selected: boolean; -}) => { - const {params, key, value, selected} = args; - const isArray = Array.isArray(params[key]); - const isString = typeof params[key] === 'string'; - - if (selected && isArray && !params[key].includes(value)) { - (params[key] as string[]).push(value); - } else if (!selected && isArray && params[key].includes(value)) { - params[key] = (params[key] as string[]).filter((iteratedValue) => iteratedValue !== value); - } else if (selected && isString && params[key] !== value) { - params[key] = [params[key] as string, value]; - } else if (!selected && isString && params[key] === value) { - params[key] = []; - } -}; - -export const setPointActionParamToParams = (args: { - actionParam: unknown; - params: StringParams; - selected: boolean; -}) => { - const {actionParam, params, selected} = args; - - if (!isStringParam(actionParam)) { - return; - } - - const [key, values] = Object.entries(actionParam)[0]; - - if (!params[key]) { - params[key] = []; - } - - if (Array.isArray(values)) { - values.forEach((value) => applyParamToParams({params, key, value, selected})); - } else { - applyParamToParams({params, key, value: values, selected}); - } -}; - -const pointsToParams = (points: Highcharts.Point[]) => { - return points.reduce((params, point) => { - const actionParam = get(point, 'options.custom.actionParam'); - setPointActionParamToParams({ - actionParam, - params, - selected: point.selected, - }); - - return params; - }, {}); -}; - -const seriesToParams = (series: Highcharts.Series[]) => { - return series.reduce((params, seriesItem) => { - const points = seriesItem.getPointsCollection(); - const actionParams = points.map((point) => get(point, 'options.custom.actionParam')); - - if (!Array.isArray(actionParams)) { - return params; - } - - actionParams.forEach((actionParam: unknown) => { - setPointActionParamToParams({ - actionParam, - params, - selected: seriesItem.selected, - }); - }); - - return params; - }, {}); -}; - -const setPointOpacity = (point: Highcharts.Point) => { - const type = extractHcTypeFromPoint(point); - const opacity = point.selected ? Opacity.SELECTED : Opacity.UNSELECTED; - - if (type === 'scatter') { - // @ts-expect-error - point.update({marker: {states: {normal: {opacity}}}}); - } else { - point.update({opacity}); - } -}; - -const setSeriesOpacity = (seriesItem: Highcharts.Series) => { - const opacity = seriesItem.selected ? Opacity.SELECTED : Opacity.UNSELECTED; - seriesItem.update({opacity, selected: seriesItem.selected}); -}; - -const handleChartLoadingForActionParams = (args: { - clickScope: GraphWidgetEventScope; - series: Highcharts.Series[]; -}) => { - const {clickScope, series} = args; - - switch (clickScope) { - case 'point': { - const allPoints = series.map((s) => s.getPointsCollection()).flatMap((s) => s); - const hasSelectedPoints = allPoints.some((p) => p.selected); - - if (hasSelectedPoints) { - allPoints.forEach(setPointOpacity); - } - - break; - } - case 'series': { - const hasSelectedSeries = series.some((s) => s.selected); - - if (hasSelectedSeries) { - series.forEach(setSeriesOpacity); - } - - break; - } - } -}; - -const handleSeriesClickForActionParams = (args: { - chart: Highcharts.Chart; - clickScope: GraphWidgetEventScope; - event: Highcharts.SeriesClickEventObject; - onChange?: ChartKitAdapterProps['onChange']; -}) => { - const {chart, clickScope, event, onChange} = args; - let actionParams: StringParams = {}; - - switch (clickScope) { - case 'point': { - event.point.select(undefined, event.metaKey); - const allPoints = chart.series.map((s) => s.getPointsCollection()).flatMap((s) => s); - allPoints.forEach(setPointOpacity); - const params = pointsToParams(allPoints); - actionParams = transformParamsToActionParams(params); - - break; - } - case 'series': { - event.point.series.select(); - - if (!event.metaKey) { - chart.series - .filter((s) => s.name !== event.point.series.name) - .forEach((s) => { - if (s.selected) { - s.select(); - } - }); - } - - chart.series.forEach(setSeriesOpacity); - const params = seriesToParams(chart.series); - actionParams = transformParamsToActionParams(params); - - break; - } - } - - onChange?.( - { - type: 'PARAMS_CHANGED', - data: {params: {...actionParams}}, - }, - {forceUpdate: true}, - true, - true, - ); -}; +import { + handleChartLoadingForActionParams, + handleSeriesClickForActionParams, +} from './action-params-handlers'; +import type {ShapedAction} from './types'; +import {extractHcTypeFromData} from './utils'; export const fixPieTotals = (args: {data: GraphWidget}) => { const {data} = args; @@ -263,6 +60,8 @@ export const applySetActionParamsEvents = (args: { set(data, pathToScatterMarkerStates, {}); } + const actionParams = pickActionParamsFromParams(get(data, 'unresolvedParams', {})); + wrap( get(data, pathToChartEvents), 'load', @@ -271,7 +70,7 @@ export const applySetActionParamsEvents = (args: { proceed: Highcharts.ChartLoadCallbackFunction, event: Event, ) { - handleChartLoadingForActionParams({series: this.series, clickScope}); + handleChartLoadingForActionParams({series: this.series, clickScope, actionParams}); proceed?.apply(this, [event]); }, ); @@ -284,7 +83,13 @@ export const applySetActionParamsEvents = (args: { proceed: Highcharts.SeriesClickCallbackFunction, event: Highcharts.SeriesClickEventObject, ) { - handleSeriesClickForActionParams({chart: this.chart, clickScope, event, onChange}); + handleSeriesClickForActionParams({ + chart: this.chart, + clickScope, + event, + onChange, + actionParams, + }); proceed?.apply(this, [event]); }, ); diff --git a/src/ui/libs/DatalensChartkit/ChartKit/helpers/chartkitAdapter.ts b/src/ui/libs/DatalensChartkit/ChartKit/helpers/chartkitAdapter.ts index 9792fc6b63..d350b412d9 100644 --- a/src/ui/libs/DatalensChartkit/ChartKit/helpers/chartkitAdapter.ts +++ b/src/ui/libs/DatalensChartkit/ChartKit/helpers/chartkitAdapter.ts @@ -8,15 +8,16 @@ import type {GraphWidget, LoadedWidgetData} from '../../types'; import {ChartKitCustomError} from '../modules/chartkit-custom-error/chartkit-custom-error'; import type {ChartKitAdapterProps} from '../types'; -import {applySetActionParamsEvents, extractHcTypeFromData, fixPieTotals} from './apply-hc-handlers'; +import {applySetActionParamsEvents, fixPieTotals} from './apply-hc-handlers'; import {tooltipRenderer} from './tooltip'; +import {extractHcTypeFromData} from './utils'; const getNormalizedClickActions = (data: GraphWidget) => { if (data.config && 'seriesActions' in data.config) { throw new ChartKitCustomError(null, { details: ` Seems you are trying to use unsupported property "config.seriesActions". This property sets according to this type: - + { config: { events?: { diff --git a/src/ui/libs/DatalensChartkit/ChartKit/helpers/helpers.test.ts b/src/ui/libs/DatalensChartkit/ChartKit/helpers/helpers.test.ts index f6639de3a3..c946938bfc 100644 --- a/src/ui/libs/DatalensChartkit/ChartKit/helpers/helpers.test.ts +++ b/src/ui/libs/DatalensChartkit/ChartKit/helpers/helpers.test.ts @@ -1,22 +1,22 @@ -import {setPointActionParamToParams} from './apply-hc-handlers'; +import {setPointActionParamToParams} from './action-params-handlers'; describe('chartkit/chartkit-adapter/helpers', () => { test.each([ - [{actionParam: {p: '2'}, params: {p: '1'}, selected: true}, {p: ['1', '2']}], - [{actionParam: {p: ['2']}, params: {p: '1'}, selected: true}, {p: ['1', '2']}], - [{actionParam: {p: '2'}, params: {p: ['1']}, selected: true}, {p: ['1', '2']}], - [{actionParam: {p: ['2']}, params: {p: ['1']}, selected: true}, {p: ['1', '2']}], - [{actionParam: {p: '1'}, params: {p: '1'}, selected: true}, {p: '1'}], - [{actionParam: {p: '1'}, params: {p: ['1']}, selected: true}, {p: ['1']}], - [{actionParam: {p: ['1']}, params: {p: ['1']}, selected: true}, {p: ['1']}], - [{actionParam: {p: ['3']}, params: {p: ['1', '2']}, selected: true}, {p: ['1', '2', '3']}], - [{actionParam: {p: ['2', '3']}, params: {p: '1'}, selected: true}, {p: ['1', '2', '3']}], - [{actionParam: {p: '1'}, params: {p: '1'}, selected: false}, {p: []}], - [{actionParam: {p: ['1']}, params: {p: '1'}, selected: false}, {p: []}], - [{actionParam: {p: '1'}, params: {p: ['1']}, selected: false}, {p: []}], - [{actionParam: {p: ['1']}, params: {p: ['1']}, selected: false}, {p: []}], - [{actionParam: {p: ['1']}, params: {p: ['1', '2']}, selected: false}, {p: ['2']}], - [{actionParam: {p: ['3']}, params: {p: ['1', '2']}, selected: false}, {p: ['1', '2']}], + [{actionParams: {p: '2'}, params: {p: '1'}, selected: true}, {p: ['1', '2']}], + [{actionParams: {p: ['2']}, params: {p: '1'}, selected: true}, {p: ['1', '2']}], + [{actionParams: {p: '2'}, params: {p: ['1']}, selected: true}, {p: ['1', '2']}], + [{actionParams: {p: ['2']}, params: {p: ['1']}, selected: true}, {p: ['1', '2']}], + [{actionParams: {p: '1'}, params: {p: '1'}, selected: true}, {p: '1'}], + [{actionParams: {p: '1'}, params: {p: ['1']}, selected: true}, {p: ['1']}], + [{actionParams: {p: ['1']}, params: {p: ['1']}, selected: true}, {p: ['1']}], + [{actionParams: {p: ['3']}, params: {p: ['1', '2']}, selected: true}, {p: ['1', '2', '3']}], + [{actionParams: {p: ['2', '3']}, params: {p: '1'}, selected: true}, {p: ['1', '2', '3']}], + [{actionParams: {p: '1'}, params: {p: '1'}, selected: false}, {p: []}], + [{actionParams: {p: ['1']}, params: {p: '1'}, selected: false}, {p: []}], + [{actionParams: {p: '1'}, params: {p: ['1']}, selected: false}, {p: []}], + [{actionParams: {p: ['1']}, params: {p: ['1']}, selected: false}, {p: []}], + [{actionParams: {p: ['1']}, params: {p: ['1', '2']}, selected: false}, {p: ['2']}], + [{actionParams: {p: ['3']}, params: {p: ['1', '2']}, selected: false}, {p: ['1', '2']}], ])('setPointsActionParamToParams (args: %j})', (args, expected) => { const params = args.params; setPointActionParamToParams(args); diff --git a/src/ui/libs/DatalensChartkit/ChartKit/helpers/types.ts b/src/ui/libs/DatalensChartkit/ChartKit/helpers/types.ts new file mode 100644 index 0000000000..76e57dc455 --- /dev/null +++ b/src/ui/libs/DatalensChartkit/ChartKit/helpers/types.ts @@ -0,0 +1,6 @@ +import {GraphWidgetEventScope, WidgetEventHandler} from 'shared'; + +export type ShapedAction = { + type: WidgetEventHandler['type']; + scope?: GraphWidgetEventScope; +}; diff --git a/src/ui/libs/DatalensChartkit/ChartKit/helpers/utils.test.ts b/src/ui/libs/DatalensChartkit/ChartKit/helpers/utils.test.ts new file mode 100644 index 0000000000..22bf04d524 --- /dev/null +++ b/src/ui/libs/DatalensChartkit/ChartKit/helpers/utils.test.ts @@ -0,0 +1,37 @@ +import type {Point} from 'highcharts'; + +import {ActionParams, isPointSelected} from './utils'; + +describe('isPointSelected', () => { + const point = {options: {custom: {actionParams: {a: 1, b: 2}}}} as unknown as Point; + + test('empty actionParams -> point is not selected', () => { + const actionParams = {}; + expect(isPointSelected(point, actionParams)).toBeFalsy(); + }); + + test('point data has no intersections with the actionParams -> point is not selected', () => { + const actionParams: ActionParams = {c: ['3']}; + expect(isPointSelected(point, actionParams)).toBeFalsy(); + }); + + test('point data has partial match with the actionParams -> point is selected', () => { + const actionParams: ActionParams = {a: ['1']}; + expect(isPointSelected(point, actionParams)).toBeTruthy(); + }); + + test('point data has full match with the actionParams -> point is selected', () => { + const actionParams: ActionParams = {a: [1], b: ['2']}; + expect(isPointSelected(point, actionParams)).toBeTruthy(); + }); + + test('point data has full match with the actionParams(multiselect) -> point is selected', () => { + const actionParams: ActionParams = {a: [1, 7], b: ['2']}; + expect(isPointSelected(point, actionParams)).toBeTruthy(); + }); + + test('point data and actionParams have parameters with different values -> point is not selected', () => { + const actionParams: ActionParams = {a: ['2']}; + expect(isPointSelected(point, actionParams)).toBeFalsy(); + }); +}); diff --git a/src/ui/libs/DatalensChartkit/ChartKit/helpers/utils.ts b/src/ui/libs/DatalensChartkit/ChartKit/helpers/utils.ts new file mode 100644 index 0000000000..17f23417b9 --- /dev/null +++ b/src/ui/libs/DatalensChartkit/ChartKit/helpers/utils.ts @@ -0,0 +1,45 @@ +import type {Point} from 'highcharts'; +import get from 'lodash/get'; + +import {GraphWidget} from '../../types'; + +export type ActionParamsValue = string | number | boolean; +export type ActionParams = Record; + +export type PointActionParams = Record; + +export function getPointActionParams(point: Point): PointActionParams { + return Object.assign( + {}, + get(point, 'series.options.custom.actionParams', {}), + get(point, 'options.custom.actionParams', {}), + ); +} + +export function isPointSelected(point: Point, actionParams: ActionParams = {}) { + const pointActionParams = getPointActionParams(point); + const matchedParamNames = Object.entries(pointActionParams).filter( + ([name]) => name in actionParams, + ); + + return ( + matchedParamNames.length > 0 && + matchedParamNames.every(([name, value]) => { + const pointParamValue = String(value); + const actionParamsValue = actionParams[name]; + if (Array.isArray(actionParamsValue)) { + return actionParamsValue.some((val) => String(val) === pointParamValue); + } + + return String(actionParamsValue) === pointParamValue; + }) + ); +} + +export const extractHcTypeFromData = (data?: GraphWidget) => { + return data?.libraryConfig.chart?.type; +}; + +export function extractHcTypeFromSeries(series?: Highcharts.Series) { + return series?.chart.options.chart?.type; +} diff --git a/src/ui/libs/DatalensChartkit/types/widget.ts b/src/ui/libs/DatalensChartkit/types/widget.ts index 28947a812b..7bc8a72466 100644 --- a/src/ui/libs/DatalensChartkit/types/widget.ts +++ b/src/ui/libs/DatalensChartkit/types/widget.ts @@ -12,11 +12,13 @@ import { ChartkitHandlers, ChartsInsightsItem, GraphTooltipLine, + GraphWidgetEventScope, StringParams, TableCell, TableHead, TableRow, TableTitle, + WidgetEvent, } from '../../../../shared'; import {ChartsData} from '../modules/data-provider/charts'; @@ -105,19 +107,6 @@ export type GraphWidgetSeriesOptions = Highcharts.SeriesOptionsType & { fname?: string; }; -type SetActionParamsEventHandler = { - type: 'setActionParams'; -}; - -export type WidgetEventHandler = SetActionParamsEventHandler; - -export type GraphWidgetEventScope = 'point' | 'series'; - -type WidgetEvent = { - handler: WidgetEventHandler | WidgetEventHandler[]; - scope?: T; -}; - export type GraphWidget = WidgetBaseWithData & WithControls & { type: 'graph'; diff --git a/src/ui/units/dash/containers/Dialogs/Widget/Widget.tsx b/src/ui/units/dash/containers/Dialogs/Widget/Widget.tsx index 027060abe5..bc1f487712 100644 --- a/src/ui/units/dash/containers/Dialogs/Widget/Widget.tsx +++ b/src/ui/units/dash/containers/Dialogs/Widget/Widget.tsx @@ -16,6 +16,7 @@ import { StringParams, WidgetKind, WidgetType, + WizardType, } from 'shared'; import {DatalensGlobalState} from 'ui'; import {BetaMark} from 'ui/components/BetaMark/BetaMark'; @@ -65,7 +66,19 @@ const isWidgetTypeWithAutoHeight = (widgetType?: WidgetKind) => { }; const isEntryTypeWithFiltering = (entryType?: WidgetType) => { - return entryType === EditorType.TableNode || entryType === EditorType.GraphNode; + const wizardFilteringAvailable = Utils.isEnabledFeature( + Feature.WizardChartChartFilteringAvailable, + ); + const widgetTypesWithFilteringAvailable: WidgetType[] = [ + EditorType.TableNode, + EditorType.GraphNode, + ]; + + if (wizardFilteringAvailable) { + widgetTypesWithFilteringAvailable.push(WizardType.GraphWizardNode); + } + + return entryType && widgetTypesWithFilteringAvailable.includes(entryType); }; type LineProps = { diff --git a/src/ui/units/preview/components/Preview/Preview.tsx b/src/ui/units/preview/components/Preview/Preview.tsx index 3f2ef02d28..d6c35bc223 100644 --- a/src/ui/units/preview/components/Preview/Preview.tsx +++ b/src/ui/units/preview/components/Preview/Preview.tsx @@ -65,7 +65,7 @@ const Preview: React.FC = (props) => { isEmbedded, } = props; - const {noControls} = Utils.getOptionsFromSearch(search); + const {noControls, actionParamsEnabled} = Utils.getOptionsFromSearch(search); const possibleEntryId = React.useMemo(() => extractEntryId(idOrSource), [idOrSource]); @@ -197,6 +197,7 @@ const Preview: React.FC = (props) => { onChartLoad={onChartLoad} onChartRender={onChartRender} noControls={noControls} + actionParamsEnabled={actionParamsEnabled} forwardedRef={chartKitRef as unknown as React.RefObject} splitTooltip={hasSplitTooltip} menuType="preview" diff --git a/src/ui/utils/utils.ts b/src/ui/utils/utils.ts index 9c17b907b6..25cc8d5ef7 100644 --- a/src/ui/utils/utils.ts +++ b/src/ui/utils/utils.ts @@ -237,6 +237,7 @@ export default class Utils { noControls: searchParams.get(URL_OPTIONS.NO_CONTROLS) === '1' || searchParams.get(URL_OPTIONS.NO_CONTROLS) === 'true', // deprecated + actionParamsEnabled: searchParams.get(URL_OPTIONS.ACTION_PARAMS_ENABLED) === '1', }; }