Visitar URL original
feat(chord): migrate chord chart to v1 chart data API (#41719) · apache/superset@deb6cac · GitHub
Skip to content

Commit deb6cac

Browse files
rusackasclaude
andcommitted
feat(chord): migrate chord chart to v1 chart data API (#41719)
Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent 0f8e2a2 commit deb6cac

6 files changed

Lines changed: 262 additions & 5 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import {
20+
buildQueryContext,
21+
ensureIsArray,
22+
QueryFormColumn,
23+
QueryFormData,
24+
} from '@superset-ui/core';
25+
26+
/**
27+
* Mirrors the legacy ChordViz.query_obj: one query grouped by the source
28+
* (groupby) and target (columns) dimensions, selecting the single metric,
29+
* ordered by that metric when sort_by_metric is on.
30+
*/
31+
export default function buildQuery(formData: QueryFormData) {
32+
const { groupby, columns, metric, sort_by_metric } = formData;
33+
const source = ensureIsArray(groupby as QueryFormColumn | QueryFormColumn[])[0];
34+
const target = ensureIsArray(columns as QueryFormColumn | QueryFormColumn[])[0];
35+
return buildQueryContext(formData, baseQueryObject => [
36+
{
37+
...baseQueryObject,
38+
columns: [source, target].filter(column => column != null),
39+
metrics: metric ? [metric] : [],
40+
orderby: sort_by_metric && metric ? [[metric, false]] : undefined,
41+
},
42+
]);
43+
}

‎superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,16 @@ const metadata = new ChartMetadata({
3939
},
4040
],
4141
name: t('Chord Diagram'),
42-
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
42+
tags: [t('Circular'), t('Proportional'), t('Relational')],
4343
thumbnail,
4444
thumbnailDark,
45-
useLegacyApi: true,
4645
});
4746

4847
export default class ChordChartPlugin extends ChartPlugin {
4948
constructor() {
5049
super({
5150
loadChart: () => import('./ReactChord'),
51+
loadBuildQuery: () => import('./buildQuery'),
5252
metadata,
5353
transformProps,
5454
controlPanel,

‎superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts‎

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,71 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import { ChartProps } from '@superset-ui/core';
19+
import {
20+
ChartProps,
21+
ensureIsArray,
22+
getColumnLabel,
23+
getMetricLabel,
24+
} from '@superset-ui/core';
25+
26+
interface ChordData {
27+
nodes: unknown[];
28+
matrix: unknown[][];
29+
}
30+
31+
/**
32+
* Builds the square adjacency matrix that d3.chord expects, the way the
33+
* legacy explore_json endpoint used to server-side: nodes are the union
34+
* of source and target values, matrix[target][source] holds the
35+
* (directional) metric value.
36+
*/
37+
function buildChordData(
38+
records: Record<string, unknown>[],
39+
sourceLabel: string,
40+
targetLabel: string,
41+
metricLabel: string,
42+
): ChordData {
43+
const nodes = Array.from(
44+
new Set(
45+
records.flatMap(record => [record[sourceLabel], record[targetLabel]]),
46+
),
47+
);
48+
const values = new Map<unknown, Map<unknown, unknown>>();
49+
records.forEach(record => {
50+
const source = record[sourceLabel];
51+
if (!values.has(source)) {
52+
values.set(source, new Map());
53+
}
54+
values.get(source)!.set(record[targetLabel], record[metricLabel]);
55+
});
56+
return {
57+
nodes,
58+
matrix: nodes.map(target =>
59+
nodes.map(source => values.get(source)?.get(target) ?? 0),
60+
),
61+
};
62+
}
2063

2164
export default function transformProps(chartProps: ChartProps) {
2265
const { width, height, formData, queriesData } = chartProps;
23-
const { yAxisFormat, colorScheme, sliceId } = formData;
66+
const { yAxisFormat, colorScheme, sliceId, groupby, columns, metric } =
67+
formData;
68+
69+
const rawData = queriesData[0].data;
70+
const data =
71+
Array.isArray(rawData) && metric
72+
? buildChordData(
73+
rawData,
74+
getColumnLabel(ensureIsArray(groupby)[0]),
75+
getColumnLabel(ensureIsArray(columns)[0]),
76+
getMetricLabel(metric),
77+
)
78+
: // legacy explore_json payloads arrive pre-shaped
79+
rawData;
2480

2581
return {
2682
colorScheme,
27-
data: queriesData[0].data,
83+
data,
2884
height,
2985
numberFormat: yAxisFormat,
3086
width,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { QueryFormData } from '@superset-ui/core';
20+
import buildQuery from '../src/buildQuery';
21+
22+
const formData: QueryFormData = {
23+
datasource: '5__table',
24+
granularity_sqla: 'ds',
25+
time_range: 'No filter',
26+
viz_type: 'chord',
27+
groupby: ['source_col'],
28+
columns: ['target_col'],
29+
metric: 'sum__value',
30+
};
31+
32+
test('groups by source then target and selects the single metric', () => {
33+
const [query] = buildQuery(formData).queries;
34+
expect(query.columns).toEqual(['source_col', 'target_col']);
35+
expect(query.metrics).toEqual(['sum__value']);
36+
expect(query.orderby).toBeUndefined();
37+
});
38+
39+
test('orders by the metric descending when sort_by_metric is on', () => {
40+
const [query] = buildQuery({ ...formData, sort_by_metric: true }).queries;
41+
expect(query.orderby).toEqual([['sum__value', false]]);
42+
});
43+
44+
test('handles bare-string source/target as stored by the single-value controls', () => {
45+
const [query] = buildQuery({
46+
...formData,
47+
groupby: 'source_col',
48+
columns: 'target_col',
49+
} as unknown as QueryFormData).queries;
50+
expect(query.columns).toEqual(['source_col', 'target_col']);
51+
});
52+
53+
test('maps legacy granularity_sqla saved form data', () => {
54+
const [query] = buildQuery(formData).queries;
55+
expect(query.granularity).toEqual('ds');
56+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { supersetTheme } from '@apache-superset/core/theme';
20+
import { ChartProps } from '@superset-ui/core';
21+
import transformProps from '../src/transformProps';
22+
23+
const baseChartProps = {
24+
width: 800,
25+
height: 600,
26+
datasource: {},
27+
theme: supersetTheme,
28+
hooks: {},
29+
};
30+
31+
const formData = {
32+
groupby: 'source_col',
33+
columns: 'target_col',
34+
metric: 'sum__value',
35+
yAxisFormat: '.2f',
36+
colorScheme: 'd3Category10',
37+
sliceId: 1,
38+
};
39+
40+
test('builds the chord node list and matrix from v1 records', () => {
41+
const chartProps = new ChartProps({
42+
...baseChartProps,
43+
formData,
44+
queriesData: [
45+
{
46+
data: [
47+
{ source_col: 'a', target_col: 'b', sum__value: 10 },
48+
{ source_col: 'b', target_col: 'a', sum__value: 5 },
49+
{ source_col: 'a', target_col: 'c', sum__value: 2 },
50+
],
51+
},
52+
],
53+
});
54+
const { data } = transformProps(chartProps) as {
55+
data: { nodes: string[]; matrix: number[][] };
56+
};
57+
expect(data.nodes).toEqual(['a', 'b', 'c']);
58+
// matrix[targetIndex][sourceIndex], matching the legacy backend layout
59+
expect(data.matrix).toEqual([
60+
[0, 5, 0], // into a: from a, b, c
61+
[10, 0, 0], // into b
62+
[2, 0, 0], // into c
63+
]);
64+
});
65+
66+
test('keeps the last value for duplicate source/target pairs', () => {
67+
const chartProps = new ChartProps({
68+
...baseChartProps,
69+
formData,
70+
queriesData: [
71+
{
72+
data: [
73+
{ source_col: 'a', target_col: 'b', sum__value: 1 },
74+
{ source_col: 'a', target_col: 'b', sum__value: 9 },
75+
],
76+
},
77+
],
78+
});
79+
const { data } = transformProps(chartProps) as {
80+
data: { nodes: string[]; matrix: number[][] };
81+
};
82+
expect(data.matrix[1][0]).toEqual(9);
83+
});
84+
85+
test('passes through legacy pre-shaped payloads', () => {
86+
const legacyPayload = { nodes: ['a'], matrix: [[0]] };
87+
const chartProps = new ChartProps({
88+
...baseChartProps,
89+
formData,
90+
queriesData: [{ data: legacyPayload }],
91+
});
92+
const { data } = transformProps(chartProps) as { data: unknown };
93+
expect(data).toEqual(legacyPayload);
94+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"composite": false,
4+
"emitDeclarationOnly": false
5+
},
6+
"extends": "../../../tsconfig.json",
7+
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
8+
}

0 commit comments

Comments
 (0)