去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-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 =
'+
height +
'px; width:' +
width +
'px;">';
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;
// 开头导入
import XspreadsheetChartPlugin from '../Xspreadsheet/XspreadsheetChartPlugin';
// 末尾添加
new XspreadsheetChartPlugin().configure({ key: 'xspreadsheet' }),
//找到 DEFAULT_ORDER 这个变量 数组末尾 添加 新图表
'xspreadsheet',
/**
* 新增图表,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']],
},
},
};
superset-frontend/src/setup/setupPlugins.ts
// 开头引入
import Xspreadsheet from '../explore/controlPanels/Xspreadsheet';
// 末尾注册
.registerValue('xspreadsheet', Xspreadsheet)