feat: Adds the ECharts Heatmap chart (#25353)

This commit is contained in:
Michael S. Molina 2024-03-28 16:16:17 -03:00 committed by GitHub
parent d69a1870a0
commit 546d48adbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 929 additions and 24 deletions

View File

@ -28,4 +28,5 @@ export { contributionOperator } from './contributionOperator';
export { prophetOperator } from './prophetOperator';
export { boxplotOperator } from './boxplotOperator';
export { flattenOperator } from './flattenOperator';
export { rankOperator } from './rankOperator';
export * from './utils';

View File

@ -0,0 +1,30 @@
/**
* 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 limitationsxw
* under the License.
*/
import { PostProcessingRank } from '@superset-ui/core';
import { PostProcessingFactory } from './types';
/* eslint-disable @typescript-eslint/no-unused-vars */
export const rankOperator: PostProcessingFactory<PostProcessingRank> = (
formData,
queryObject,
options,
) => ({
operation: 'rank',
options,
});

View File

@ -19,5 +19,5 @@
import { QueryFormData, QueryObject } from '@superset-ui/core';
export interface PostProcessingFactory<T> {
(formData: QueryFormData, queryObject: QueryObject): T;
(formData: QueryFormData, queryObject: QueryObject, options?: any): T;
}

View File

@ -0,0 +1,47 @@
/**
* 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 { QueryObject, SqlaFormData } from '@superset-ui/core';
import { rankOperator } from '@superset-ui/chart-controls';
const formData: SqlaFormData = {
x_axis: 'dttm',
metrics: ['sales'],
groupby: ['department'],
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: 'table',
truncate_metric: true,
};
const queryObject: QueryObject = {
is_timeseries: true,
metrics: ['sales'],
columns: ['department'],
time_range: '2015 : 2016',
granularity: 'month',
post_processing: [],
};
test('should add rankOperator', () => {
const options = { metric: 'sales', group_by: 'department' };
expect(rankOperator(formData, queryObject, options)).toEqual({
operation: 'rank',
options,
});
});

View File

@ -224,6 +224,15 @@ export type PostProcessingFlatten =
| _PostProcessingFlatten
| DefaultPostProcessing;
interface _PostProcessingRank {
operation: 'rank';
options?: {
metric: string;
group_by: string | null;
};
}
export type PostProcessingRank = _PostProcessingRank | DefaultPostProcessing;
/**
* Parameters for chart data postprocessing.
* See superset/utils/pandas_processing.py.
@ -241,7 +250,8 @@ export type PostProcessingRule =
| PostProcessingSort
| PostProcessingResample
| PostProcessingRename
| PostProcessingFlatten;
| PostProcessingFlatten
| PostProcessingRank;
export function isPostProcessingAggregation(
rule?: PostProcessingRule,

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import { t, ChartMetadata, ChartPlugin, ChartLabel } from '@superset-ui/core';
import transformProps from './transformProps';
import transportation from './images/transportation.jpg';
import channels from './images/channels.jpg';
@ -35,7 +35,8 @@ const metadata = new ChartMetadata({
{ url: channels, caption: t('Relationships between community channels') },
{ url: employment, caption: t('Employment and education') },
],
name: t('Heatmap'),
label: ChartLabel.DEPRECATED,
name: t('Heatmap (legacy)'),
tags: [
t('Business'),
t('Intensity'),
@ -43,11 +44,15 @@ const metadata = new ChartMetadata({
t('Density'),
t('Predictive'),
t('Single Metric'),
t('Deprecated'),
],
thumbnail,
useLegacyApi: true,
});
/**
* @deprecated in version 4.0.
*/
export default class HeatmapChartPlugin extends ChartPlugin {
constructor() {
super({

View File

@ -2,6 +2,20 @@
"name": "@superset-ui/plugin-chart-echarts",
"version": "0.18.25",
"description": "Superset Chart - Echarts",
"keywords": [
"superset"
],
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-echarts#readme",
"bugs": {
"url": "https://github.com/apache/superset/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/apache/superset.git",
"directory": "superset-frontend/plugins/plugin-chart-echarts"
},
"license": "Apache-2.0",
"author": "Superset",
"sideEffects": false,
"main": "lib/index.js",
"module": "esm/index.js",
@ -9,23 +23,6 @@
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "https://github.com/apache/superset.git",
"directory": "superset-frontend/plugins/plugin-chart-echarts"
},
"keywords": [
"superset"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache/superset/issues"
},
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-echarts#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"d3-array": "^1.2.0",
"echarts": "^5.4.1",
@ -35,6 +32,10 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"memoize-one": "*",
"react": "^16.13.1"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,33 @@
/**
* 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 { HeatmapTransformedProps } from './types';
import Echart from '../components/Echart';
export default function Heatmap(props: HeatmapTransformedProps) {
const { height, width, echartOptions, refs } = props;
return (
<Echart
refs={refs}
height={height}
width={width}
echartOptions={echartOptions}
/>
);
}

View File

@ -0,0 +1,68 @@
/**
* 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 {
QueryFormColumn,
QueryFormOrderBy,
buildQueryContext,
ensureIsArray,
getColumnLabel,
getMetricLabel,
getXAxisColumn,
} from '@superset-ui/core';
import { rankOperator } from '@superset-ui/chart-controls';
import { HeatmapFormData } from './types';
export default function buildQuery(formData: HeatmapFormData) {
const { groupby, normalize_across, sort_x_axis, sort_y_axis, x_axis } =
formData;
const metric = getMetricLabel(formData.metric);
const columns = [
...ensureIsArray(getXAxisColumn(formData)),
...ensureIsArray(groupby),
];
const orderby: QueryFormOrderBy[] = [
[
sort_x_axis.includes('value') ? metric : columns[0],
sort_x_axis.includes('asc'),
],
[
sort_y_axis.includes('value') ? metric : columns[1],
sort_y_axis.includes('asc'),
],
];
const group_by =
normalize_across === 'x'
? getColumnLabel(x_axis)
: normalize_across === 'y'
? getColumnLabel(groupby as unknown as QueryFormColumn)
: undefined;
return buildQueryContext(formData, baseQueryObject => [
{
...baseQueryObject,
columns,
orderby,
post_processing: [
rankOperator(formData, baseQueryObject, {
metric,
group_by,
}),
],
},
]);
}

View File

@ -0,0 +1,304 @@
/**
* 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 } from '@superset-ui/core';
import {
ControlPanelConfig,
formatSelectOptionsForRange,
getStandardizedControls,
} from '@superset-ui/chart-controls';
const sortAxisChoices = [
['alpha_asc', t('Axis ascending')],
['alpha_desc', t('Axis descending')],
['value_asc', t('Metric ascending')],
['value_desc', t('Metric descending')],
];
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['x_axis'],
['time_grain_sqla'],
['groupby'],
['metric'],
['adhoc_filters'],
['row_limit'],
[
{
name: 'sort_x_axis',
config: {
type: 'SelectControl',
label: t('Sort X Axis'),
choices: sortAxisChoices,
renderTrigger: false,
clearable: false,
default: 'alpha_asc',
},
},
],
[
{
name: 'sort_y_axis',
config: {
type: 'SelectControl',
label: t('Sort Y Axis'),
choices: sortAxisChoices,
renderTrigger: false,
clearable: false,
default: 'alpha_asc',
},
},
],
[
{
name: 'normalize_across',
config: {
type: 'SelectControl',
label: t('Normalize Across'),
choices: [
['heatmap', t('heatmap')],
['x', t('x')],
['y', t('y')],
],
default: 'heatmap',
renderTrigger: false,
description: (
<>
<div>
{t(
'Color will be shaded based the normalized (0% to 100%) value of a given cell against the other cells in the selected range: ',
)}
</div>
<ul>
<li>{t('x: values are normalized within each column')}</li>
<li>{t('y: values are normalized within each row')}</li>
<li>
{t(
'heatmap: values are normalized across the entire heatmap',
)}
</li>
</ul>
</>
),
},
},
],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
[
{
name: 'legend_type',
config: {
type: 'SelectControl',
label: t('Legend Type'),
renderTrigger: true,
choices: [
['continuous', t('Continuous')],
['piecewise', t('Piecewise')],
],
default: 'continuous',
clearable: false,
},
},
],
['linear_color_scheme'],
[
{
name: 'xscale_interval',
config: {
type: 'SelectControl',
label: t('XScale Interval'),
renderTrigger: true,
choices: [[-1, t('Auto')]].concat(
formatSelectOptionsForRange(1, 50),
),
default: -1,
clearable: false,
description: t(
'Number of steps to take between ticks when displaying the X scale',
),
},
},
],
[
{
name: 'yscale_interval',
config: {
type: 'SelectControl',
label: t('YScale Interval'),
choices: [[-1, t('Auto')]].concat(
formatSelectOptionsForRange(1, 50),
),
default: -1,
clearable: false,
renderTrigger: true,
description: t(
'Number of steps to take between ticks when displaying the Y scale',
),
},
},
],
[
{
name: 'left_margin',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Left Margin'),
choices: [
['auto', t('Auto')],
[50, '50'],
[75, '75'],
[100, '100'],
[125, '125'],
[150, '150'],
[200, '200'],
],
default: 'auto',
renderTrigger: true,
description: t(
'Left margin, in pixels, allowing for more room for axis labels',
),
},
},
],
[
{
name: 'bottom_margin',
config: {
type: 'SelectControl',
clearable: false,
freeForm: true,
label: t('Bottom Margin'),
choices: [
['auto', t('Auto')],
[50, '50'],
[75, '75'],
[100, '100'],
[125, '125'],
[150, '150'],
[200, '200'],
],
default: 'auto',
renderTrigger: true,
description: t(
'Bottom margin, in pixels, allowing for more room for axis labels',
),
},
},
],
[
{
name: 'value_bounds',
config: {
type: 'BoundsControl',
label: t('Value bounds'),
renderTrigger: true,
default: [null, null],
description: t('Hard value bounds applied for color coding.'),
},
},
],
['y_axis_format'],
['x_axis_time_format'],
['currency_format'],
[
{
name: 'show_legend',
config: {
type: 'CheckboxControl',
label: t('Legend'),
renderTrigger: true,
default: true,
description: t('Whether to display the legend (toggles)'),
},
},
],
[
{
name: 'show_percentage',
config: {
type: 'CheckboxControl',
label: t('Show percentage'),
renderTrigger: true,
description: t(
'Whether to include the percentage in the tooltip',
),
default: true,
},
},
],
[
{
name: 'show_values',
config: {
type: 'CheckboxControl',
label: t('Show Values'),
renderTrigger: true,
default: false,
description: t(
'Whether to display the numerical values within the cells',
),
},
},
],
[
{
name: 'normalized',
config: {
type: 'CheckboxControl',
label: t('Normalized'),
renderTrigger: true,
description: t(
'Whether to apply a normal distribution based on rank on the color scale',
),
default: false,
},
},
],
],
},
],
controlOverrides: {
groupby: {
label: t('Y-Axis'),
description: t('Dimension to use on y-axis.'),
multi: false,
validators: [validateNonEmpty],
},
y_axis_format: {
label: t('Value Format'),
},
},
formDataOverrides: formData => ({
...formData,
metric: getStandardizedControls().shiftMetric(),
}),
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -0,0 +1,55 @@
/**
* 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 transformProps from './transformProps';
import buildQuery from './buildQuery';
import example1 from './images/example1.png';
import example2 from './images/example2.png';
import example3 from './images/example3.png';
import thumbnail from './images/thumbnail.png';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Correlation'),
description: t(
'Visualize a related metric across pairs of groups. Heatmaps excel at showcasing the correlation or strength between two groups. Color is used to emphasize the strength of the link between each pair of groups.',
),
exampleGallery: [{ url: example1 }, { url: example2 }, { url: example3 }],
name: t('Heatmap'),
tags: [
t('Business'),
t('Intensity'),
t('Density'),
t('Single Metric'),
t('ECharts'),
],
thumbnail,
});
export default class EchartsHeatmapChartPlugin extends ChartPlugin {
constructor() {
super({
buildQuery,
loadChart: () => import('./Heatmap'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@ -0,0 +1,243 @@
/**
* 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 {
GenericDataType,
QueryFormColumn,
getColumnLabel,
getMetricLabel,
getSequentialSchemeRegistry,
getTimeFormatter,
getValueFormatter,
} from '@superset-ui/core';
import memoizeOne from 'memoize-one';
import { maxBy, minBy } from 'lodash';
import { EChartsOption, HeatmapSeriesOption } from 'echarts';
import { CallbackDataParams } from 'echarts/types/src/util/types';
import { HeatmapChartProps, HeatmapTransformedProps } from './types';
import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types';
import { parseAxisBound } from '../utils/controls';
import { NULL_STRING } from '../constants';
// Calculated totals per x and y categories plus total
const calculateTotals = memoizeOne(
(
data: Record<string, any>[],
xAxis: string,
groupby: string,
metric: string,
) =>
data.reduce(
(acc, row) => {
const value = row[metric];
if (typeof value !== 'number') {
return acc;
}
const x = row[xAxis] as string;
const y = row[groupby] as string;
const xTotal = acc.x[x] || 0;
const yTotal = acc.y[y] || 0;
return {
x: { ...acc.x, [x]: xTotal + value },
y: { ...acc.y, [y]: yTotal + value },
total: acc.total + value,
};
},
{ x: {}, y: {}, total: 0 },
),
);
export default function transformProps(
chartProps: HeatmapChartProps,
): HeatmapTransformedProps {
const refs: Refs = {};
const { width, height, formData, queriesData, datasource } = chartProps;
const {
bottomMargin,
xAxis,
groupby,
linearColorScheme,
leftMargin,
legendType = 'continuous',
metric,
normalizeAcross,
normalized,
showLegend,
showPercentage,
showValues,
xscaleInterval,
yscaleInterval,
valueBounds,
yAxisFormat,
xAxisTimeFormat,
currencyFormat,
} = formData;
const metricLabel = getMetricLabel(metric);
const xAxisLabel = getColumnLabel(xAxis);
// groupby is overridden to be a single value
const yAxisLabel = getColumnLabel(groupby as unknown as QueryFormColumn);
const { data, colnames, coltypes } = queriesData[0];
const { columnFormats = {}, currencyFormats = {} } = datasource;
const colorColumn = normalized ? 'rank' : metricLabel;
const colors = getSequentialSchemeRegistry().get(linearColorScheme)?.colors;
const getAxisFormatter =
(colType: GenericDataType) => (value: number | string) => {
if (colType === GenericDataType.Temporal) {
if (typeof value === 'string') {
return getTimeFormatter(xAxisTimeFormat)(Number.parseInt(value, 10));
}
return getTimeFormatter(xAxisTimeFormat)(value);
}
return String(value);
};
const xAxisFormatter = getAxisFormatter(coltypes[0]);
const yAxisFormatter = getAxisFormatter(coltypes[1]);
const valueFormatter = getValueFormatter(
metric,
currencyFormats,
columnFormats,
yAxisFormat,
currencyFormat,
);
let [min, max] = (valueBounds || []).map(parseAxisBound);
if (min === undefined) {
min = minBy(data, row => row[colorColumn])?.[colorColumn] as number;
}
if (max === undefined) {
max = maxBy(data, row => row[colorColumn])?.[colorColumn] as number;
}
const series: HeatmapSeriesOption[] = [
{
name: metricLabel,
type: 'heatmap',
data: data.map(row =>
colnames.map(col => {
const value = row[col];
if (!value) {
return NULL_STRING;
}
if (typeof value === 'boolean') {
return String(value);
}
return value;
}),
),
label: {
show: showValues,
formatter: (params: CallbackDataParams) =>
valueFormatter(params.value[2]),
},
},
];
const echartOptions: EChartsOption = {
grid: {
containLabel: true,
bottom: bottomMargin,
left: leftMargin,
},
series,
tooltip: {
...getDefaultTooltip(refs),
formatter: (params: CallbackDataParams) => {
const totals = calculateTotals(
data,
xAxisLabel,
yAxisLabel,
metricLabel,
);
const x = params.value[0];
const y = params.value[1];
const value = params.value[2];
const formattedX = xAxisFormatter(x);
const formattedY = yAxisFormatter(y);
const formattedValue = valueFormatter(value);
let percentage = 0;
let suffix = 'heatmap';
if (typeof value === 'number') {
if (normalizeAcross === 'x') {
percentage = (value / totals.x[x]) * 100;
suffix = formattedX;
} else if (normalizeAcross === 'y') {
percentage = (value / totals.y[y]) * 100;
suffix = formattedY;
} else {
percentage = (value / totals.total) * 100;
suffix = 'heatmap';
}
}
return `
<div>
<div>${colnames[0]}: <b>${formattedX}</b></div>
<div>${colnames[1]}: <b>${formattedY}</b></div>
<div>${colnames[2]}: <b>${formattedValue}</b></div>
${
showPercentage
? `<div>% (${suffix}): <b>${valueFormatter(
percentage,
)}%</b></div>`
: ''
}
</div>`;
},
},
visualMap: {
type: legendType,
min,
max,
calculable: true,
orient: 'horizontal',
right: 0,
top: 0,
itemHeight: legendType === 'continuous' ? 300 : 14,
itemWidth: 15,
formatter: min => valueFormatter(min as number),
inRange: {
color: colors,
},
show: showLegend,
// By default, ECharts uses the last dimension which is rank
dimension: normalized ? 3 : 2,
},
xAxis: {
type: 'category',
axisLabel: {
formatter: xAxisFormatter,
interval: xscaleInterval === -1 ? 'auto' : xscaleInterval - 1,
},
},
yAxis: {
type: 'category',
axisLabel: {
formatter: yAxisFormatter,
interval: yscaleInterval === -1 ? 'auto' : yscaleInterval - 1,
},
},
};
return {
refs,
echartOptions,
width,
height,
formData,
};
}

View File

@ -0,0 +1,53 @@
/**
* 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 {
Currency,
QueryFormColumn,
QueryFormData,
QueryFormMetric,
} from '@superset-ui/core';
import { BaseChartProps, BaseTransformedProps } from '../types';
export interface HeatmapFormData extends QueryFormData {
bottomMargin: string;
currencyFormat?: Currency;
leftMargin: string;
legendType: 'continuous' | 'piecewise';
linearColorScheme?: string;
metric: QueryFormMetric;
normalizeAcross: 'heatmap' | 'x' | 'y';
normalized?: boolean;
showLegend?: boolean;
showPercentage?: boolean;
showValues?: boolean;
sortXAxis: string;
sortYAxis: string;
timeFormat?: string;
xAxis: QueryFormColumn;
xscaleInterval: number;
valueBounds: [number | undefined | null, number | undefined | null];
yAxisFormat?: string;
yscaleInterval: number;
}
export interface HeatmapChartProps extends BaseChartProps<HeatmapFormData> {
formData: HeatmapFormData;
}
export type HeatmapTransformedProps = BaseTransformedProps<HeatmapFormData>;

View File

@ -31,6 +31,7 @@ export { default as EchartsGaugeChartPlugin } from './Gauge';
export { default as EchartsRadarChartPlugin } from './Radar';
export { default as EchartsFunnelChartPlugin } from './Funnel';
export { default as EchartsTreeChartPlugin } from './Tree';
export { default as EchartsHeatmapChartPlugin } from './Heatmap';
export { default as EchartsTreemapChartPlugin } from './Treemap';
export {
BigNumberChartPlugin,
@ -51,6 +52,7 @@ export { default as RadarTransformProps } from './Radar/transformProps';
export { default as TimeseriesTransformProps } from './Timeseries/transformProps';
export { default as TreeTransformProps } from './Tree/transformProps';
export { default as TreemapTransformProps } from './Treemap/transformProps';
export { default as HeatmapTransformProps } from './Heatmap/transformProps';
export { default as SunburstTransformProps } from './Sunburst/transformProps';
export { default as BubbleTransformProps } from './Bubble/transformProps';
export { default as WaterfallTransformProps } from './Waterfall/transformProps';

View File

@ -19,7 +19,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { InputNumber } from 'src/components/Input';
import { t, styled } from '@superset-ui/core';
import { debounce } from 'lodash';
import { debounce, parseInt } from 'lodash';
import ControlHeader from 'src/explore/components/ControlHeader';
type ValueType = (number | null)[];
@ -43,8 +43,16 @@ const MaxInput = styled(InputNumber)`
margin-left: ${({ theme }) => theme.gridUnit}px;
`;
const parseNumber = (value: undefined | number | string | null) =>
value === null || Number.isNaN(Number(value)) ? null : Number(value);
const parseNumber = (value: undefined | number | string | null) => {
if (
value === null ||
value === undefined ||
(typeof value === 'string' && Number.isNaN(parseInt(value)))
) {
return null;
}
return Number(value);
};
export default function BoundsControl({
onChange = () => {},

View File

@ -88,6 +88,7 @@ const DEFAULT_ORDER = [
'time_pivot',
'deck_arc',
'heatmap',
'heatmap_v2',
'deck_grid',
'deck_screengrid',
'treemap_v2',

View File

@ -67,6 +67,7 @@ import {
EchartsBubbleChartPlugin,
EchartsWaterfallChartPlugin,
BigNumberPeriodOverPeriodChartPlugin,
EchartsHeatmapChartPlugin,
} from '@superset-ui/plugin-chart-echarts';
import {
SelectFilterPlugin,
@ -158,6 +159,7 @@ export default class MainPreset extends Preset {
new EchartsWaterfallChartPlugin().configure({
key: 'waterfall',
}),
new EchartsHeatmapChartPlugin().configure({ key: 'heatmap_v2' }),
new SelectFilterPlugin().configure({ key: FilterPlugins.Select }),
new RangeFilterPlugin().configure({ key: FilterPlugins.Range }),
new TimeFilterPlugin().configure({ key: FilterPlugins.Time }),

View File

@ -28,6 +28,7 @@ from superset.utils.pandas_postprocessing.geography import (
)
from superset.utils.pandas_postprocessing.pivot import pivot
from superset.utils.pandas_postprocessing.prophet import prophet
from superset.utils.pandas_postprocessing.rank import rank
from superset.utils.pandas_postprocessing.rename import rename
from superset.utils.pandas_postprocessing.resample import resample
from superset.utils.pandas_postprocessing.rolling import rolling
@ -50,6 +51,7 @@ __all__ = [
"geodetic_parse",
"pivot",
"prophet",
"rank",
"rename",
"resample",
"rolling",

View File

@ -0,0 +1,40 @@
# 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.
from __future__ import annotations
import pandas as pd
def rank(
df: pd.DataFrame,
metric: str,
group_by: str | None = None,
) -> pd.DataFrame:
"""
Calculates the rank of a metric within a group.
:param df: N-dimensional DataFrame.
:param metric: The metric to rank.
:param group_by: The column to group by.
:return: a flat DataFrame
"""
if group_by:
gb = df.groupby(group_by, group_keys=False)
df["rank"] = gb.apply(lambda x: x[metric].rank(pct=True))
else:
df["rank"] = df[metric].rank(pct=True)
return df