From 85319109a5a40de5b61978f5d1fd78c798b74137 Mon Sep 17 00:00:00 2001 From: Jesse Yang Date: Fri, 30 Apr 2021 17:15:18 -0700 Subject: [PATCH] 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 --- .../src/Graph/constants.ts | 14 +--- .../src/Graph/controlPanel.tsx | 46 ++++++++++++ .../src/Graph/transformProps.ts | 74 ++++++++++++++++--- .../plugin-chart-echarts/src/Graph/types.ts | 8 ++ .../test/Graph/transformProps.test.ts | 70 ++++++++++++------ 5 files changed, 165 insertions(+), 47 deletions(-) diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/constants.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/constants.ts index 3c0c154e8b779..f7dea8013b35a 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/constants.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/constants.ts @@ -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, -}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx index 42ef0ebc15778..8151555279791 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx @@ -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', diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/transformProps.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/transformProps.ts index a6bfe1bbf8bf1..bfcf04b91a11c 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/transformProps.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/transformProps.ts @@ -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; + emphasis: Exclude; + select: Exclude; +}; + +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 = 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, diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/types.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/types.ts index a95d75209e5d2..76be9bb1a4e24 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/types.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Graph/types.ts @@ -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, diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts index 96c9387da6a17..7bf475aadd68e 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts @@ -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', + label: { show: true }, name: 'source_value_2', - value: 5, + select: { + itemStyle: { borderWidth: 3, opacity: 1 }, + label: { fontWeight: 'bolder' }, + }, symbolSize: 10, - category: undefined, - select: DEFAULT_GRAPH_SERIES_OPTION.select, - tooltip: DEFAULT_GRAPH_SERIES_OPTION.tooltip, - label: { show: true }, + tooltip: { formatter: '{b}: {c}' }, + value: 5, }, { + category: undefined, id: '3', + label: { show: true }, name: 'target_value_2', - value: 5, + select: { + itemStyle: { borderWidth: 3, opacity: 1 }, + label: { fontWeight: 'bolder' }, + }, symbolSize: 10, - category: undefined, - select: DEFAULT_GRAPH_SERIES_OPTION.select, - tooltip: DEFAULT_GRAPH_SERIES_OPTION.tooltip, - label: { show: true }, + 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, + }, ], }), ]),