From 9070ac954eb4988d4a6ab7ded7bd03d9ca102484 Mon Sep 17 00:00:00 2001 From: krsnik93 Date: Wed, 7 Apr 2021 09:32:17 +0100 Subject: [PATCH] feat: echarts gauge chart (#993) * feat: echarts gauge chart * remove unused legend imports * move font size multipliers to constants * add tests * rename roundcap * add modulo on color picking to wrap around the color scheme Co-authored-by: Ivan Krsnik --- .../src/Gauge/EchartsGauge.tsx | 25 ++ .../src/Gauge/buildQuery.ts | 28 ++ .../src/Gauge/constants.ts | 80 +++++ .../src/Gauge/controlPanel.tsx | 284 +++++++++++++++ .../src/Gauge/images/thumbnail.png | Bin 0 -> 21333 bytes .../plugin-chart-echarts/src/Gauge/index.ts | 39 ++ .../src/Gauge/transformProps.ts | 222 ++++++++++++ .../plugin-chart-echarts/src/Gauge/types.ts | 71 ++++ .../plugins/plugin-chart-echarts/src/index.ts | 1 + .../src/utils/controls.ts | 11 + .../test/Gauge/buildQuery.test.ts | 48 +++ .../test/Gauge/transformProps.test.ts | 334 ++++++++++++++++++ 12 files changed, 1143 insertions(+) create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/buildQuery.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/constants.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/images/thumbnail.png create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/index.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/types.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Gauge/buildQuery.test.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx new file mode 100644 index 000000000000..5f57bad421f9 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx @@ -0,0 +1,25 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { EchartsProps } from '../types'; +import Echart from '../components/Echart'; + +export default function EchartsGauge({ height, width, echartOptions }: EchartsProps) { + return ; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/buildQuery.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/buildQuery.ts new file mode 100644 index 000000000000..077e2baf46d1 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/buildQuery.ts @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { buildQueryContext, QueryFormData } from '@superset-ui/core'; + +export default function buildQuery(formData: QueryFormData) { + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + groupby: formData.groupby || [], + }, + ]); +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/constants.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/constants.ts new file mode 100644 index 000000000000..0257354f21de --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/constants.ts @@ -0,0 +1,80 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { GaugeSeriesOption } from 'echarts'; + +export const DEFAULT_GAUGE_SERIES_OPTION: GaugeSeriesOption = { + splitLine: { + lineStyle: { + color: '#63677A', + }, + }, + axisLine: { + lineStyle: { + color: [[1, '#E6EBF8']], + }, + }, + axisLabel: { + color: '#464646', + }, + axisTick: { + lineStyle: { + width: 2, + color: '#63677A', + }, + }, + detail: { + color: 'auto', + }, +}; + +export const INTERVAL_GAUGE_SERIES_OPTION: GaugeSeriesOption = { + splitLine: { + lineStyle: { + color: 'auto', + }, + }, + axisTick: { + lineStyle: { + color: 'auto', + }, + }, + axisLabel: { + color: 'auto', + }, + pointer: { + itemStyle: { + color: 'auto', + }, + }, +}; + +export const OFFSETS = { + ticksFromLine: 10, + titleFromCenter: 20, +}; + +export const FONT_SIZE_MULTIPLIERS = { + axisTickLength: 0.25, + axisLabelDistance: 1.2, + splitLineLength: 1, + splitLineWidth: 0.25, + titleOffsetFromTitle: 2, + detailOffsetFromTitle: 0.9, + detailFontSize: 1.2, +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx new file mode 100644 index 000000000000..50f259d51739 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx @@ -0,0 +1,284 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { t, validateNonEmpty, validateInteger } from '@superset-ui/core'; +import { sharedControls, ControlPanelConfig, D3_FORMAT_OPTIONS } from '@superset-ui/chart-controls'; +import { DEFAULT_FORM_DATA } from './types'; + +const config: ControlPanelConfig = { + controlPanelSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: [ + [ + { + name: 'groupby', + config: { + ...sharedControls.groupby, + label: t('Group by'), + description: t('Columns to group by'), + }, + }, + ], + ['metric'], + ['adhoc_filters'], + [ + { + name: 'row_limit', + config: { + ...sharedControls.row_limit, + choices: [...Array(10).keys()].map(n => n + 1), + default: DEFAULT_FORM_DATA.rowLimit, + }, + }, + ], + ], + }, + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [ + [

{t('General')}

], + [ + { + name: 'min_val', + config: { + type: 'TextControl', + isInt: true, + default: String(DEFAULT_FORM_DATA.minVal), + validators: [validateNonEmpty, validateInteger], + renderTrigger: true, + label: t('Min'), + description: t('Minimum value on the gauge axis'), + }, + }, + { + name: 'max_val', + config: { + type: 'TextControl', + isInt: true, + default: DEFAULT_FORM_DATA.maxVal, + validators: [validateNonEmpty, validateInteger], + renderTrigger: true, + label: t('Max'), + description: t('Maximum value on the gauge axis'), + }, + }, + ], + [ + { + name: 'start_angle', + config: { + type: 'TextControl', + label: t('Start angle'), + description: t('Angle at which to start progress axis'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.startAngle, + }, + }, + { + name: 'end_angle', + config: { + type: 'TextControl', + label: t('End angle'), + description: t('Angle at which to end progress axis'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.endAngle, + }, + }, + ], + ['color_scheme'], + [ + { + name: 'font_size', + config: { + type: 'SliderControl', + label: t('Font size'), + description: t('Font size for axis labels, detail value and other text elements'), + renderTrigger: true, + min: 10, + max: 20, + default: DEFAULT_FORM_DATA.fontSize, + }, + }, + ], + [ + { + name: 'number_format', + config: { + type: 'SelectControl', + label: t('Number format'), + description: 'D3 format syntax: https://github.com/d3/d3-format', + freeForm: true, + renderTrigger: true, + default: DEFAULT_FORM_DATA.numberFormat, + choices: D3_FORMAT_OPTIONS, + }, + }, + ], + [ + { + name: 'value_formatter', + config: { + type: 'TextControl', + label: t('Value format'), + description: t('Additional text to add before or after the value, e.g. unit'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.valueFormatter, + }, + }, + ], + [ + { + name: 'show_pointer', + config: { + type: 'CheckboxControl', + label: t('Show pointer'), + description: t('Whether to show the pointer'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.showPointer, + }, + }, + ], + [ + { + name: 'animation', + config: { + type: 'CheckboxControl', + label: t('Animation'), + description: t('Whether to animate the progress and the value or just display them'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.animation, + }, + }, + ], + [

{t('Axis')}

], + [ + { + name: 'show_axis_tick', + config: { + type: 'CheckboxControl', + label: t('Show axis line ticks'), + description: t('Whether to show minor ticks on the axis'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.showAxisTick, + }, + }, + ], + [ + { + name: 'show_split_line', + config: { + type: 'CheckboxControl', + label: t('Show split lines'), + description: t('Whether to show the split lines on the axis'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.showSplitLine, + }, + }, + ], + [ + { + name: 'split_number', + config: { + type: 'SliderControl', + label: t('Split number'), + description: t('Number of split segments on the axis'), + renderTrigger: true, + min: 3, + max: 30, + default: DEFAULT_FORM_DATA.splitNumber, + }, + }, + ], + [

{t('Progress')}

], + [ + { + name: 'show_progress', + config: { + type: 'CheckboxControl', + label: t('Show progress'), + description: t('Whether to show the progress of gauge chart'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.showProgress, + }, + }, + ], + [ + { + name: 'overlap', + config: { + type: 'CheckboxControl', + label: t('Overlap'), + description: t( + 'Whether the progress bar overlaps when there are multiple groups of data', + ), + renderTrigger: true, + default: DEFAULT_FORM_DATA.overlap, + }, + }, + ], + [ + { + name: 'round_cap', + config: { + type: 'CheckboxControl', + label: t('Round cap'), + description: t('Style the ends of the progress bar with a round cap'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.roundCap, + }, + }, + ], + [

{t('Intervals')}

], + [ + { + name: 'intervals', + config: { + type: 'TextControl', + label: t('Interval bounds'), + description: t( + 'Comma-separated interval bounds, e.g. 2,4,5 for intervals 0-2, 2-4 and 4-5. Last number should match the value provided for MAX.', + ), + renderTrigger: true, + default: DEFAULT_FORM_DATA.intervals, + }, + }, + ], + [ + { + name: 'interval_color_indices', + config: { + type: 'TextControl', + label: t('Interval colors'), + description: t( + 'Comma-separated color picks for the intervals, e.g. 1,2,4. Integers denote colors from the chosen color scheme and are 1-indexed. Length must be matching that of interval bounds.', + ), + renderTrigger: true, + default: DEFAULT_FORM_DATA.intervalColorIndices, + }, + }, + ], + ], + }, + ], +}; + +export default config; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/images/thumbnail.png b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/images/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..2ad56001daada48951212bf83392e86774cadbd3 GIT binary patch literal 21333 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 literal 0 HcmV?d00001 diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/index.ts new file mode 100644 index 000000000000..54fd67185c06 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/index.ts @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core'; +import controlPanel from './controlPanel'; +import transformProps from './transformProps'; +import thumbnail from './images/thumbnail.png'; +import buildQuery from './buildQuery'; + +export default class EchartsGaugeChartPlugin extends ChartPlugin { + constructor() { + super({ + buildQuery, + controlPanel, + loadChart: () => import('./EchartsGauge'), + metadata: new ChartMetadata({ + credits: ['https://echarts.apache.org'], + name: t('Gauge Chart'), + thumbnail, + }), + transformProps, + }); + } +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts new file mode 100644 index 000000000000..da9de2976ef0 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts @@ -0,0 +1,222 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + QueryFormMetric, + ChartProps, + CategoricalColorNamespace, + CategoricalColorScale, + DataRecord, + getNumberFormatter, + getMetricLabel, +} from '@superset-ui/core'; +import { EChartsOption, GaugeSeriesOption } from 'echarts'; +import { GaugeDataItemOption } from 'echarts/types/src/chart/gauge/GaugeSeries'; +import { parseNumbersList } from '../utils/controls'; +import { + DEFAULT_FORM_DATA as DEFAULT_GAUGE_FORM_DATA, + EchartsGaugeFormData, + AxisTickLineStyle, +} from './types'; +import { + DEFAULT_GAUGE_SERIES_OPTION, + INTERVAL_GAUGE_SERIES_OPTION, + OFFSETS, + FONT_SIZE_MULTIPLIERS, +} from './constants'; + +const setIntervalBoundsAndColors = ( + intervals: string, + intervalColorIndices: string, + colorFn: CategoricalColorScale, + normalizer: number, +): Array<[number, string]> => { + let intervalBoundsNonNormalized; + let intervalColorIndicesArray; + try { + intervalBoundsNonNormalized = parseNumbersList(intervals, ','); + intervalColorIndicesArray = parseNumbersList(intervalColorIndices, ','); + } catch (error) { + intervalBoundsNonNormalized = [] as number[]; + intervalColorIndicesArray = [] as number[]; + } + + const intervalBounds = intervalBoundsNonNormalized.map(bound => bound / normalizer); + const intervalColors = intervalColorIndicesArray.map( + ind => colorFn.colors[(ind - 1) % colorFn.colors.length], + ); + + return intervalBounds.map((val, idx) => { + const color = intervalColors[idx]; + return [val, color || colorFn.colors[idx]]; + }); +}; + +const calculateAxisLineWidth = (data: DataRecord[], fontSize: number, overlap: boolean): number => + overlap ? fontSize : data.length * fontSize; + +export default function transformProps(chartProps: ChartProps) { + const { width, height, formData, queriesData } = chartProps; + const { + groupby, + metric, + minVal, + maxVal, + colorScheme, + fontSize, + numberFormat, + animation, + showProgress, + overlap, + roundCap, + showAxisTick, + showSplitLine, + splitNumber, + startAngle, + endAngle, + showPointer, + intervals, + intervalColorIndices, + valueFormatter, + }: EchartsGaugeFormData = { ...DEFAULT_GAUGE_FORM_DATA, ...formData }; + const data = (queriesData[0]?.data || []) as DataRecord[]; + const numberFormatter = getNumberFormatter(numberFormat); + const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); + const normalizer = maxVal; + const axisLineWidth = calculateAxisLineWidth(data, fontSize, overlap); + const axisTickLength = FONT_SIZE_MULTIPLIERS.axisTickLength * fontSize; + const splitLineLength = FONT_SIZE_MULTIPLIERS.splitLineLength * fontSize; + const titleOffsetFromTitle = FONT_SIZE_MULTIPLIERS.titleOffsetFromTitle * fontSize; + const detailOffsetFromTitle = FONT_SIZE_MULTIPLIERS.detailOffsetFromTitle * fontSize; + const intervalBoundsAndColors = setIntervalBoundsAndColors( + intervals, + intervalColorIndices, + colorFn, + normalizer, + ); + const transformedData: GaugeDataItemOption[] = data.map((data_point, index) => ({ + value: data_point[getMetricLabel(metric as QueryFormMetric)] as number, + name: groupby.map(column => `${column}: ${data_point[column]}`).join(', '), + itemStyle: { + color: colorFn(index), + }, + title: { + offsetCenter: ['0%', `${index * titleOffsetFromTitle + OFFSETS.titleFromCenter}%`], + fontSize, + }, + detail: { + offsetCenter: [ + '0%', + `${index * titleOffsetFromTitle + OFFSETS.titleFromCenter + detailOffsetFromTitle}%`, + ], + fontSize: FONT_SIZE_MULTIPLIERS.detailFontSize * fontSize, + }, + })); + + const formatValue = (value: number) => valueFormatter.replace('{value}', numberFormatter(value)); + + const progress = { + show: showProgress, + overlap, + roundCap, + width: fontSize, + }; + const splitLine = { + show: showSplitLine, + distance: -axisLineWidth - splitLineLength - OFFSETS.ticksFromLine, + length: splitLineLength, + lineStyle: { + width: FONT_SIZE_MULTIPLIERS.splitLineWidth * fontSize, + color: DEFAULT_GAUGE_SERIES_OPTION.splitLine?.lineStyle?.color, + }, + }; + const axisLine = { + roundCap, + lineStyle: { + width: axisLineWidth, + color: DEFAULT_GAUGE_SERIES_OPTION.axisLine?.lineStyle?.color, + }, + }; + const axisLabel = { + distance: + axisLineWidth - + FONT_SIZE_MULTIPLIERS.axisLabelDistance * fontSize - + (showSplitLine ? splitLineLength : 0) - + OFFSETS.ticksFromLine, + fontSize, + formatter: numberFormatter, + color: DEFAULT_GAUGE_SERIES_OPTION.axisLabel?.color, + }; + const axisTick = { + show: showAxisTick, + distance: -axisLineWidth - axisTickLength - OFFSETS.ticksFromLine, + length: axisTickLength, + lineStyle: DEFAULT_GAUGE_SERIES_OPTION.axisTick?.lineStyle as AxisTickLineStyle, + }; + const detail = { + valueAnimation: animation, + formatter: (value: number) => formatValue(value), + color: DEFAULT_GAUGE_SERIES_OPTION.detail?.color, + }; + let pointer; + + if (intervalBoundsAndColors.length) { + splitLine.lineStyle.color = INTERVAL_GAUGE_SERIES_OPTION.splitLine?.lineStyle?.color; + axisTick.lineStyle.color = INTERVAL_GAUGE_SERIES_OPTION?.axisTick?.lineStyle?.color as string; + axisLabel.color = INTERVAL_GAUGE_SERIES_OPTION.axisLabel?.color; + axisLine.lineStyle.color = intervalBoundsAndColors; + pointer = { + show: showPointer, + itemStyle: INTERVAL_GAUGE_SERIES_OPTION.pointer?.itemStyle, + }; + } else { + pointer = { + show: showPointer, + }; + } + + const series: GaugeSeriesOption[] = [ + { + type: 'gauge', + startAngle, + endAngle, + min: minVal, + max: maxVal, + progress, + animation, + axisLine: axisLine as GaugeSeriesOption['axisLine'], + splitLine, + splitNumber, + axisLabel, + axisTick, + pointer, + detail, + data: transformedData, + }, + ]; + + const echartOptions: EChartsOption = { + series, + }; + + return { + width, + height, + echartOptions, + }; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/types.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/types.ts new file mode 100644 index 000000000000..42b579b4fc8e --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/types.ts @@ -0,0 +1,71 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { DEFAULT_LEGEND_FORM_DATA } from '../types'; + +export type AxisTickLineStyle = { + width: number; + color: string; +}; + +export type EchartsGaugeFormData = { + colorScheme?: string; + groupby: string[]; + metric?: object; + rowLimit: number; + minVal: number; + maxVal: number; + fontSize: number; + numberFormat: string; + animation: boolean; + showProgress: boolean; + overlap: boolean; + roundCap: boolean; + showAxisTick: boolean; + showSplitLine: boolean; + splitNumber: number; + startAngle: number; + endAngle: number; + showPointer: boolean; + intervals: string; + intervalColorIndices: string; + valueFormatter: string; +}; + +export const DEFAULT_FORM_DATA: EchartsGaugeFormData = { + ...DEFAULT_LEGEND_FORM_DATA, + groupby: [], + rowLimit: 10, + minVal: 0, + maxVal: 100, + fontSize: 15, + numberFormat: 'SMART_NUMBER', + animation: true, + showProgress: true, + overlap: true, + roundCap: false, + showAxisTick: false, + showSplitLine: false, + splitNumber: 10, + startAngle: 225, + endAngle: -45, + showPointer: true, + intervals: '', + intervalColorIndices: '', + valueFormatter: '{value}', +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/index.ts index ccdc6f95052e..2c62d49cd062 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/index.ts @@ -20,6 +20,7 @@ export { default as EchartsBoxPlotChartPlugin } from './BoxPlot'; export { default as EchartsTimeseriesChartPlugin } from './Timeseries'; export { default as EchartsPieChartPlugin } from './Pie'; export { default as EchartsGraphChartPlugin } from './Graph'; +export { default as EchartsGaugeChartPlugin } from './Gauge'; /** * Note: this file exports the default export from EchartsTimeseries.tsx. diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/utils/controls.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/utils/controls.ts index 1c4f310be280..c0385bf5c093 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/utils/controls.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/utils/controls.ts @@ -16,6 +16,9 @@ * specific language governing permissions and limitations * under the License. */ + +import { validateNumber } from '@superset-ui/core'; + // eslint-disable-next-line import/prefer-default-export export function parseYAxisBound(bound?: string | number | null): number | undefined { if (bound === undefined || bound === null || Number.isNaN(Number(bound))) { @@ -23,3 +26,11 @@ export function parseYAxisBound(bound?: string | number | null): number | undefi } return Number(bound); } + +export function parseNumbersList(value: string, delim = ';') { + if (!value || !value.trim()) return []; + return value.split(delim).map(num => { + if (validateNumber(num)) throw new Error('All values must be numeric'); + return Number(num); + }); +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Gauge/buildQuery.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Gauge/buildQuery.test.ts new file mode 100644 index 000000000000..e300f2cf7233 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Gauge/buildQuery.test.ts @@ -0,0 +1,48 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import buildQuery from '../../src/Gauge/buildQuery'; + +describe('Gauge buildQuery', () => { + const baseFormData = { + datasource: '5__table', + metric: 'foo', + viz_type: 'my_chart', + }; + + it('should build query fields with no group by column', () => { + const formData = { ...baseFormData, groupby: null }; + const queryContext = buildQuery(formData); + const [query] = queryContext.queries; + expect(query.groupby).toEqual([]); + }); + + it('should build query fields with single group by column', () => { + const formData = { ...baseFormData, groupby: ['foo'] }; + const queryContext = buildQuery(formData); + const [query] = queryContext.queries; + expect(query.groupby).toEqual(['foo']); + }); + + it('should build query fields with multiple group by columns', () => { + const formData = { ...baseFormData, groupby: ['foo', 'bar'] }; + const queryContext = buildQuery(formData); + const [query] = queryContext.queries; + expect(query.groupby).toEqual(['foo', 'bar']); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts new file mode 100644 index 000000000000..210ba3be80e5 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts @@ -0,0 +1,334 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { ChartProps } from '@superset-ui/core'; +import transformProps from '../../src/Gauge/transformProps'; +import { DEFAULT_GAUGE_SERIES_OPTION } from '../../src/Gauge/constants'; + +describe('Echarts Gauge transformProps', () => { + const baseFormData = { + datasource: '26__table', + vizType: 'gauge_chart', + metric: 'count', + adhocFilters: [], + rowLimit: 10, + minVal: '0', + maxVal: 100, + startAngle: 225, + endAngle: -45, + colorScheme: 'SUPERSET_DEFAULT', + fontSize: 14, + numberFormat: 'SMART_NUMBER', + valueFormatter: '{value}', + showPointer: true, + animation: true, + showAxisTick: false, + showSplitLine: false, + splitNumber: 10, + showProgress: true, + overlap: true, + roundCap: false, + }; + + it('should transform chart props for no group by column', () => { + const formData = { ...baseFormData, groupby: [] }; + const queriesData = [ + { + colnames: ['count'], + data: [ + { + count: 16595, + }, + ], + }, + ]; + + const chartPropsConfig = { + formData, + width: 800, + height: 600, + queriesData, + }; + + const chartProps = new ChartProps(chartPropsConfig); + expect(transformProps(chartProps)).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + data: [ + { + value: 16595, + name: '', + itemStyle: { + color: '#1f77b4', + }, + title: { + offsetCenter: ['0%', '20%'], + fontSize: 14, + }, + detail: { + offsetCenter: ['0%', '32.6%'], + fontSize: 16.8, + }, + }, + ], + }), + ]), + }), + }), + ); + }); + + it('should transform chart props for single group by column', () => { + const formData = { ...baseFormData, groupby: ['year'] }; + const queriesData = [ + { + colnames: ['year', 'count'], + data: [ + { + year: 1988, + count: 15, + }, + { + year: 1995, + count: 219, + }, + ], + }, + ]; + + const chartPropsConfig = { + formData, + width: 800, + height: 600, + queriesData, + }; + + const chartProps = new ChartProps(chartPropsConfig); + expect(transformProps(chartProps)).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + data: [ + { + value: 15, + name: 'year: 1988', + itemStyle: { + color: '#1f77b4', + }, + title: { + offsetCenter: ['0%', '20%'], + fontSize: 14, + }, + detail: { + offsetCenter: ['0%', '32.6%'], + fontSize: 16.8, + }, + }, + { + value: 219, + name: 'year: 1995', + itemStyle: { + color: '#ff7f0e', + }, + title: { + offsetCenter: ['0%', '48%'], + fontSize: 14, + }, + detail: { + offsetCenter: ['0%', '60.6%'], + fontSize: 16.8, + }, + }, + ], + }), + ]), + }), + }), + ); + }); + + it('should transform chart props for multiple group by columns', () => { + const formData = { ...baseFormData, groupby: ['year', 'platform'] }; + const queriesData = [ + { + colnames: ['year', 'platform', 'count'], + data: [ + { + year: 2011, + platform: 'PC', + count: 140, + }, + { + year: 2008, + platform: 'PC', + count: 76, + }, + ], + }, + ]; + + const chartPropsConfig = { + formData, + width: 800, + height: 600, + queriesData, + }; + + const chartProps = new ChartProps(chartPropsConfig); + expect(transformProps(chartProps)).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + data: [ + { + value: 140, + name: 'year: 2011, platform: PC', + itemStyle: { + color: '#1f77b4', + }, + title: { + offsetCenter: ['0%', '20%'], + fontSize: 14, + }, + detail: { + offsetCenter: ['0%', '32.6%'], + fontSize: 16.8, + }, + }, + { + value: 76, + name: 'year: 2008, platform: PC', + itemStyle: { + color: '#ff7f0e', + }, + title: { + offsetCenter: ['0%', '48%'], + fontSize: 14, + }, + detail: { + offsetCenter: ['0%', '60.6%'], + fontSize: 16.8, + }, + }, + ], + }), + ]), + }), + }), + ); + }); + + it('should transform chart props for intervals', () => { + const formData = { + ...baseFormData, + groupby: ['year', 'platform'], + intervals: '50,100', + intervalColorIndices: '1,2', + }; + const queriesData = [ + { + colnames: ['year', 'platform', 'count'], + data: [ + { + year: 2011, + platform: 'PC', + count: 140, + }, + { + year: 2008, + platform: 'PC', + count: 76, + }, + ], + }, + ]; + + const chartPropsConfig = { + formData, + width: 800, + height: 600, + queriesData, + }; + + const chartProps = new ChartProps(chartPropsConfig); + expect(transformProps(chartProps)).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + axisLine: { + lineStyle: { + width: 14, + color: [ + [0.5, '#1f77b4'], + [1, '#ff7f0e'], + ], + }, + roundCap: false, + }, + data: [ + { + value: 140, + name: 'year: 2011, platform: PC', + itemStyle: { + color: '#1f77b4', + }, + title: { + offsetCenter: ['0%', '20%'], + fontSize: 14, + }, + detail: { + offsetCenter: ['0%', '32.6%'], + fontSize: 16.8, + }, + }, + { + value: 76, + name: 'year: 2008, platform: PC', + itemStyle: { + color: '#ff7f0e', + }, + title: { + offsetCenter: ['0%', '48%'], + fontSize: 14, + }, + detail: { + offsetCenter: ['0%', '60.6%'], + fontSize: 16.8, + }, + }, + ], + }), + ]), + }), + }), + ); + }); +});