Skip to content
This repository has been archived by the owner on Jun 25, 2020. It is now read-only.

Commit

Permalink
feat(tablevis): this pr is to add a new tablevis plguin to the system
Browse files Browse the repository at this point in the history
  • Loading branch information
Conglei Shi committed Aug 12, 2019
1 parent 8479099 commit dd0f916
Show file tree
Hide file tree
Showing 18 changed files with 877 additions and 1 deletion.
34 changes: 34 additions & 0 deletions packages/superset-ui-plugin-chart-table/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## @superset-ui/plugin-chart-table

[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-table.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/plugin-chart-table.svg?style=flat-square)
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui-plugins.svg?path=packages%2Fsuperset-ui-plugin-chart-table&style=flat-square)](https://david-dm.org/apache-superset/superset-ui-plugins?path=packages/superset-ui-plugin-chart-table)

This plugin provides Table for Superset.

### Usage

Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to lookup this chart throughout the app.

```js
import TableChartPlugin from '@superset-ui/plugin-chart-table';

new TableChartPlugin()
.configure({ key: 'table' })
.register();
```

Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui-plugins/?selectedKind=plugin-chart-table) for more details.

```js
<SuperChart
chartType="table"
chartProps={{
width: 600,
height: 600,
formData: {...},
payload: {
data: {...},
},
}}
/>
```
40 changes: 40 additions & 0 deletions packages/superset-ui-plugin-chart-table/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@superset-ui/plugin-chart-table",
"version": "0.10.0",
"description": "Superset Chart - Table",
"sideEffects": false,
"main": "lib/index.js",
"module": "esm/index.js",
"files": [
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/apache-superset/superset-ui-plugins.git"
},
"keywords": [
"superset"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache-superset/superset-ui-plugins/issues"
},
"homepage": "https://github.com/apache-superset/superset-ui-plugins#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@airbnb/lunar": "^1.8.1",
"@airbnb/lunar-icons": "^1.1.1",
"core-js": "^3.0.1",
"dompurify": "^1.0.3"
},
"peerDependencies": {
"@superset-ui/chart": "^0.10.0",
"@superset-ui/number-format": "^0.10.0",
"@superset-ui/time-format": "^0.10.0",
"@superset-ui/translation": "^0.10.0"
}
}
276 changes: 276 additions & 0 deletions packages/superset-ui-plugin-chart-table/src/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import React, { CSSProperties } from 'react';
import DataTable from '@airbnb/lunar/lib/components/DataTable';
import { Renderers, RendererProps } from '@airbnb/lunar/lib/components/DataTable/types';
import { HEIGHT_TO_PX } from '@airbnb/lunar/lib/components/DataTable/constants';
import { FormDataMetric, Metric } from '@superset-ui/chart';
import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format';
import { getTimeFormatter } from '@superset-ui/time-format';

type ColumnType = {
key: string;
label: string;
format: string;
};

type Props = {
data: any[];
height: number;
alignPositiveNegative?: boolean;
colorPositiveNegative?: boolean;
columns: ColumnType[];
filters?: {
[key: string]: any[];
};
includeSearch?: boolean;
metrics: FormDataMetric[];
onAddFilter?: (key: string, value: number[]) => void;
onRemoveFilter?: (key: string, value: number[]) => void;
orderDesc: boolean;
pageLength: number | string;
percentMetrics: string[];
tableFilter: boolean;
tableTimestampFormat: string;
timeseriesLimitMetric: FormDataMetric;
};

function NOOP(key: string, value: []) {}

const defaultProps = {
alignPositiveNegative: false,
colorPositiveNegative: false,
filters: {},
includeSearch: false,
onAddFilter: NOOP,
onRemoveFilter: NOOP,
};

export type TableProps = Props & Readonly<typeof defaultProps>;

type Cell = {
key: string;
value: any;
};

type TableState = {
selectedCells: Set<string>;
};

const NEGATIVE_COLOR = '#ff8787';
const POSITIVE_COLOR = '#ced4da';

const formatPercent = getNumberFormatter(NumberFormats.PERCENT_3_POINT);

