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 000000000000..2ad56001daad Binary files /dev/null and b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Gauge/images/thumbnail.png differ 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, + }, + }, + ], + }), + ]), + }), + }), + ); + }); +});