開發與維運

Superset BI 數據可視化平臺,可視化組件二次開發之集成 Echarts 圖表

前置基礎


Flask 是什麼?


Flask 是一個使用 Python 編寫的輕量級 Web 應用框架。

  • 官方文檔
  • 中文文檔

NumPy 是什麼?


NumPy是Python的一個用於科學計算的基礎包。它提供了多維數組對象,多種衍生的對象(例如隱藏數組和矩陣)和一個用於數組快速運算的混合的程序,包括數學,邏輯,排序,選擇,I/O,離散傅立葉變換,基礎線性代數,基礎統計操作,隨機模擬等等。

  • 官網

微信圖片_20220611112325.png

Pandas 是什麼?


Pandas是一個強大的分析結構化數據的工具集;它的使用基礎是Numpy(提供高性能的矩陣運算);用於數據挖掘和數據分析,同時也提供數據清洗功能。

  • 官方文檔
  • 中文文檔


微信圖片_20220611112339.png

Echarts 是什麼?


ECharts 是一款開源的、基於 web 的、跨平臺的支持快速創建交互式可視化的框架,它易於使用、擁有豐富的內置交互以及高性能。ECharts 通過一套聲明式的可視設計語言定製內置的圖表類型,並且底層的流式架構和高性能的圖形渲染器極大地提高了 ECharts 的擴展性和性能。

Echarts 官網

Echarts Multiple Y Axis 圖表

官網 multiple-y-axis 圖表 demo

接下來,我們就是要集成它到我們的 Superset


二次開發 Step-By-Step


使用容器環境安裝 Echarts

微信圖片_20220611112400.png微信圖片_20220611112403.png

docker-compose ps
docker-compose exec superset-node bash
cd /app/superset-frontend/
npm i echarts

visualizations 文件夾


它位於項目 superset-frontend/src/visualizations

我們在此處新建如下文件夾 EchartsMultipleYAxis,它包含:


  • images (可視化組件展示用的縮略圖)
    • thumbnail.png
    • thumbnailLarge.png
  • EchartsMultipleYAxisPlugin.jsx(組件核心邏輯)
  • index.js(組件導出)
  • options.js(Echarts Multiple Y Axis 圖表官方默認配置項)
  • transformProps.js(將 Superset 傳過來的屬性過濾一下)


EchartsMultipleYAxisPlugin.jsx為少 這裡簡單的寫了一下(大家可以根據需求自行調整):


.....
function EchartsMultipleYAxisPlugin(elem, props) {
  const {
    width, height,
    data, colorScheme,
  } = props;
  elem.style.width = width;
  elem.style.height = height;
  const echart = echarts.init(elem);
  let colors = CategoricalColorNamespace.getScale(colorScheme).colors;
  defaultOptions.color = colors;
  const legend = [];
  const xAxisData = [];
  let xAxisDataFlag = true;
  let yAxisMaxs = [];
  defaultOptions.yAxis.forEach((y, index) => {
    const sd = data[index];
    const title = sd.key;
    y.name = sd.key;
    y.axisLabel.formatter = `{value}`;
    y.axisLine.lineStyle.color = colors[index];
    const seriesD = [];
    defaultOptions.series[index].data = [];
    sd.values.forEach((value) => {
      xAxisDataFlag && xAxisData.push(value.x);
      defaultOptions.series[index].data.push((value.y).toFixed(2));
    })
    defaultOptions.series[index].name = y.name
    const yAxisMax = Math.max.apply(null, defaultOptions.series[index].data);
    y.max = Math.ceil(yAxisMax) + 5;
    yAxisMaxs.push(yAxisMax);
    xAxisDataFlag = false;
    defaultOptions.legend.data.push(y.name);
  });
  defaultOptions.xAxis[0].data = xAxisData;
  echart.setOption(defaultOptions);
}
....

進入 presets/MainPreset.js,加入如下兩句:


......
import EchartsMultipleYAxisChartPlugin from "../EchartsMultipleYAxis";
......
new EchartsMultipleYAxisChartPlugin().configure({ key: 'echarts_multiple_y_axis' })
......

