diff --git a/src/i18n-keysets/sql/en.json b/src/i18n-keysets/sql/en.json index 6054a25ac6..18c7f6b8cc 100644 --- a/src/i18n-keysets/sql/en.json +++ b/src/i18n-keysets/sql/en.json @@ -49,6 +49,7 @@ "label_visualization-column-100p": "Normalized column chart", "label_visualization-donut": "Donut chart", "label_visualization-flat-table": "Table", + "label_visualization-funnel": "Funnel chart", "label_visualization-line": "Line chart", "label_visualization-metric": "Indicator", "label_visualization-pie": "Pie chart", diff --git a/src/i18n-keysets/sql/ru.json b/src/i18n-keysets/sql/ru.json index f082ac1353..3118d12b52 100644 --- a/src/i18n-keysets/sql/ru.json +++ b/src/i18n-keysets/sql/ru.json @@ -47,12 +47,13 @@ "label_visualization-bar-100p": "Нормированная линейчатая диаграмма", "label_visualization-column": "Столбчатая диаграмма", "label_visualization-column-100p": "Нормированная cтолбчатая диаграмма", - "label_visualization-donut": "Бубликовая диаграмма", + "label_visualization-donut": "Кольцевая диаграмма", "label_visualization-flat-table": "Таблица", + "label_visualization-funnel": "Воронка", "label_visualization-line": "Линейная диаграмма", "label_visualization-metric": "Индикатор", "label_visualization-pie": "Круговая диаграмма", "label_visualization-scatter": "Точечная диаграмма", "label_visualization-treemap": "Древовидная диаграмма", "text_default-name": "Новый QL-чарт" -} +} \ No newline at end of file diff --git a/src/i18n-keysets/wizard/en.json b/src/i18n-keysets/wizard/en.json index b2232d2afd..09914f2344 100644 --- a/src/i18n-keysets/wizard/en.json +++ b/src/i18n-keysets/wizard/en.json @@ -302,6 +302,7 @@ "label_visualization-combined-chart": "Combined chart", "label_visualization-donut": "Donut chart", "label_visualization-flat-table": "Table", + "label_visualization-funnel": "Funnel chart", "label_visualization-geolayer": "Map", "label_visualization-geopoint": "Points", "label_visualization-geopoint-with-cluster": "Points with cluster", diff --git a/src/i18n-keysets/wizard/ru.json b/src/i18n-keysets/wizard/ru.json index 19cadcc5c3..0d603f4e40 100644 --- a/src/i18n-keysets/wizard/ru.json +++ b/src/i18n-keysets/wizard/ru.json @@ -302,6 +302,7 @@ "label_visualization-combined-chart": "Комбинированная диаграмма", "label_visualization-donut": "Кольцевая диаграмма", "label_visualization-flat-table": "Таблица", + "label_visualization-funnel": "Воронка", "label_visualization-geolayer": "Карта", "label_visualization-geopoint": "Точки", "label_visualization-geopoint-with-cluster": "Точки с кластеризацией", @@ -388,4 +389,4 @@ "value_geotype-select-geopolygon": "Полигоны (Геoполигоны)", "value_geotype-select-heatmap": "Теплокарта (Геоточки)", "value_geotype-select-polyline": "Полилинии (Геоточки)" -} +} \ No newline at end of file diff --git a/src/server/components/features/features-list/FunnelChart.ts b/src/server/components/features/features-list/FunnelChart.ts new file mode 100644 index 0000000000..c3e2597639 --- /dev/null +++ b/src/server/components/features/features-list/FunnelChart.ts @@ -0,0 +1,10 @@ +import {Feature} from '../../../../shared'; +import {createFeatureConfig} from '../utils'; + +export default createFeatureConfig({ + name: Feature.FunnelChart, + state: { + development: true, + production: false, + }, +}); diff --git a/src/server/modes/charts/plugins/datalens/js/js.ts b/src/server/modes/charts/plugins/datalens/js/js.ts index 1d56143476..757a7b055f 100644 --- a/src/server/modes/charts/plugins/datalens/js/js.ts +++ b/src/server/modes/charts/plugins/datalens/js/js.ts @@ -35,6 +35,7 @@ import type {PivotData} from '../preparers/backend-pivot-table/types'; import {prepareGravityChartBarX, prepareHighchartsBarX} from '../preparers/bar-x'; import {prepareGravityChartsBarY, prepareHighchartsBarY} from '../preparers/bar-y'; import prepareFlatTableData from '../preparers/flat-table'; +import {prepareFunnel} from '../preparers/funnel'; import prepareGeopointData from '../preparers/geopoint'; import prepareGeopointWithClusterData from '../preparers/geopoint-with-cluster'; import prepareGeopolygonData from '../preparers/geopolygon'; @@ -581,6 +582,11 @@ function prepareSingleResult({ rowsLimit = 800; break; + case WizardVisualizationId.Funnel: + prepare = prepareFunnel; + rowsLimit = 800; + break; + case 'flatTable': prepare = prepareFlatTableData; rowsLimit = 100000; diff --git a/src/server/modes/charts/plugins/datalens/preparers/funnel/index.ts b/src/server/modes/charts/plugins/datalens/preparers/funnel/index.ts new file mode 100644 index 0000000000..dc80ff8bec --- /dev/null +++ b/src/server/modes/charts/plugins/datalens/preparers/funnel/index.ts @@ -0,0 +1,158 @@ +import type {ChartData, FunnelSeries} from '@gravity-ui/chartkit/gravity-charts'; +import merge from 'lodash/merge'; + +import { + PlaceholderId, + getFakeTitleOrTitle, + getFormatOptions, + isMeasureValue, + isNumberField, + isPseudoField, +} from '../../../../../../../shared'; +import {getBaseChartConfig} from '../../gravity-charts/utils'; +import type {ColorValue} from '../../utils/color-helpers'; +import {getColorsByMeasureField, getThresholdValues} from '../../utils/color-helpers'; +import { + chartKitFormatNumberWrapper, + findIndexInOrder, + isGradientMode, +} from '../../utils/misc-helpers'; +import {getFormattedValue} from '../helpers/get-formatted-value'; +import {getLegendColorScale} from '../helpers/legend'; +import type {PrepareFunctionArgs} from '../types'; + +export function prepareFunnel({ + shared, + idToTitle, + idToDataType, + resultData, + placeholders, + labels, + colors, + colorsConfig, +}: PrepareFunctionArgs): Partial { + const {data, order} = resultData; + const measures = placeholders.find((p) => p.id === PlaceholderId.Measures)?.items ?? []; + + const colorItem = colors?.[0]; + const colorField = colorItem + ? {...colorItem, data_type: idToDataType[colorItem.guid]} + : colorItem; + const colorIndex = colorField + ? findIndexInOrder(order, colorField, idToTitle[colorField.guid]) + : -1; + + const labelItem = labels?.[0]; + const labelField = labelItem + ? {...labelItem, data_type: idToDataType[labelItem.guid]} + : labelItem; + const labelIndex = labelField + ? findIndexInOrder(order, labelField, idToTitle[labelField.guid]) + : -1; + + const series: FunnelSeries = { + type: 'funnel', + name: '', + data: [], + dataLabels: { + enabled: Boolean(labelItem), + }, + }; + + data.forEach((values) => { + measures.forEach((measureItem) => { + const actualTitle = idToTitle[measureItem.guid]; + const i = findIndexInOrder(order, measureItem, actualTitle); + const _measureValue = values[i]; + if (_measureValue === null) { + return; + } + + const measureValue = Number(_measureValue); + const measureName = getFakeTitleOrTitle(measureItem); + + let colorValue; + if (isMeasureValue(colorField)) { + colorValue = measureValue; + } else if (isNumberField(colorField)) { + colorValue = values[colorIndex]; + } + + let labelValue; + if (isPseudoField(labelField)) { + labelValue = isMeasureValue(labelField) + ? chartKitFormatNumberWrapper(measureValue, { + lang: 'ru', + ...getFormatOptions(measureItem), + }) + : measureName; + } else { + labelValue = getFormattedValue(labelField, values[labelIndex]); + } + + series.data.push({ + value: Number(measureValue), + name: measureName, + label: labelValue as string | undefined, + custom: { + colorValue: colorValue === null ? null : Number(colorValue), + }, + }); + }); + }); + + const isColoringByMeasure = isGradientMode({ + colorField: colorField, + colorFieldDataType: colorField?.data_type, + colorsConfig, + }); + + let legend: ChartData['legend'] = {}; + + if (isColoringByMeasure) { + const points = series.data.map((d) => ({colorValue: d.custom.colorValue as unknown})); + const colorValues = points.map((p) => p.colorValue) as ColorValue[]; + + const gradientThresholdValues = getThresholdValues(colorsConfig, colorValues); + const gradientColors = getColorsByMeasureField({ + values: colorValues, + colorsConfig, + gradientThresholdValues, + }); + + series.data.forEach((d) => { + const pointColorValue = Number(d.custom.colorValue); + + if (gradientColors[pointColorValue]) { + // eslint-disable-next-line no-param-reassign + d.color = gradientColors[pointColorValue]; + } + }); + + const colorScale = getLegendColorScale({ + colorsConfig, + points, + }); + legend = { + enabled: true, + type: 'continuous', + colorScale, + }; + } else { + legend.enabled = series.data.length > 1; + } + + return merge(getBaseChartConfig(shared), { + series: { + data: [series], + }, + legend, + chart: { + zoom: {enabled: false}, + margin: { + left: 20, + right: 20, + }, + }, + }); +} diff --git a/src/server/modes/charts/plugins/datalens/preparers/helpers/get-formatted-value.ts b/src/server/modes/charts/plugins/datalens/preparers/helpers/get-formatted-value.ts index 17757ba5ed..9566c9441d 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/helpers/get-formatted-value.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/helpers/get-formatted-value.ts @@ -3,6 +3,10 @@ import {getFormatOptions, isDateField, isNumberField} from '../../../../../../.. import {chartKitFormatNumberWrapper, formatDate} from '../../utils/misc-helpers'; export function getFormattedValue(field: ServerField, value: unknown) { + if (value === null) { + return null; + } + if (isDateField(field)) { return formatDate({ valueType: field.data_type, diff --git a/src/shared/constants/visualization.ts b/src/shared/constants/visualization.ts index d9b3bd4224..09067632c5 100644 --- a/src/shared/constants/visualization.ts +++ b/src/shared/constants/visualization.ts @@ -18,6 +18,7 @@ export enum WizardVisualizationId { Geopolygon = 'geopolygon', GeopointWithCluster = 'geopoint-with-cluster', CombinedChart = 'combined-chart', + Funnel = 'funnel', } export enum QlVisualizationId { diff --git a/src/shared/types/configs/index.ts b/src/shared/types/configs/index.ts index 90559ad7df..45899e3042 100644 --- a/src/shared/types/configs/index.ts +++ b/src/shared/types/configs/index.ts @@ -15,6 +15,7 @@ export type IconId = | 'visPie' | 'visPivot' | 'visTreemap' + | 'visFunnel' | 'collectionColored' | 'collectionColoredDark' | 'collectionColoredBig' diff --git a/src/shared/types/feature.ts b/src/shared/types/feature.ts index 3b253e505d..8bb4ae616a 100644 --- a/src/shared/types/feature.ts +++ b/src/shared/types/feature.ts @@ -99,6 +99,8 @@ export enum Feature { EnableDashColorPickersByTheme = 'EnableDashColorPickersByTheme', /** Shows updated settings page */ EnableNewServiceSettings = 'EnableNewServiceSettings', + /** Funnel chart visualization */ + FunnelChart = 'FunnelChart', /** Replace static master token with dynamic one */ UsDynamicMasterToken = 'UsDynamicMasterToken', /** Enable using dynamic master token in proxy */ diff --git a/src/shared/types/wizard/index.ts b/src/shared/types/wizard/index.ts index 11fdecd475..fe4341b332 100644 --- a/src/shared/types/wizard/index.ts +++ b/src/shared/types/wizard/index.ts @@ -238,7 +238,8 @@ export interface GraphShared extends CommonShared { | WizardVisualizationId.Pie | WizardVisualizationId.Donut | WizardVisualizationId.Scatter - | WizardVisualizationId.Treemap; + | WizardVisualizationId.Treemap + | WizardVisualizationId.Funnel; iconProps: VisualizationIconProps; name: string; hidden?: boolean; diff --git a/src/shared/utils/visualization-check.ts b/src/shared/utils/visualization-check.ts index 73f380e606..9acf32a2b6 100644 --- a/src/shared/utils/visualization-check.ts +++ b/src/shared/utils/visualization-check.ts @@ -36,6 +36,10 @@ export function isGravityChartsVisualization({ id: string; features?: FeatureConfig; }) { + if (id === WizardVisualizationId.Funnel) { + return true; + } + const isPieOrTreemap = [ WizardVisualizationId.Pie, WizardVisualizationId.Donut, diff --git a/src/ui/assets/icons/vis-funnel.svg b/src/ui/assets/icons/vis-funnel.svg new file mode 100644 index 0000000000..cc7cd2bd26 --- /dev/null +++ b/src/ui/assets/icons/vis-funnel.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/ui/configs/common/icons.ts b/src/ui/configs/common/icons.ts index 49c724af48..cf486534a4 100644 --- a/src/ui/configs/common/icons.ts +++ b/src/ui/configs/common/icons.ts @@ -21,6 +21,7 @@ import visColumn from 'assets/icons/vis-column.svg'; import visCombined from 'assets/icons/vis-combined.svg'; import visDonut from 'assets/icons/vis-donut.svg'; import visFlatTable from 'assets/icons/vis-flat-table.svg'; +import visFunnel from 'assets/icons/vis-funnel.svg'; import visGeolayers from 'assets/icons/vis-geolayers.svg'; import visHeatmap from 'assets/icons/vis-heatmap.svg'; import visLines from 'assets/icons/vis-lines.svg'; @@ -47,6 +48,7 @@ export default { visPie, visPivot, visTreemap, + visFunnel, visHeatmap, collectionColored, collectionColoredDark, diff --git a/src/ui/constants/visualizations/funnel.ts b/src/ui/constants/visualizations/funnel.ts new file mode 100644 index 0000000000..9eb60d5d1f --- /dev/null +++ b/src/ui/constants/visualizations/funnel.ts @@ -0,0 +1,32 @@ +import {SquareHashtag} from '@gravity-ui/icons'; +import type {Field, GraphShared} from 'shared'; +import {WizardVisualizationId} from 'shared'; +import {prepareFieldToMeasureTransformation} from 'units/wizard/utils/visualization'; + +import {ITEM_TYPES, PRIMITIVE_DATA_TYPES} from '../misc'; + +export const FUNNEL_VISUALIZATION: GraphShared['visualization'] = { + id: WizardVisualizationId.Funnel, + type: 'funnel', + name: 'label_visualization-funnel', + iconProps: {id: 'visFunnel', width: '24'}, + allowColors: true, + checkAllowedDesignItems: ({item}) => ITEM_TYPES.MEASURES_AND_PSEUDO.has(item.type), + allowLabels: true, + checkAllowedLabels: (item: Field) => ITEM_TYPES.MEASURES_AND_PSEUDO.has(item.type), + allowFilters: true, + placeholders: [ + { + allowedTypes: ITEM_TYPES.DIMENSIONS_AND_MEASURES, + allowedFinalTypes: ITEM_TYPES.MEASURES, + allowedDataTypes: PRIMITIVE_DATA_TYPES, + id: 'measures', + type: 'measures', + title: 'section_measures', + iconProps: {data: SquareHashtag}, + items: [], + required: true, + transform: prepareFieldToMeasureTransformation, + }, + ], +}; diff --git a/src/ui/constants/visualizations/index.ts b/src/ui/constants/visualizations/index.ts index 9fd7c5de07..b1a9431f7e 100644 --- a/src/ui/constants/visualizations/index.ts +++ b/src/ui/constants/visualizations/index.ts @@ -20,6 +20,7 @@ export * from './line'; export * from './metric'; export * from './combined-chart'; export * from './scatter'; +export * from './funnel'; import {COMBINED_CHART_VISUALIZATION} from './combined-chart'; import {DONUT_VISUALIZATION} from './donut'; diff --git a/src/ui/units/wizard/containers/Wizard/SectionVisualization/PlaceholdersContainer/LabelsPlaceholder/LabelsPlaceholder.tsx b/src/ui/units/wizard/containers/Wizard/SectionVisualization/PlaceholdersContainer/LabelsPlaceholder/LabelsPlaceholder.tsx index e19a03d17a..46c80804b4 100644 --- a/src/ui/units/wizard/containers/Wizard/SectionVisualization/PlaceholdersContainer/LabelsPlaceholder/LabelsPlaceholder.tsx +++ b/src/ui/units/wizard/containers/Wizard/SectionVisualization/PlaceholdersContainer/LabelsPlaceholder/LabelsPlaceholder.tsx @@ -78,7 +78,7 @@ class LabelsPlaceholder extends React.Component { return false; } - return (visualization as any).checkAllowedLabels(item); + return (visualization as any).checkAllowedLabels?.(item); }; private onLabelsUpdate = (items: Field[]) => { diff --git a/src/ui/units/wizard/reducers/preview.ts b/src/ui/units/wizard/reducers/preview.ts index 702a7bd8ac..9d247874aa 100644 --- a/src/ui/units/wizard/reducers/preview.ts +++ b/src/ui/units/wizard/reducers/preview.ts @@ -215,7 +215,7 @@ function mutateAndValidateVisualization({ shapes = [], }: MutateAndValidateVisualizationArgs) { // We validate each item sequentially, while it is important that each previous item is executed - let everythingIsOk = visualization.placeholders.some((placeholder: Placeholder) => { + let everythingIsOk = visualization.placeholders?.some((placeholder: Placeholder) => { return placeholder.items.length > 0; }); diff --git a/src/ui/units/wizard/utils/visualization.ts b/src/ui/units/wizard/utils/visualization.ts index c165fd476e..7f27870120 100644 --- a/src/ui/units/wizard/utils/visualization.ts +++ b/src/ui/units/wizard/utils/visualization.ts @@ -1,7 +1,8 @@ import _ from 'lodash'; +import {isEnabledFeature} from 'ui/utils/isEnabledFeature'; import type {ChartkitGlobalSettings, Field} from '../../../../shared'; -import {DatasetFieldAggregation, isParameter} from '../../../../shared'; +import {DatasetFieldAggregation, Feature, isParameter} from '../../../../shared'; import {DL} from '../../../../ui'; import { AREA_100P_VISUALIZATION, @@ -13,6 +14,7 @@ import { COMBINED_CHART_VISUALIZATION, DONUT_VISUALIZATION, FLAT_TABLE_VISUALIZATION, + FUNNEL_VISUALIZATION, GEOLAYER_VISUALIZATION, GEOPOINT_VISUALIZATION, GEOPOINT_WITH_CLUSTER_VISUALIZATION, @@ -68,6 +70,10 @@ export function getAvailableVisualizations(options?: ChartkitGlobalSettings) { PIVOT_TABLE_VISUALIZATION, ]; + if (isEnabledFeature(Feature.FunnelChart)) { + items.push(FUNNEL_VISUALIZATION); + } + if (isYandexMapEnabled) { items.push( GEOPOINT_VISUALIZATION,