当前位置: 首页 > 工具软件 > xspreadsheet > 使用案例 >

superset集成xspreadsheet

古弘
2023-12-01

集成x-spreadsheet

准备工作

去github下载最新的xspreadsheet,5.20号更新了一个功能。下载之后,解压,npm install , npm run dev没问题后。把dist文件夹下所有文件复制,包括子目录。放在新建的文件夹\superset-frontend\src\xspreadsheet下面。这个文件就是一个组件,之后去调用就可以了。

后端

修改 viz.py , 模仿TableViz,添加 XspreadsheetViz,几乎只用修改 get_data

class XspreadsheetVix(BaseViz):

    """集成xspreadsheet"""

    viz_type = "xspreadsheet"
    verbose_name = _("Xspreadsheet")
    is_timeseries = False
    enforce_numerical_metrics = False

    def should_be_timeseries(self):
        fd = self.form_data
        # TODO handle datasource-type-specific code in datasource
        conditions_met = (fd.get("granularity") and fd.get("granularity") != "all") or (
            fd.get("granularity_sqla") and fd.get("time_grain_sqla")
        )
        if fd.get("include_time") and not conditions_met:
            raise Exception(
                _("Pick a granularity in the Time section or " "uncheck 'Include Time'")
            )
        return fd.get("include_time")

    def query_obj(self) -> Dict[str, Any]:  # Any类型兼容所有类型,赋值时不用安全检查
        '''
        重写 qurey_obj,处理查询条件
        :return: d
        '''
        d = super().query_obj()
        fd = self.form_data

        if fd.get("all_columns") and (
            fd.get("groupby") or fd.get("metrics") or fd.get("percent_metrics")
        ):
            raise Exception(
                _(
                    "Choose either fields to [Group By] and [Metrics] and/or "
                    "[Percentage Metrics], or [Columns], not both"
                )
            )

        sort_by = fd.get("timeseries_limit_metric")
        if fd.get("all_columns"):
            d["columns"] = fd.get("all_columns")
            d["groupby"] = []
            order_by_cols = fd.get("order_by_cols") or []
            d["orderby"] = [json.loads(t) for t in order_by_cols]
        elif sort_by:
            sort_by_label = utils.get_metric_name(sort_by)
            if sort_by_label not in utils.get_metric_names(d["metrics"]):
                d["metrics"] += [sort_by]
            d["orderby"] = [(sort_by, not fd.get("order_desc", True))]

        # Add all percent metrics that are not already in the list
        if "percent_metrics" in fd:
            d["metrics"].extend(
                m for m in fd["percent_metrics"] or [] if m not in d["metrics"]
            )

        d["is_timeseries"] = self.should_be_timeseries()
        return d


    @staticmethod
    def df2xspreadsheetjson(df) -> Dict:
        '''
        df对象转为 x-spreadsheet格式的json字符串
        :param df: 从数据库得到的 dataframe
        :return: str
        '''
        cols = []
        # 重命名标题行,主要考虑标题行空等情况
        for col in df.columns:
            if col == "":
                cols.append("N/A")  # 标题行空,一般不可能
            elif col is None:
                cols.append("NULL")  # 表是空的
            else:
                cols.append(col)  # 复制过去
        # print(cols)
        df.columns = cols

        if df.shape[0] < 1 or df.shape[1] < 1:
            return '{}'

        metrics = cols
        df = df[metrics]  # 取我们需要的字段
        # 直接拼接字符串
        info = ''
        # 先拼接标题行
        info += '\"0\":{\"cells\":{'
        for i in range(len(metrics)):
            if i != len(metrics) - 1:
                info += '\"' + str(i) + '\":' + '{\"text\":\"' + str(metrics[i]) + "\"},"
            else:
                info += '\"' + str(i) + '\":' + '{\"text\":\"' + str(metrics[i]) + "\"}"
        info += '}},'

        for index, row in df.iterrows():
            info += '\"' + str(index + 1) + '\":'
            for j in range(len(metrics)):
                if j == 0:
                    info += "{\"cells\":{"
                # print(row[col])
                if j != len(metrics) - 1:
                    info += '\"' + str(j) + '\":' + '{\"text\":\"' + str(row[metrics[j]]) + "\"},"
                else:
                    info += '\"' + str(j) + '\":' + '{\"text\":\"' + str(row[metrics[j]]) + "\"}"
            info += '}},'

        rows = '{' + info + '\"len\":\"' + str(df.shape[0] + 2) + '\"}'  # 加标题行长度,最好再加上一行+1+1
        cols = '\"cols\":{\"len\":\"' + str(df.shape[1] + 1) + '\"}'
        ret_str = '{\"name\":\"表格\",\"rows\":' + rows + ',' + cols + '}'

        return json.loads(ret_str)  # 最后把它转为一个字典,方便后续处理



    def get_data(self, df: pd.DataFrame) -> Dict:
        """
        Transform the query result to the table representation.

        :param df: The interim dataframe
        :returns: The table visualization data
        """

        non_percent_metric_columns = []
        # Transform the data frame to adhere to the UI ordering of the columns and
        # metrics whilst simultaneously computing the percentages (via normalization)
        # for the percent metrics.

        if DTTM_ALIAS in df:
            # 处理时间序列
            if self.should_be_timeseries():
                non_percent_metric_columns.append(DTTM_ALIAS)
            else:
                del df[DTTM_ALIAS]

        non_percent_metric_columns.extend(
            self.form_data.get("all_columns") or self.form_data.get("groupby") or []
        )

        non_percent_metric_columns.extend(
            utils.get_metric_names(self.form_data.get("metrics") or [])
        )

        timeseries_limit_metric = utils.get_metric_name(
            self.form_data.get("timeseries_limit_metric")
        )
        if timeseries_limit_metric:
            non_percent_metric_columns.append(timeseries_limit_metric)

        percent_metric_columns = utils.get_metric_names(
            self.form_data.get("percent_metrics") or []
        )

        df = pd.concat(
            [
                df[non_percent_metric_columns],
                (
                    df[percent_metric_columns]
                    .div(df[percent_metric_columns].sum())
                    .add_prefix("%")
                ),
            ],
            axis=1,
        )
        data = self.df2xspreadsheetjson(df)  # 转化为xspreadsheet格式的jsonDict

        return data

    def json_dumps(self, obj, sort_keys=False):
        return json.dumps(
            obj, default=utils.json_iso_dttm_ser, sort_keys=sort_keys, ignore_nan=True
        )

