Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions docs/developer_docs/extensions/components/alert.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ function MyExtension() {

## Source Links

- [Story file](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/Alert.stories.tsx)
- [Component source](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/index.tsx)
- [Story file](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/components/Alert/Alert.stories.tsx)
- [Component source](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/components/Alert/index.tsx)

---

Expand Down
4 changes: 2 additions & 2 deletions docs/developer_docs/extensions/components/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export function MyExtensionPanel() {

Components in `@apache-superset/core/components` are automatically documented here. To add a new extension component:

1. Add the component to `superset-frontend/packages/superset-core/src/ui/components/`
2. Export it from `superset-frontend/packages/superset-core/src/ui/components/index.ts`
1. Add the component to `superset-frontend/packages/superset-core/src/components/`
2. Export it from `superset-frontend/packages/superset-core/src/components/index.ts`
3. Create a Storybook story with an `Interactive` export:

```tsx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export default function extractQueryFields(
metric: 'metrics',
metric_2: 'metrics',
secondary_metric: 'metrics',
left_metric: 'metrics',
right_metric: 'metrics',
x: 'metrics',
y: 'metrics',
size: 'metrics',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ describe('extractQueryFields', () => {
).toEqual(['metric_1', 'metric_2', 'my_custom_metric']);
});

test('should extract butterfly chart metrics', () => {
expect(
extractQueryFields({
groupby: ['category'],
left_metric: 'left_sum',
right_metric: 'right_sum',
}).metrics,
).toEqual(['left_sum', 'right_sum']);
});

test('should extract columns', () => {
expect(extractQueryFields({ columns: 'col_1' })).toEqual({
columns: ['col_1'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,44 @@
* specific language governing permissions and limitations
* under the License.
*/
import { allEventHandlers, type Event } from '../utils/eventHandlers';
import Echart from '../components/Echart';
import { ButterflyTransformedProps } from './types';
import { EventHandlers } from '../types';
import { ButterflyTransformedProps } from './types';

type ButterflyChartEvent = {
name?: string;
data?: { name?: string };
event?: Event['event'];
};

function getCategoryKey(params: ButterflyChartEvent): string {
return params.data?.name ?? params.name ?? '';
}

export default function Butterfly(props: ButterflyTransformedProps) {
const { height, width, echartOptions, refs, onLegendStateChanged, formData } =
props;
const {
height,
width,
echartOptions,
selectedValues,
refs,
onLegendStateChanged,
formData,
} = props;

const { click, contextmenu } = allEventHandlers(props);

const eventHandlers: EventHandlers = {
click: (params: ButterflyChartEvent) => {
click({ name: getCategoryKey(params) });
},
contextmenu: (params: ButterflyChartEvent) => {
contextmenu({
...params,
name: getCategoryKey(params),
});
},
legendselectchanged: payload => {
onLegendStateChanged?.(payload.selected);
},
Expand All @@ -43,6 +72,7 @@ export default function Butterfly(props: ButterflyTransformedProps) {
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
selectedValues={selectedValues}
vizType={formData.vizType}
/>
);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import { Behavior, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import { EchartsButterflyChartProps, EchartsButterflyFormData } from './types';
import example from './images/example.png';
import exampleDark from './images/example-dark.png';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';

export default class EchartsButterflyChartPlugin extends ChartPlugin<
EchartsButterflyFormData,
Expand All @@ -33,20 +37,27 @@ export default class EchartsButterflyChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./Butterfly'),
metadata: new ChartMetadata({
behaviors: [
Behavior.InteractiveChart,
Behavior.DrillToDetail,
Behavior.DrillBy,
],
credits: ['https://echarts.apache.org'],
category: t('Comparison'),
description: t(
'A butterfly chart compares two metrics across categories using horizontal bars ' +
'that extend left and right from a central axis.',
),
exampleGallery: [{ url: example, urlDark: exampleDark }],
name: t('Butterfly Chart'),
tags: [
t('Categorical'),
t('Comparison'),
t('ECharts'),
t('Multi-Variables'),
],
thumbnail: '',
thumbnail,
thumbnailDark,
}),
transformProps,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ import { DEFAULT_FORM_DATA } from './constants';
import { defaultGrid } from '../defaults';
import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types';
import { NULL_STRING } from '../constants';
import { getChartPadding, getLegendProps } from '../utils/series';
import { OpacityEnum } from '../constants';
import {
getChartPadding,
getLegendProps,
getColtypesMapping,
extractGroupbyLabel,
} from '../utils/series';
import { resolveLegendLayout } from '../utils/legendLayout';
import { convertInteger } from '../utils/convertInteger';

Expand All @@ -44,19 +49,11 @@ type EChartsOption = ComposeOption<BarSeriesOption>;
const LABEL_LEFT = { position: 'left' as const };
const LABEL_RIGHT = { position: 'right' as const };

function formatCategory(value: unknown): string {
if (value == null) {
return NULL_STRING;
}
if (typeof value === 'string' || typeof value === 'number') {
return String(value);
}
return String(value);
}

function formatTooltip(
params: CallbackDataParams[],
formatter: NumberFormatter | CurrencyFormatter,
categoryLabels: string[],
categoryByKey: Map<string, string>,
) {
const axisParams = params.filter(
param => param.seriesName && typeof param.value === 'number',
Expand All @@ -65,7 +62,13 @@ function formatTooltip(
return '';
}

const title = axisParams[0].name;
const { dataIndex, name } = axisParams[0];
const title =
(typeof dataIndex === 'number'
? categoryLabels.at(dataIndex)
: undefined) ??
(typeof name === 'string' ? categoryByKey.get(name) : undefined) ??
name;
const rows = axisParams.map(param => [
param.seriesName!,
formatter(Math.abs(param.value as number)),
Expand All @@ -86,6 +89,8 @@ export default function transformProps(
hooks,
theme,
inContextMenu,
filterState,
emitCrossFilters,
} = chartProps;
const refs: Refs = {};
const { data = [] } = queriesData[0];
Expand Down Expand Up @@ -117,32 +122,75 @@ export default function transformProps(
...formData,
};

const groupbyColumn = ensureIsArray(groupby)[0];
const categoryLabel = getColumnLabel(groupbyColumn);
const leftMetricLabel = leftMetric ? getMetricLabel(leftMetric) : '';
const rightMetricLabel = rightMetric ? getMetricLabel(rightMetric) : '';
const leftSeriesName = leftLabel || leftMetricLabel;
const rightSeriesName = rightLabel || rightMetricLabel;

const coltypeMapping = getColtypesMapping(queriesData[0]);
const groupbyColumns = ensureIsArray(groupby);
const groupbyLabels = groupbyColumns.map(getColumnLabel);

const defaultFormatter = currencyFormat?.symbol
? new CurrencyFormatter({ d3Format: xAxisFormat, currency: currencyFormat })
: getNumberFormatter(xAxisFormat);

const categories = data.map(row => formatCategory(row[categoryLabel]));
const leftData = data.map(row => {
const value = Number(row[leftMetricLabel] ?? 0);
return {
value: -Math.abs(value),
label: LABEL_LEFT,
};
});
const rightData = data.map(row => {
const value = Number(row[rightMetricLabel] ?? 0);
return {
value: Math.abs(value),
label: LABEL_RIGHT,
};
const categories = data.map(datum =>
extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping }),
);
const categoryKeys = data.map((datum, index) => {
const label = categories.at(index) ?? '';
return `${label}__${JSON.stringify(
groupbyLabels.map(col =>
Object.hasOwn(datum, col) ? datum[col] : undefined,
),
)}`;
});
const categoryByKey = new Map(
categoryKeys.flatMap((key, index) => {
const label = categories.at(index);
return label === undefined ? [] : [[key, label] as const];
}),
);

const labelMap = data.reduce<Record<string, string[]>>(
(acc, datum, index) => {
const uniqueKey = categoryKeys.at(index);
if (uniqueKey === undefined) {
return acc;
}
acc[uniqueKey] = groupbyLabels.map(col =>
Object.hasOwn(datum, col) ? (datum[col] as string) : '',
);
return acc;
},
{},
);
const selectedValues = (filterState.selectedValues || []).reduce(
(acc: Record<number, string>, value: string) => {
const index = categoryKeys.indexOf(value);
return index >= 0 ? { ...acc, [index]: value } : acc;
},
{},
);
const getOpacity = (categoryKey: string) =>
filterState.selectedValues?.length &&
!filterState.selectedValues.includes(categoryKey)
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent;

const leftData = data.map((row, i) => ({
name: categoryKeys[i],
value: -Math.abs(Number(row[leftMetricLabel] ?? 0)),
label: LABEL_LEFT,
itemStyle: { opacity: getOpacity(categoryKeys[i]) },
}));
const rightData = data.map((row, i) => ({
name: categoryKeys[i],
value: Math.abs(Number(row[rightMetricLabel] ?? 0)),
label: LABEL_RIGHT,
itemStyle: { opacity: getOpacity(categoryKeys[i]) },
}));

const labelFormatter = (params: CallbackDataParams) => {
const value = Math.abs(params.value as number);
Expand Down Expand Up @@ -280,6 +328,8 @@ export default function transformProps(
formatTooltip(
ensureIsArray(params) as CallbackDataParams[],
defaultFormatter,
categories,
categoryByKey,
),
},
series,
Expand All @@ -294,5 +344,10 @@ export default function transformProps(
setDataMask,
onContextMenu,
onLegendStateChanged,
groupby: groupbyColumns,
labelMap,
selectedValues,
emitCrossFilters,
coltypeMapping,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import {
QueryFormMetric,
RgbaColor,
} from '@superset-ui/core';
import { BaseTransformedProps, LegendFormData, TitleFormData } from '../types';
import {
BaseTransformedProps,
LegendFormData,
TitleFormData,
CrossFilterTransformedProps,
} from '../types';

export type EchartsButterflyFormData = QueryFormData &
LegendFormData &
Expand All @@ -49,4 +54,4 @@ export interface EchartsButterflyChartProps extends ChartProps {
}

export type ButterflyTransformedProps =
BaseTransformedProps<EchartsButterflyFormData>;
BaseTransformedProps<EchartsButterflyFormData> & CrossFilterTransformedProps;
Loading
Loading