Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7a9f687
Test fork commit
nelsondrew Apr 9, 2025
1cec095
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 Apr 16, 2025
696d5d9
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 Apr 25, 2025
672c16f
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 Apr 29, 2025
3df058e
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 Apr 29, 2025
dc5820d
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 May 1, 2025
dce9fe3
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 May 4, 2025
7f51dca
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 May 11, 2025
e88e98d
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 May 14, 2025
4e92bb6
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 May 16, 2025
6d6d237
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 May 18, 2025
0e4927a
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 May 20, 2025
e0fd65f
Merge branch 'master' of https://github.com/apache/superset
amaannawab923 May 22, 2025
b374343
Radar chart normalisation
amaannawab923 May 22, 2025
9daf1a6
Getter method for safely obtaining denormalised values
amaannawab923 May 22, 2025
0c1f934
column config visiblity
amaannawab923 May 22, 2025
d0e4da2
Removing any
amaannawab923 May 22, 2025
11f3ba2
cosistency with normalize
amaannawab923 May 22, 2025
009dec2
is normalized
amaannawab923 May 22, 2025
11dd197
Transform props test radar chart
amaannawab923 May 22, 2025
0cc2183
Updated description
amaannawab923 May 22, 2025
4cd764d
Resolved comments
amaannawab923 May 23, 2025
022ed85
Apache license
amaannawab923 May 23, 2025
b7d608a
Remove normalisation control & apply normalisation as default
amaannawab923 May 23, 2025
e0959d2
Correcting unit test
amaannawab923 May 23, 2025
a6a13a6
removing logic of metric value as max
amaannawab923 May 24, 2025
9c6606e
handling min value
amaannawab923 May 24, 2025
66707a8
type
amaannawab923 May 24, 2025
a82c59d
Normalized tooltip as tsx
amaannawab923 May 26, 2025
edde6c9
using is defined
amaannawab923 May 26, 2025
b158611
No use before for metrics with custom bounds
amaannawab923 May 26, 2025
bf2fb5e
Using diff variable
amaannawab923 May 26, 2025
86478bd
string formatter tooltip
amaannawab923 May 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getNumberFormatter,
getTimeFormatter,
NumberFormatter,
isDefined,
} from '@superset-ui/core';
import type { CallbackDataParams } from 'echarts/types/src/util/types';
import type { RadarSeriesDataItemOption } from 'echarts/types/src/chart/radar/RadarSeries';
Expand All @@ -35,6 +36,7 @@ import {
EchartsRadarFormData,
EchartsRadarLabelType,
RadarChartTransformedProps,
SeriesNormalizedMap,
} from './types';
import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants';
import {
Expand All @@ -46,18 +48,31 @@ import {
import { defaultGrid } from '../defaults';
import { Refs } from '../types';
import { getDefaultTooltip } from '../utils/tooltip';
import { findGlobalMax, renderNormalizedTooltip } from './utils';

export function formatLabel({
params,
labelType,
numberFormatter,
getDenormalizedSeriesValue,
metricsWithCustomBounds,
metricLabels,
}: {
params: CallbackDataParams;
labelType: EchartsRadarLabelType;
numberFormatter: NumberFormatter;
getDenormalizedSeriesValue: (seriesName: string, value: string) => number;
metricsWithCustomBounds: Set<string>;
metricLabels: string[];
}): string {
const { name = '', value } = params;
const formattedValue = numberFormatter(value as number);
const { name = '', value, dimensionIndex = 0 } = params;
const metricLabel = metricLabels[dimensionIndex];

const formattedValue = numberFormatter(
metricsWithCustomBounds.has(metricLabel)
? (value as number)
: (getDenormalizedSeriesValue(name, String(value)) as number),
);

switch (labelType) {
case EchartsRadarLabelType.Value:
Expand Down Expand Up @@ -85,6 +100,7 @@ export default function transformProps(
} = chartProps;
const refs: Refs = {};
const { data = [] } = queriesData[0];
const globalMax = findGlobalMax(data, Object.keys(data[0] || {}));
const coltypeMapping = getColtypesMapping(queriesData[0]);

const {
Expand All @@ -111,14 +127,38 @@ export default function transformProps(
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getNumberFormatter(numberFormat);
const denormalizedSeriesValues: SeriesNormalizedMap = {};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use an Object to store the denormalised values belonging to each series because after normalising we need to pass the denormalised values to the label formatter & tooltip formatter to keep the chart consistent & preserve original values

const getDenormalizedSeriesValue = (
seriesName: string,
normalizedValue: string,
): number =>
denormalizedSeriesValues?.[seriesName]?.[normalizedValue] ??
Number(normalizedValue);

const metricLabels = metrics.map(getMetricLabel);

const metricsWithCustomBounds = new Set(
metricLabels.filter(metricLabel => {
const config = columnConfig?.[metricLabel];
const hasMax = !!isDefined(config?.radarMetricMaxValue);
const hasMin =
isDefined(config?.radarMetricMinValue) &&
config?.radarMetricMinValue !== 0;
return hasMax || hasMin;
}),
);

const formatter = (params: CallbackDataParams) =>
formatLabel({
params,
numberFormatter,
labelType,
getDenormalizedSeriesValue,
metricsWithCustomBounds,
metricLabels,
});

const metricLabels = metrics.map(getMetricLabel);
const groupbyLabels = groupby.map(getColumnLabel);

const metricLabelAndMaxValueMap = new Map<string, number>();
Expand Down Expand Up @@ -212,28 +252,58 @@ export default function transformProps(
{},
);

const normalizeArray = (arr: number[], decimals = 10, seriesName: string) =>
arr.map((value, index) => {
const metricLabel = metricLabels[index];
if (metricsWithCustomBounds.has(metricLabel)) {
return value;
}

const max = Math.max(...arr);
const normalizedValue = Number((value / max).toFixed(decimals));

denormalizedSeriesValues[seriesName][String(normalizedValue)] = value;
return normalizedValue;
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We normalise the values for each series only if we have isNormalised as true


// Normalize the transformed data
const normalizedTransformedData = transformedData.map(series => {
if (Array.isArray(series.value)) {
const seriesName = String(series?.name || '');
denormalizedSeriesValues[seriesName] = {};

return {
...series,
value: normalizeArray(series.value as number[], 10, seriesName),
};
}
return series;
});

const indicator = metricLabels.map(metricLabel => {
const isMetricWithCustomBounds = metricsWithCustomBounds.has(metricLabel);
if (!isMetricWithCustomBounds) {
return {
name: metricLabel,
max: 1,
min: 0,
};
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the values are normalised we consider min & max as 0 & 1 respectively & hence we dont need to calculate the rest as they are only in case when isNormalised is not true & some column config is provided

const maxValueInControl = columnConfig?.[metricLabel]?.radarMetricMaxValue;
const minValueInControl = columnConfig?.[metricLabel]?.radarMetricMinValue;

// Ensure that 0 is at the center of the polar coordinates
const metricValueAsMax =
const maxValue =
metricLabelAndMaxValueMap.get(metricLabel) === 0
? Number.MAX_SAFE_INTEGER
: metricLabelAndMaxValueMap.get(metricLabel);
const max =
maxValueInControl === null ? metricValueAsMax : maxValueInControl;
: globalMax;
const max = isDefined(maxValueInControl) ? maxValueInControl : maxValue;

let min: number;
// If the min value doesn't exist, set it to 0 (default),
// if it is null, set it to the min value of the data,
// otherwise, use the value from the control
if (minValueInControl === undefined) {
min = 0;
} else if (minValueInControl === null) {
min = metricLabelAndMinValueMap.get(metricLabel) || 0;
} else {
if (isDefined(minValueInControl)) {
min = minValueInControl;
} else {
min = 0;
}

return {
Expand All @@ -255,10 +325,24 @@ export default function transformProps(
backgroundColor: theme.colors.grayscale.light5,
},
},
data: transformedData,
data: normalizedTransformedData,
},
];

const NormalizedTooltipFormater = (
params: CallbackDataParams & {
color: string;
name: string;
value: number[];
},
) =>
renderNormalizedTooltip(
params,
metricLabels,
getDenormalizedSeriesValue,
metricsWithCustomBounds,
);

const echartOptions: EChartsCoreOption = {
grid: {
...defaultGrid,
Expand All @@ -267,6 +351,7 @@ export default function transformProps(
...getDefaultTooltip(refs),
show: !inContextMenu,
trigger: 'item',
formatter: NormalizedTooltipFormater,
},
legend: {
...getLegendProps(legendType, legendOrientation, showLegend, theme),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { DEFAULT_LEGEND_FORM_DATA } from '../constants';

type RadarColumnConfig = Record<
string,
{ radarMetricMaxValue?: number; radarMetricMinValue?: number }
{ radarMetricMaxValue?: number | null; radarMetricMinValue?: number }
>;

export type EchartsRadarFormData = QueryFormData &
Expand All @@ -53,6 +53,7 @@ export type EchartsRadarFormData = QueryFormData &
isCircle: boolean;
numberFormat: string;
dateFormat: string;
isNormalized: boolean;
};

export enum EchartsRadarLabelType {
Expand Down Expand Up @@ -83,3 +84,17 @@ export type RadarChartTransformedProps =
BaseTransformedProps<EchartsRadarFormData> &
ContextMenuTransformedProps &
CrossFilterTransformedProps;

/**
* Represents a mapping from a normalized value (as string) to an original numeric value.
*/
interface NormalizedValueMap {
[normalized: string]: number;
}

/**
* Represents a collection of series, each containing its own NormalizedValueMap.
*/
export interface SeriesNormalizedMap {
[seriesName: string]: NormalizedValueMap;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interfaces of Series -> Normalised -> denormalised values

92 changes: 92 additions & 0 deletions superset-frontend/plugins/plugin-chart-echarts/src/Radar/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* 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.
*/
/*
function for finding the max metric values among all series data for Radar Chart
*/
export const findGlobalMax = (
data: Record<string, unknown>[],
metrics: string[],
): number => {
if (!data?.length || !metrics?.length) return 0;

return data.reduce((globalMax, row) => {
const rowMax = metrics.reduce((max, metric) => {
const value = row[metric];
return typeof value === 'number' &&
Number.isFinite(value) &&
!Number.isNaN(value)
? Math.max(max, value)
: max;
}, 0);

return Math.max(globalMax, rowMax);
}, 0);
};

interface TooltipParams {
color: string;
name?: string;
value: number[];
}

interface TooltipMetricValue {
metric: string;
value: number;
}

export const renderNormalizedTooltip = (
params: TooltipParams,
metrics: string[],
getDenormalizedValue: (seriesName: string, value: string) => number,
metricsWithCustomBounds: Set<string>,
): string => {
const { color, name = '', value: values } = params;
const seriesName = name || 'series0';

const colorDot = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:5px;height:5px;background-color:${color}"></span>`;

// Get metric values with denormalization if needed
const metricValues: TooltipMetricValue[] = metrics.map((metric, index) => {
const value = values[index];
const originalValue = metricsWithCustomBounds.has(metric)
? value
: getDenormalizedValue(name, String(value));

return {
metric,
value: originalValue,
};
});

const tooltipRows = metricValues
.map(
({ metric, value }) => `
<div style="display:flex;">
<div>${colorDot}${metric}:</div>
<div style="font-weight:bold;margin-left:auto;">${value}</div>
</div>
`,
)
.join('');

return `
<div style="font-weight:bold;margin-bottom:5px;">${seriesName}</div>
${tooltipRows}
`;
};
Loading
Loading