explore 文件夾

進入 superset-frontend/src/explore/components/controls/VizTypeControl.jsx 文件

加入 'echarts_multiple_y_axis',排序一下。


const DEFAULT_ORDER = [
  ...other,
  'echarts_multiple_y_axis'
]

進入 superset-frontend/src/explore/components/controlPanels 文件夾

我們在這裡新建一個文件 EchartsMultipleYAxis.js為少 在這裡加上相關配置。


import { t } from '@superset-ui/translation';
export default {
  label: t('Echarts Multiple Y Axis Chart'),
  controlPanelSections: [
    {
      label: t('Query'),
      expanded: true,
      controlSetRows: [
        ['metrics'],
        ['adhoc_filters'],
        ['groupby'],
        ['columns'],
        ['row_limit'],
        ['contribution'],
      ],
    },
    {
      label: t('Chart Options'),
      expanded: true,
      controlSetRows: [
        ['color_scheme', 'label_colors'],
      ],
    },
  ],
  controlOverrides: {
    groupby: {
      label: t('Series'),
    },
    columns: {
      label: t('Breakdowns'),
      description: t('Defines how each series is broken down'),
    },
  },
};

setup 文件夾


進入 superset-frontend/src/explore/setupPlugins.ts,加入如下代碼:


....
.registerValue('echarts_multiple_y_axis', EchartsMultipleYAxis)

用來註冊下這個插件。

viz.py 文件

我們加入如下 python 代碼。


class EchartsMultipleYAxisViz(DistributionPieViz):
    """Echarts Multiple Y Axis Chart"""
    viz_type = "echarts_multiple_y_axis"
    verbose_name = _("Echarts Multiple Y Axis Chart")
    is_timeseries = False
    def query_obj(self) -> QueryObjectDict:
        d = super().query_obj()
        fd = self.form_data
        if len(d["groupby"]) < len(fd.get("groupby") or []) + len(
            fd.get("columns") or []
        ):
            raise QueryObjectValidationError(
                _("Can't have overlap between Series and Breakdowns")
            )
        if not fd.get("metrics"):
            raise QueryObjectValidationError(_("Pick at least one metric"))
        if not fd.get("groupby"):
            raise QueryObjectValidationError(_("Pick at least one field for [Series]"))
        return d
    def get_data(self, df: pd.DataFrame) -> VizData:
        if df.empty:
            return None
        fd = self.form_data
        metrics = self.metric_labels
        columns = fd.get("columns") or []
        # pandas will throw away nulls when grouping/pivoting,
        # so we substitute NULL_STRING for any nulls in the necessary columns
        filled_cols = self.groupby + columns
        df[filled_cols] = df[filled_cols].fillna(value=NULL_STRING)
        row = df.groupby(self.groupby).sum()[metrics[0]].copy()
        row.sort_values(ascending=False, inplace=True)
        pt = df.pivot_table(index=self.groupby, columns=columns, values=metrics)
        if fd.get("contribution"):
            pt = pt.T
            pt = (pt / pt.sum()).T
        pt = pt.reindex(row.index)
        chart_data = []
        for name, ys in pt.items():
            if pt[name].dtype.kind not in "biufc" or name in self.groupby:
                continue
            if isinstance(name, str):
                series_title = name
            else:
                offset = 0 if len(metrics) > 1 else 1
                series_title = ", ".join([str(s) for s in name[offset:]])
            values = []
            for i, v in ys.items():
                x = i
                if isinstance(x, (tuple, list)):
                    x = ", ".join([str(s) for s in x])
                else:
                    x = str(x)
                values.append({"x": x, "y": v})
            d = {"key": series_title, "values": values}
            chart_data.append(d)
        return chart_data

關於 viz.py 代碼細節


數據科學本身就是複雜的。

NumPy or Pandas 都可以拿出來單獨講好久,好久,好久……


選擇圖表

微信圖片_20220611112452.png

看效果

微信圖片_20220611112455.png

Leave a Reply

Your email address will not be published. Required fields are marked *