class TableVis extends React.Component<TableProps, TableState> {
static defaultProps = defaultProps;

constructor(props: TableProps) {
super(props);
this.state = {
selectedCells: new Set(),
};
}

handleCellSelected = (cell: Cell) => () => {
const { selectedCells } = this.state;
const { tableFilter, onRemoveFilter, onAddFilter } = this.props;

if (!tableFilter) {
return;
}
const newSelectedCells = new Set(Array.from(selectedCells));
const cellHash = `${cell.key}#${cell.value}`;
if (newSelectedCells.has(cellHash)) {
newSelectedCells.delete(cellHash);
onRemoveFilter(cell.key, [cell.value]);
} else {
newSelectedCells.add(cellHash);
onAddFilter(cell.key, [cell.value]);
}
this.setState({
selectedCells: newSelectedCells,
});
};

static getDerivedStateFromProps(props: TableProps, state: TableState) {
const { filters } = props;
const { selectedCells } = state;
const newSelectedCells = new Set(Array.from(selectedCells));
Object.keys(filters).forEach(key => {
filters[key].forEach(value => {
newSelectedCells.add(`${key}#${value}`);
});
});
return {
...state,
selectedCells: newSelectedCells,
};
}

render() {
const {
metrics: rawMetrics,
timeseriesLimitMetric,
orderDesc,
percentMetrics,
data,
alignPositiveNegative,
colorPositiveNegative,
columns,
tableTimestampFormat,
tableFilter,
} = this.props;
const { selectedCells } = this.state;
const metrics = (rawMetrics || [])
.map(m => (m as Metric).label || (m as string))
// Add percent metrics
.concat((percentMetrics || []).map(m => `%${m}`))
// Removing metrics (aggregates) that are strings
.filter(m => typeof data[0][m as string] === 'number');
const dataArray: {
[key: string]: any;
} = {};

const sortByKey =
timeseriesLimitMetric &&
((timeseriesLimitMetric as Metric).label || (timeseriesLimitMetric as string));

metrics.forEach(metric => {
const arr = [];
for (let i = 0; i < data.length; i += 1) {
arr.push(data[i][metric]);
}

dataArray[metric] = arr;
});

const maxes: {
[key: string]: number;
} = {};
const mins: {
[key: string]: number;
} = {};

for (let i = 0; i < metrics.length; i += 1) {
if (alignPositiveNegative) {
maxes[metrics[i]] = Math.max(...dataArray[metrics[i]].map(Math.abs));
} else {
maxes[metrics[i]] = Math.max(...dataArray[metrics[i]]);
mins[metrics[i]] = Math.min(...dataArray[metrics[i]]);
}
}

// const tsFormatter = getTimeFormatter(tableTimestampFormat);
let formattedData = data.map(row => ({
data: row,
}));

if (sortByKey) {
formattedData = formattedData.sort((a, b) => {
const delta = a.data[sortByKey] - b.data[sortByKey];
if (orderDesc) {
return -delta;
}
return delta;
});
if (metrics.indexOf(sortByKey) < 0) {
formattedData = formattedData.map(row => {
const data = { ...row.data };
delete data[sortByKey];
return {
data,
};
});
}
}

const tsFormatter = getTimeFormatter(tableTimestampFormat);

const heightType = 'small';
const getRenderer = (column: ColumnType) => (props: RendererProps) => {
const { key } = props;
let value = props.row.rowData.data[key];
const cellHash = `${key}#${value}`;
const isMetric = metrics.indexOf(key) >= 0;
let Parent;

if (isMetric) {
let left = 0;
let width = 0;
if (alignPositiveNegative) {
width = Math.abs(Math.round((value / maxes[key]) * 100));
} else {
const posExtent = Math.abs(Math.max(maxes[key], 0));
const negExtent = Math.abs(Math.min(mins[key], 0));
const tot = posExtent + negExtent;
left = Math.round((Math.min(negExtent + value, negExtent) / tot) * 100);
width = Math.round((Math.abs(value) / tot) * 100);
}
const color = colorPositiveNegative && value < 0 ? NEGATIVE_COLOR : POSITIVE_COLOR;
Parent = ({ children }: { children: React.ReactNode }) => {
const boxStyle: CSSProperties = {
margin: '0px -16px',
backgroundColor: tableFilter && selectedCells.has(cellHash) ? '#ffec99' : undefined,
};
const boxContainerStyle: CSSProperties = {
position: 'relative',
height: HEIGHT_TO_PX[heightType],
textAlign: isMetric ? 'right' : 'left',
display: 'flex',
alignItems: 'center',
margin: '0px 16px',
};
const barStyle: CSSProperties = {
background: color,
width: `${width}%`,
left: `${left}%`,
position: 'absolute',
height: 24,
};

return (
<div style={boxStyle}>
<div style={boxContainerStyle}>
<div style={barStyle} />
<div style={{ zIndex: 10, marginLeft: 'auto' }}>{children}</div>
</div>
</div>
);
};

if (key[0] === '%') {
value = formatPercent(value);
} else {
value = getNumberFormatter(column.format)(value);
}
} else {
if (key === '__timestamp') {
value = tsFormatter(value);
}
Parent = ({ children }: { children: React.ReactNode }) => (
<React.Fragment>{children}</React.Fragment>
);
}

return (
<div
onClick={this.handleCellSelected({
key,
value: props.row.rowData.data[key],
})}
>
<Parent>{value}</Parent>
</div>
);
};

const renderers: Renderers = {};

columns.forEach(column => {
renderers[column.key] = getRenderer(column);
});

return <DataTable data={formattedData} zebra rowHeight={heightType} renderers={renderers} />;
}
}

export default TableVis;
10 changes: 10 additions & 0 deletions packages/superset-ui-plugin-chart-table/src/TableFormData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ChartFormData, FormDataMetric } from '@superset-ui/chart';

type TableFormData = ChartFormData & {
all_columns: string[];
percent_metrics: FormDataMetric[];
include_time: boolean;
order_by_cols: string[];
};

export default TableFormData;
34 changes: 34 additions & 0 deletions packages/superset-ui-plugin-chart-table/src/buildQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { buildQueryContext, FormDataMetric, Metric } from '@superset-ui/chart';
import Metrics from '@superset-ui/chart/lib/query/Metrics';
import TableFormData from './TableFormData';

export default function buildQuery(formData: TableFormData) {
// Set the single QueryObject's groupby field with series in formData
return buildQueryContext(formData, baseQueryObject => {
const isTimeseries = formData.include_time;
let columns: string[] = [];
let groupby = baseQueryObject.groupby;
let orderby: [Metric, boolean][] = [];
const sortby = formData.timeseries_limit_metric;
if (formData.all_columns && formData.all_columns.length > 0) {
columns = [...formData.all_columns];
const orderByColumns = formData.order_by_cols || [];
orderByColumns.forEach(columnOrder => {
const parsedColumnOrder: [FormDataMetric, boolean] = JSON.parse(columnOrder);
orderby.push([Metrics.formatMetric(parsedColumnOrder[0]), parsedColumnOrder[1]]);
});
groupby = [];
} else if (sortby) {
orderby.push([Metrics.formatMetric(sortby), !formData.order_desc]);
}
return [
{
...baseQueryObject,
columns,
is_timeseries: isTimeseries,
orderby,
groupby,
},
];
});
}
Loading

0 comments on commit dd0f916

Please sign in to comment.