练习2:Echarts图表SVG渲染
我们可以看到Superset中的Echarts图表都是使用canvas来渲染的。
Superset 并不支持 SVG 渲染选项,我觉得 Superset 没有提供 SVG 渲染的选项是有他的考虑在里面的, 虽然 SVG 使用浏览器内置的缩放功能时不会模糊,但是在数据量较大(经验判断 > 1k)、较多交互时使用 Canvas 更适合。 而 Superset 的目标是面向大数据分析,不支持 SVG 渲染也是可以理解,但在实际情况中有的客户会对图表的清晰度有很高的需求,SVG 渲染也是必须的。
既然 Superset 使用的是 Echarts 来做图标库,那 Echarts 支持的功能,Superset 也是能实现的。下图是我实现的 SVG、Canvas 选择组件以及图形效果:
再上图我们可以看到这个单选组件,可以选择 SVG 还是 Canvas 渲染,并且前端图表也是使用 SVG 渲染出来了。不多说,接下来看代码:
首先从定义控件的代码开始
- constants.ts
- controls.tsx
- controlPanel.tsx
superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
export const DEFAULT_ECHART_OPTS = {
renderer: 'canvas',
};
superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
import { DEFAULT_ECHART_OPTS } from './constants';
export const rendererControl: ControlSetItem = {
name: 'renderer',
config: {
type: 'RadioButtonControl',
renderTrigger: true,
label: t('Chart renderer'),
default: DEFAULT_ECHART_OPTS.renderer,
options: [
['canvas', t('Canvas')],
['svg', t('SVG')],
],
description: t('How charts are rendered.'),
},
};
superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
import { rendererControl } from '../../../controls';
const config: ControlPanelConfig = {
controlPanelSections: [
// 此处代码忽略...
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
// 此处代码忽略...
[rendererControl],
// 此处代码忽略...
]
}
]
}
经过练习1,相信这部分的代码不需要过多讲解了。
我们来回顾一下echart如何使用渲染器的:
import * as echarts from 'echarts';
import * as echarts from 'echarts/core';
// 可以根据需要选用只用到的渲染器
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
echarts.use([SVGRenderer, CanvasRenderer]);
// 使用 Canvas 渲染器(默认)
var chart = echarts.init(containerDom, null, { renderer: 'canvas' });
// 等价于:
var chart = echarts.init(containerDom);
// 使用 SVG 渲染器
var chart = echarts.init(containerDom, null, { renderer: 'svg' });
然后来看看 Superset 是如何创建echart实例的(截取了第129行片段代码):
chartRef.current = init(divRef.current);
知道该怎么做了吧。
- types.ts
- Echart.tsx
- transformProps.ts
- EchartsTimeseries.tsx
superset-frontend/plugins/plugin-chart-echarts/src/types.ts
import type { EChartsInitOpts } from 'echarts/core';
export interface EchartsProps {
height: number;
width: number;
echartInitOpts?: EChartsInitOpts; // 此处添加
echartOptions: EChartsCoreOption;
eventHandlers?: EventHandlers;
zrEventHandlers?: EventHandlers;
selectedValues?: Record<number, string>;
forceClear?: boolean;
refs: Refs;
}
export interface BaseTransformedProps<F> {
echartOptions: EChartsCoreOption;
formData: F;
height: number;
onContextMenu?: (
clientX: number,
clientY: number,
filters?: ContextMenuFilters,
) => void;
setDataMask?: SetDataMaskHook;
onLegendStateChanged?: (state: LegendState) => void;
filterState?: FilterState;
refs: Refs;
width: number;
emitCrossFilters?: boolean;
coltypeMapping?: Record<string, number>;
echartInitOpts?: EChartsInitOpts; // 此处添加
}
superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx
import { CanvasRenderer, SVGRenderer } from 'echarts/renderers';
use([
CanvasRenderer,
SVGRenderer,
// 此处代码忽略...
]);
function Echart(
{
width,
height,
echartInitOpts = {}, // 此处添加
echartOptions,
eventHandlers,
zrEventHandlers,
selectedValues = {},
refs,
}: EchartsProps,
ref: Ref<EchartsHandler>,
) {
// 此处代码忽略...
useEffect(() => {
if (!divRef.current) return;
if (!chartRef.current) {
chartRef.current = init(divRef.current, null, echartInitOpts); //修改此处
}
Object.entries(eventHandlers || {}).forEach(([name, handler]) => {
chartRef.current?.off(name);
chartRef.current?.on(name, handler);
});
Object.entries(zrEventHandlers || {}).forEach(([name, handler]) => {
chartRef.current?.getZr().off(name);
chartRef.current?.getZr().on(name, handler);
});
chartRef.current.setOption(echartOptions, true);
}, [theme, echartInitOpts, echartOptions, eventHandlers, zrEventHandlers]);
// 此处代码忽略...
}
superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
import type { EChartsCoreOption, EChartsInitOpts } from 'echarts/core';
export default function transformProps(
chartProps: EchartsTimeseriesChartProps,
): TimeseriesChartTransformedProps {
// 此处代码忽略...
const {
// 此处代码忽略...
renderer,
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
// 此处代码忽略...
const echartInitOpts: EChartsInitOpts = {
renderer,
};
return {
echartInitOpts, // 此处添加
echartOptions,
emitCrossFilters,
formData,
groupby: groupBy,
height,
labelMap,
selectedValues,
setDataMask,
setControlValue,
width,
legendData,
onContextMenu,
onLegendStateChanged,
onFocusedSeries,
xValueFormatter: tooltipFormatter,
xAxis: {
label: xAxisLabel,
type: xAxisType,
},
refs,
coltypeMapping: dataTypes,
};
}
superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
export default function EchartsTimeseries({
formData,
height,
width,
echartInitOpts, // 此处添加
echartOptions,
groupby,
labelMap,
selectedValues,
setDataMask,
setControlValue,
legendData = [],
onContextMenu,
onLegendStateChanged,
onFocusedSeries,
xValueFormatter,
xAxis,
refs,
emitCrossFilters,
coltypeMapping,
}: TimeseriesChartTransformedProps) {
// 此处代码忽略...
return (
<>
<div ref={extraControlRef}>
<ExtraControls formData={formData} setControlValue={setControlValue} />
</div>
<Echart
ref={echartRef}
refs={refs}
height={height - extraControlHeight}
width={width}
echartInitOpts={echartInitOpts} // 此处添加
echartOptions={echartOptions}
eventHandlers={eventHandlers}
zrEventHandlers={zrEventHandlers}
selectedValues={selectedValues}
/>
</>
);
}
在上面的代码中:
types.ts
:在这里我们更改了EchartsProps
与BaseTransformedProps
类型的定义,使它可以接收一个echartInitOpts
参数。Echart.tsx
:在这个文件中将Echart
组件接收到的Props
解包获取到echartInitOpts
,然后修改了init
方法。transformProps.ts
:这里将formData
解包获取到控制件中的renderer
,然后构建echartInitOpts
返回出去。EchartsTimeseries.tsx
:解包props
获取到echartInitOpts
传递给Echart
组件。
一整个流程下来都是在定义 controlPanel
的控件,然后传递控件值。