前端

1. 在superset-frontend/src/visualizations/ 目录下

1-1 新增文件夹Xspreadsheet
主要新加的文件目录

1-2 新建文件夹 images 放入新增图表的图片

1-3 新增文件 XspreadsheetChartPlugin.js

import { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import transformProps from './transformProps';
import thumbnail from './images/xspreadsheet.png';

const metadata = new ChartMetadata({
  name: t('X_Spreadsheet'),
  description: '',
  credits: ['https://github.com/myliang/x-spreadsheet'],
  thumbnail,
});

export default class MixLineBarChartPlugin extends ChartPlugin {
  constructor() {
    super({
      metadata,
      transformProps,
      loadChart: () => import('./ReactXspreadsheet.js'), // 前端渲染逻辑
    });
  }
}

1-4 新增文件 ReactXspreadsheet.js 注册

import reactify from '@superset-ui/chart/esm/components/reactify';
import Component from './Xspreadsheet';

export default reactify(Component);

1-5 新增文件 transformProps.js 前端后端数据转换

export default function transformProps(chartProps) {
  const { width, height, queryData, formData } = chartProps;
  console.log("DEBUG|chartProps=", chartProps);
  // formData 前端页面的数据
  // queryData  后端返回的数据
  return {
    width,
    height,
    formData,
    queryData,
  };
}

1-6 新增文件 Xspreadsheet.js 前端渲染图表主要逻辑

import '../../xspreadsheet/xspreadsheet.css';
import '../../xspreadsheet/xspreadsheet';
import '../../xspreadsheet/locale/zh-cn';

import React from 'react';
import ReactDOM from 'react-dom';
import d3 from 'd3';
import PropTypes from 'prop-types';
import { CategoricalColorNamespace } from '@superset-ui/color';

// 数据类型检查
const propTypes = {
  data: PropTypes.object,
  width: PropTypes.number,
  height: PropTypes.number,
};

function Xspreadsheet(element, props) {
  const { width, height, formData, queryData } = props; // transformProps.js 返回的数据
  // console.clear(); // 先清一下
  console.log('DEBUG|props=', props); // debug,查看数据

  const fd = formData;
  console.log('DEBUG|formData=', formData);
  // 处理series 显示的数据 [{'name':xx, 'type':xx, 'data':xx, 'yAxisIndex':xx}]

  // div.html(html);
  // init echarts,light 为制定主题,可以查看官方api
  // echarts 渲染图表的数据格式 在官网可以查看
  const div = d3.select(element);
  const sliceId = 'xspreadsheet-' + fd.sliceId;
  console.log('DEBUG|', sliceId);
  const html =
    '<div id=' +
    sliceId +
    ' style="height:' +
    height +
    'px; width:' +
    width +
    'px;"></div>';
  div.html(html);
  const data = queryData.data;
  x_spreadsheet.locale('zh-cn');
  const xs = x_spreadsheet(document.getElementById(sliceId)).loadData(data); // 创建表格
}

Xspreadsheet.displayName = 'Xspreadsheet';
Xspreadsheet.propTypes = propTypes;

export default Xspreadsheet;

2、 修改 superset-frontend/src/visualizations/presets/MainPreset.js

// 开头导入
import XspreadsheetChartPlugin from '../Xspreadsheet/XspreadsheetChartPlugin';

// 末尾添加
new XspreadsheetChartPlugin().configure({ key: 'xspreadsheet' }),

3、 修改 superset-frontend/src/explore/components/controls/VizTypeControl.jsx

 //找到 DEFAULT_ORDER 这个变量 数组末尾 添加 新图表
 
'xspreadsheet',

4、新增 superset-frontend/src/explore/controlPanels/Xspreadsheet.js

/**
 * 新增图表,xspreadsheet,布局直接复制已有的 Table.js
 */
import { t } from '@superset-ui/translation';
import * as v from '../validators';
import { D3_TIME_FORMAT_OPTIONS } from '../controls';
import { formatSelectOptions } from '../../modules/utils';

export default {
  controlPanelSections: [
    {
      label: t('GROUP BY'),
      description: t('Use this section if you want a query that aggregates'),
      expanded: true,
      controlSetRows: [
        ['groupby'],
        ['metrics'],
        [
          {
            name: 'percent_metrics',
            config: {
              type: 'MetricsControl',
              multi: true,
              mapStateToProps: state => {
                const datasource = state.datasource;
                return {
                  columns: datasource ? datasource.columns : [],
                  savedMetrics: datasource ? datasource.metrics : [],
                  datasourceType: datasource && datasource.type,
                };
              },
              default: [],
              label: t('Percentage Metrics'),
              validators: [],
              description: t(
                'Metrics for which percentage of total are to be displayed',
              ),
            },
          },
        ],
        ['timeseries_limit_metric', 'row_limit'],
        [
          {
            name: 'include_time',
            config: {
              type: 'CheckboxControl',
              label: t('Include Time'),
              description: t(
                'Whether to include the time granularity as defined in the time section',
              ),
              default: false,
            },
          },
          'order_desc',
        ],
      ],
    },
    {
      label: t('NOT GROUPED BY'),
      description: t('Use this section if you want to query atomic rows'),
      expanded: true,
      controlSetRows: [
        ['all_columns'],
        [
          {
            name: 'order_by_cols',
            config: {
              type: 'SelectControl',
              multi: true,
              label: t('Ordering'),
              default: [],
              description: t('One or many metrics to display'),
              mapStateToProps: state => ({
                choices: state.datasource
                  ? state.datasource.order_by_choices
                  : [],
              }),
            },
          },
        ],
        ['row_limit', null],
      ],
    },
    {
      label: t('Query'),
      expanded: true,
      controlSetRows: [['adhoc_filters']],
    },
    {
      label: t('Options'),
      expanded: true,
      controlSetRows: [
        [
          {
            name: 'table_timestamp_format',
            config: {
              type: 'SelectControl',
              freeForm: true,
              label: t('Table Timestamp Format'),
              default: '%Y-%m-%d %H:%M:%S',
              renderTrigger: true,
              validators: [v.nonEmpty],
              clearable: false,
              choices: D3_TIME_FORMAT_OPTIONS,
              description: t('Timestamp Format'),
            },
          },
        ],
        [
          {
            name: 'page_length',
            config: {
              type: 'SelectControl',
              freeForm: true,
              renderTrigger: true,
              label: t('Page Length'),
              default: 0,
              choices: formatSelectOptions([
                0,
                10,
                25,
                40,
                50,
                75,
                100,
                150,
                200,
              ]),
              description: t('Rows per page, 0 means no pagination'),
            },
          },
          null,
        ],
        [
          {
            name: 'include_search',
            config: {
              type: 'CheckboxControl',
              label: t('Search Box'),
              renderTrigger: true,
              default: false,
              description: t('Whether to include a client-side search box'),
            },
          },
          'table_filter',
        ],
        [
          {
            name: 'align_pn',
            config: {
              type: 'CheckboxControl',
              label: t('Align +/-'),
              renderTrigger: true,
              default: false,
              description: t(
                'Whether to align the background chart for +/- values',
              ),
            },
          },
          {
            name: 'color_pn',
            config: {
              type: 'CheckboxControl',
              label: t('Color +/-'),
              renderTrigger: true,
              default: true,
              description: t('Whether to color +/- values'),
            },
          },
        ],
      ],
    },
  ],
  controlOverrides: {
    metrics: {
      validators: [],
    },
  },
  sectionOverrides: {
    druidTimeSeries: {
      controlSetRows: [['granularity', 'druid_time_origin'], ['time_range']],
    },
    sqlaTimeSeries: {
      controlSetRows: [['granularity_sqla', 'time_grain_sqla'], ['time_range']],
    },
  },
};

6. 修改 setupPlugins.ts

superset-frontend/src/setup/setupPlugins.ts

// 开头引入
import Xspreadsheet from '../explore/controlPanels/Xspreadsheet';

// 末尾注册
.registerValue('xspreadsheet', Xspreadsheet)
 类似资料: