mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
feat(plugin-chart-graph): add node/edge size and edge symbol control (#1084)
* feat(plugin-chart-graph): add node/edge size and edge symbol control * Fix test case
This commit is contained in:
parent
6089bcfd89
commit
85319109a5
@ -25,8 +25,6 @@ export const DEFAULT_GRAPH_SERIES_OPTION: GraphSeriesOption = {
|
||||
initLayout: 'circular',
|
||||
layoutAnimation: true,
|
||||
},
|
||||
edgeSymbol: ['circle', 'arrow'],
|
||||
edgeSymbolSize: [10, 10],
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
@ -43,16 +41,13 @@ export const DEFAULT_GRAPH_SERIES_OPTION: GraphSeriesOption = {
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'adjacency',
|
||||
lineStyle: {
|
||||
width: 10,
|
||||
},
|
||||
},
|
||||
animation: true,
|
||||
animationDuration: 500,
|
||||
animationEasing: 'cubicOut',
|
||||
lineStyle: { color: 'source', curveness: 0.1 },
|
||||
select: {
|
||||
itemStyle: { borderWidth: 3 },
|
||||
itemStyle: { borderWidth: 3, opacity: 1 },
|
||||
label: { fontWeight: 'bolder' },
|
||||
},
|
||||
// Ref: https://echarts.apache.org/en/option.html#series-graph.data.tooltip.formatter
|
||||
@ -60,10 +55,3 @@ export const DEFAULT_GRAPH_SERIES_OPTION: GraphSeriesOption = {
|
||||
// - c: data value
|
||||
tooltip: { formatter: '{b}: {c}' },
|
||||
};
|
||||
|
||||
export const NORMALIZATION_LIMITS = {
|
||||
minNodeSize: 10,
|
||||
maxNodeSize: 60,
|
||||
minEdgeWidth: 0.5,
|
||||
maxEdgeWidth: 8,
|
||||
};
|
||||
|
@ -121,6 +121,24 @@ const controlPanel: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'edgeSymbol',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
label: t('Edge symbols'),
|
||||
description: t('Symbol of two ends of edge line'),
|
||||
default: DEFAULT_FORM_DATA.edgeSymbol,
|
||||
choices: [
|
||||
['none,none', t('None -> None')],
|
||||
['none,arrow', t('None -> Arrow')],
|
||||
['circle,arrow', t('Circle -> Arrow')],
|
||||
['circle,circle', t('Circle -> Circle')],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'draggable',
|
||||
@ -184,6 +202,34 @@ const controlPanel: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'baseNodeSize',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Node size'),
|
||||
renderTrigger: true,
|
||||
isFloat: true,
|
||||
default: DEFAULT_FORM_DATA.baseNodeSize,
|
||||
description: t(
|
||||
'Median node size, the largest node will be 4 times larger than the smallest',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'baseEdgeWidth',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Edge width'),
|
||||
renderTrigger: true,
|
||||
isFloat: true,
|
||||
default: DEFAULT_FORM_DATA.baseEdgeWidth,
|
||||
description: t(
|
||||
'Median edge width, the thickest edge will be 4 times thicker than the thinnest.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'edgeLength',
|
||||
|
@ -30,43 +30,88 @@ import {
|
||||
EchartsGraphFormData,
|
||||
EChartGraphNode,
|
||||
DEFAULT_FORM_DATA as DEFAULT_GRAPH_FORM_DATA,
|
||||
EdgeSymbol,
|
||||
} from './types';
|
||||
import { DEFAULT_GRAPH_SERIES_OPTION, NORMALIZATION_LIMITS } from './constants';
|
||||
import { DEFAULT_GRAPH_SERIES_OPTION } from './constants';
|
||||
import { EchartsProps } from '../types';
|
||||
import { getChartPadding, getLegendProps } from '../utils/series';
|
||||
|
||||
type EdgeWithStyles = GraphEdgeItemOption & {
|
||||
lineStyle: Exclude<GraphEdgeItemOption['lineStyle'], undefined>;
|
||||
emphasis: Exclude<GraphEdgeItemOption['emphasis'], undefined>;
|
||||
select: Exclude<GraphEdgeItemOption['select'], undefined>;
|
||||
};
|
||||
|
||||
function verifyEdgeSymbol(symbol: string): EdgeSymbol {
|
||||
if (symbol === 'none' || symbol === 'circle' || symbol === 'arrow') {
|
||||
return symbol;
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
function parseEdgeSymbol(symbols?: string | null): [EdgeSymbol, EdgeSymbol] {
|
||||
const [start, end] = (symbols || '').split(',');
|
||||
return [verifyEdgeSymbol(start), verifyEdgeSymbol(end)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Emphasized edge width with a min and max.
|
||||
*/
|
||||
function getEmphasizedEdgeWidth(width: number) {
|
||||
return Math.max(5, Math.min(width * 2, 20));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize node size, edge width, and apply label visibility thresholds.
|
||||
*/
|
||||
function normalizeStyles(
|
||||
nodes: EChartGraphNode[],
|
||||
links: GraphEdgeItemOption[],
|
||||
links: EdgeWithStyles[],
|
||||
{
|
||||
baseNodeSize,
|
||||
baseEdgeWidth,
|
||||
showSymbolThreshold,
|
||||
}: {
|
||||
baseNodeSize: number;
|
||||
baseEdgeWidth: number;
|
||||
showSymbolThreshold?: number;
|
||||
},
|
||||
) {
|
||||
const minNodeSize = baseNodeSize * 0.5;
|
||||
const maxNodeSize = baseNodeSize * 2;
|
||||
const minEdgeWidth = baseEdgeWidth * 0.5;
|
||||
const maxEdgeWidth = baseEdgeWidth * 2;
|
||||
const [nodeMinValue, nodeMaxValue] = d3Extent(nodes, x => x.value) as [number, number];
|
||||
|
||||
const nodeSpread = nodeMaxValue - nodeMinValue;
|
||||
nodes.forEach(node => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
node.symbolSize =
|
||||
(((node.value - nodeMinValue) / nodeSpread) * NORMALIZATION_LIMITS.maxNodeSize || 0) +
|
||||
NORMALIZATION_LIMITS.minNodeSize;
|
||||
node.symbolSize = (((node.value - nodeMinValue) / nodeSpread) * maxNodeSize || 0) + minNodeSize;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
node.label = {
|
||||
...node.label,
|
||||
show: showSymbolThreshold ? node.value > showSymbolThreshold : true,
|
||||
};
|
||||
});
|
||||
|
||||
const [linkMinValue, linkMaxValue] = d3Extent(links, x => x.value) as [number, number];
|
||||
const linkSpread = linkMaxValue - linkMinValue;
|
||||
links.forEach(link => {
|
||||
const lineWidth =
|
||||
((link.value! - linkMinValue) / linkSpread) * maxEdgeWidth || 0 + minEdgeWidth;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
link.lineStyle!.width =
|
||||
((link.value! - linkMinValue) / linkSpread) * NORMALIZATION_LIMITS.maxEdgeWidth ||
|
||||
0 + NORMALIZATION_LIMITS.minEdgeWidth;
|
||||
link.lineStyle.width = lineWidth;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
link.emphasis.lineStyle = {
|
||||
...link.emphasis.lineStyle,
|
||||
width: getEmphasizedEdgeWidth(lineWidth),
|
||||
};
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
link.select.lineStyle = {
|
||||
...link.select.lineStyle,
|
||||
width: getEmphasizedEdgeWidth(lineWidth * 0.8),
|
||||
opacity: 1,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -122,6 +167,9 @@ export default function transformProps(chartProps: ChartProps): EchartsProps {
|
||||
legendOrientation,
|
||||
legendType,
|
||||
showLegend,
|
||||
baseEdgeWidth,
|
||||
baseNodeSize,
|
||||
edgeSymbol,
|
||||
}: EchartsGraphFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData };
|
||||
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
@ -129,7 +177,7 @@ export default function transformProps(chartProps: ChartProps): EchartsProps {
|
||||
const nodes: { [name: string]: number } = {};
|
||||
const categories: Set<string> = new Set();
|
||||
const echartNodes: EChartGraphNode[] = [];
|
||||
const echartLinks: GraphEdgeItemOption[] = [];
|
||||
const echartLinks: EdgeWithStyles[] = [];
|
||||
|
||||
/**
|
||||
* Get the node id of an existing node,
|
||||
@ -183,10 +231,12 @@ export default function transformProps(chartProps: ChartProps): EchartsProps {
|
||||
target: targetNode.id,
|
||||
value,
|
||||
lineStyle: {},
|
||||
emphasis: {},
|
||||
select: {},
|
||||
});
|
||||
});
|
||||
|
||||
normalizeStyles(echartNodes, echartLinks, { showSymbolThreshold });
|
||||
normalizeStyles(echartNodes, echartLinks, { showSymbolThreshold, baseEdgeWidth, baseNodeSize });
|
||||
|
||||
const categoryList = [...categories];
|
||||
|
||||
@ -202,8 +252,8 @@ export default function transformProps(chartProps: ChartProps): EchartsProps {
|
||||
links: echartLinks,
|
||||
roam,
|
||||
draggable,
|
||||
edgeSymbol: DEFAULT_GRAPH_SERIES_OPTION.edgeSymbol,
|
||||
edgeSymbolSize: DEFAULT_GRAPH_SERIES_OPTION.edgeSymbolSize,
|
||||
edgeSymbol: parseEdgeSymbol(edgeSymbol),
|
||||
edgeSymbolSize: baseEdgeWidth * 2,
|
||||
selectedMode,
|
||||
...getChartPadding(showLegend, legendOrientation, legendMargin),
|
||||
animation: DEFAULT_GRAPH_SERIES_OPTION.animation,
|
||||
|
@ -25,6 +25,8 @@ import {
|
||||
LegendType,
|
||||
} from '../types';
|
||||
|
||||
export type EdgeSymbol = 'none' | 'circle' | 'arrow';
|
||||
|
||||
export type EchartsGraphFormData = EchartsLegendFormData & {
|
||||
source: string;
|
||||
target: string;
|
||||
@ -39,7 +41,10 @@ export type EchartsGraphFormData = EchartsLegendFormData & {
|
||||
showSymbolThreshold: number;
|
||||
repulsion: number;
|
||||
gravity: number;
|
||||
baseNodeSize: number;
|
||||
baseEdgeWidth: number;
|
||||
edgeLength: number;
|
||||
edgeSymbol: string;
|
||||
friction: number;
|
||||
};
|
||||
|
||||
@ -59,7 +64,10 @@ export const DEFAULT_FORM_DATA: EchartsGraphFormData = {
|
||||
showSymbolThreshold: 0,
|
||||
repulsion: 1000,
|
||||
gravity: 0.3,
|
||||
edgeSymbol: 'none,arrow',
|
||||
edgeLength: 400,
|
||||
baseEdgeWidth: 3,
|
||||
baseNodeSize: 20,
|
||||
friction: 0.2,
|
||||
legendOrientation: LegendOrientation.Top,
|
||||
legendType: LegendType.Scroll,
|
||||
|
@ -68,51 +68,77 @@ describe('EchartsGraph tranformProps', () => {
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
{
|
||||
category: undefined,
|
||||
id: '0',
|
||||
label: { show: true },
|
||||
name: 'source_value_1',
|
||||
select: {
|
||||
itemStyle: { borderWidth: 3, opacity: 1 },
|
||||
label: { fontWeight: 'bolder' },
|
||||
},
|
||||
symbolSize: 50,
|
||||
tooltip: { formatter: '{b}: {c}' },
|
||||
value: 6,
|
||||
symbolSize: 70,
|
||||
category: undefined,
|
||||
select: DEFAULT_GRAPH_SERIES_OPTION.select,
|
||||
tooltip: DEFAULT_GRAPH_SERIES_OPTION.tooltip,
|
||||
label: { show: true },
|
||||
},
|
||||
{
|
||||
category: undefined,
|
||||
id: '1',
|
||||
label: { show: true },
|
||||
name: 'target_value_1',
|
||||
select: {
|
||||
itemStyle: { borderWidth: 3, opacity: 1 },
|
||||
label: { fontWeight: 'bolder' },
|
||||
},
|
||||
symbolSize: 50,
|
||||
tooltip: { formatter: '{b}: {c}' },
|
||||
value: 6,
|
||||
symbolSize: 70,
|
||||
category: undefined,
|
||||
select: DEFAULT_GRAPH_SERIES_OPTION.select,
|
||||
tooltip: DEFAULT_GRAPH_SERIES_OPTION.tooltip,
|
||||
label: { show: true },
|
||||
},
|
||||
{
|
||||
category: undefined,
|
||||
id: '2',
|
||||
name: 'source_value_2',
|
||||
value: 5,
|
||||
symbolSize: 10,
|
||||
category: undefined,
|
||||
select: DEFAULT_GRAPH_SERIES_OPTION.select,
|
||||
tooltip: DEFAULT_GRAPH_SERIES_OPTION.tooltip,
|
||||
label: { show: true },
|
||||
name: 'source_value_2',
|
||||
select: {
|
||||
itemStyle: { borderWidth: 3, opacity: 1 },
|
||||
label: { fontWeight: 'bolder' },
|
||||
},
|
||||
symbolSize: 10,
|
||||
tooltip: { formatter: '{b}: {c}' },
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'target_value_2',
|
||||
value: 5,
|
||||
symbolSize: 10,
|
||||
category: undefined,
|
||||
select: DEFAULT_GRAPH_SERIES_OPTION.select,
|
||||
tooltip: DEFAULT_GRAPH_SERIES_OPTION.tooltip,
|
||||
id: '3',
|
||||
label: { show: true },
|
||||
name: 'target_value_2',
|
||||
select: {
|
||||
itemStyle: { borderWidth: 3, opacity: 1 },
|
||||
label: { fontWeight: 'bolder' },
|
||||
},
|
||||
symbolSize: 10,
|
||||
tooltip: { formatter: '{b}: {c}' },
|
||||
value: 5,
|
||||
},
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
links: [
|
||||
{ source: '0', target: '1', value: 6, lineStyle: { width: 8 } },
|
||||
{ source: '2', target: '3', value: 5, lineStyle: { width: 0.5 } },
|
||||
{
|
||||
emphasis: { lineStyle: { width: 12 } },
|
||||
lineStyle: { width: 6 },
|
||||
select: { lineStyle: { opacity: 1, width: 9.600000000000001 } },
|
||||
source: '0',
|
||||
target: '1',
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
emphasis: { lineStyle: { width: 5 } },
|
||||
lineStyle: { width: 1.5 },
|
||||
select: { lineStyle: { opacity: 1, width: 5 } },
|
||||
source: '2',
|
||||
target: '3',
|
||||
value: 5,
|
||||
},
|
||||
],
|
||||
}),
|
||||
]),
|
||||
|
Loading…
Reference in New Issue
Block a user