Skip to content

Commit

Permalink
feat(viz): Pivot table chart POC (apache#1023)
Browse files Browse the repository at this point in the history
* feat(plugin-chart-pivot-table): add new plugin

* Implement pivot table chart

* Toggle display of grand totals

* Update table viz name

* Minor changes

* Update types

* Implement transpose pivot

* Keep the original order of metrics when sorting

* Use D3 value formatting

* Fix type error

* Explicitly cast payload to JsonObject to fix type error

* Fix tests

* Update react-pivottable dependency

* Solve merge conflicts

* Change thumbnail

* Replace console logs with TODO comments

* Implement z-a sorting

* Update README

Co-authored-by: Ville Brofeldt <ville.v.brofeldt@gmail.com>
  • Loading branch information
2 people authored and zhaoyongjie committed Nov 17, 2021
1 parent 285e9c8 commit 1e2dce6
Show file tree
Hide file tree
Showing 17 changed files with 5,207 additions and 4,222 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'whatwg-fetch';
import fetchRetry from 'fetch-retry';
import { CallApi, Payload, JsonValue } from '../types';
import { CallApi, Payload, JsonValue, JsonObject } from '../types';
import { CACHE_AVAILABLE, CACHE_KEY, HTTP_STATUS_NOT_MODIFIED, HTTP_STATUS_OK } from '../constants';

function tryParsePayload(payload: Payload) {
Expand Down Expand Up @@ -108,7 +108,7 @@ export default async function callApi({
// not e.g., 'application/x-www-form-urlencoded'
const formData: FormData = new FormData();
Object.keys(payload).forEach(key => {
const value = payload[key] as JsonValue;
const value = (payload as JsonObject)[key] as JsonValue;
if (typeof value !== 'undefined') {
formData.append(key, stringify ? JSON.stringify(value) : String(value));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## @superset-ui/plugin-chart-pivot-table

[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-pivot-table.svg?style=flat-square)](https://www.npmjs.com/package/@superset-ui/plugin-chart-pivot-table)

This plugin provides Pivot 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 PivotTableChartPlugin from '@superset-ui/plugin-chart-pivot-table';

new PivotTableChartPlugin().configure({ key: 'pivot-table-v2' }).register();
```

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

```js
<SuperChart
chartType="pivot-table-v2"
width={600}
height={600}
formData={...}
queriesData={[{
data: {...},
}]}
/>
```

### File structure generated

```
├── package.json
├── README.md
├── tsconfig.json
├── src
│   ├── PivotTableChart.tsx
│   ├── images
│   │   └── thumbnail.png
│   ├── index.ts
│   ├── plugin
│   │   ├── buildQuery.ts
│   │   ├── controlPanel.ts
│   │   ├── index.ts
│   │   └── transformProps.ts
│   └── types.ts
├── test
│   └── index.test.ts
└── types
└── external.d.ts
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@superset-ui/plugin-chart-pivot-table",
"version": "0.0.0",
"description": "Superset Chart - Pivot 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.git"
},
"keywords": [
"superset"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache-superset/superset-ui/issues"
},
"homepage": "https://github.com/apache-superset/superset-ui#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@superset-ui/chart-controls": "0.17.30",
"@superset-ui/core": "0.17.30",
"@superset-ui/react-pivottable": "^0.12.5"
},
"peerDependencies": {
"react": "^16.13.1"
},
"devDependencies": {
"@babel/types": "^7.13.12",
"@types/jest": "^26.0.0",
"jest": "^26.0.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { styled, AdhocMetric, getNumberFormatter } from '@superset-ui/core';
// @ts-ignore
import PivotTable from '@superset-ui/react-pivottable/PivotTable';
// @ts-ignore
import { sortAs, aggregatorTemplates } from '@superset-ui/react-pivottable/Utilities';
import '@superset-ui/react-pivottable/pivottable.css';
import { PivotTableProps, PivotTableStylesProps } from './types';

const Styles = styled.div<PivotTableStylesProps>`
padding: ${({ theme }) => theme.gridUnit * 4}px;
height: ${({ height }) => height}px;
width: ${({ width }) => width}px;
overflow-y: scroll;
}
`;

// TODO: remove eslint-disable when click callbacks are implemented
/* eslint-disable @typescript-eslint/no-unused-vars */
const clickCellCallback = (
e: MouseEvent,
value: number,
filters: Record<string, any>,
pivotData: Record<string, any>,
) => {
// TODO: Implement a callback
};

const clickColumnHeaderCallback = (
e: MouseEvent,
value: string,
filters: Record<string, any>,
pivotData: Record<string, any>,
isSubtotal: boolean,
isGrandTotal: boolean,
) => {
// TODO: Implement a callback
};

const clickRowHeaderCallback = (
e: MouseEvent,
value: string,
filters: Record<string, any>,
pivotData: Record<string, any>,
isSubtotal: boolean,
isGrandTotal: boolean,
) => {
// TODO: Implement a callback
};

export default function PivotTableChart(props: PivotTableProps) {
const {
data,
height,
width,
groupbyRows,
groupbyColumns,
metrics,
tableRenderer,
colOrder,
rowOrder,
aggregateFunction,
transposePivot,
rowSubtotalPosition,
colSubtotalPosition,
colTotals,
rowTotals,
valueFormat,
} = props;

const adaptiveFormatter = getNumberFormatter(valueFormat);

const aggregators = (tpl => ({
Count: tpl.count(adaptiveFormatter),
'Count Unique Values': tpl.countUnique(adaptiveFormatter),
'List Unique Values': tpl.listUnique(', '),
Sum: tpl.sum(adaptiveFormatter),
Average: tpl.average(adaptiveFormatter),
Median: tpl.median(adaptiveFormatter),
'Sample Variance': tpl.var(1, adaptiveFormatter),
'Sample Standard Deviation': tpl.stdev(1, adaptiveFormatter),
Minimum: tpl.min(adaptiveFormatter),
Maximum: tpl.max(adaptiveFormatter),
First: tpl.first(adaptiveFormatter),
Last: tpl.last(adaptiveFormatter),
'Sum as Fraction of Total': tpl.fractionOf(tpl.sum(), 'total', adaptiveFormatter),
'Sum as Fraction of Rows': tpl.fractionOf(tpl.sum(), 'row', adaptiveFormatter),
'Sum as Fraction of Columns': tpl.fractionOf(tpl.sum(), 'col', adaptiveFormatter),
'Count as Fraction of Total': tpl.fractionOf(tpl.count(), 'total', adaptiveFormatter),
'Count as Fraction of Rows': tpl.fractionOf(tpl.count(), 'row', adaptiveFormatter),
'Count as Fraction of Columns': tpl.fractionOf(tpl.count(), 'col', adaptiveFormatter),
}))(aggregatorTemplates);

const metricNames = metrics.map((metric: string | AdhocMetric) =>
typeof metric === 'string' ? metric : (metric.label as string),
);

const unpivotedData = data.reduce(
(acc: Record<string, any>[], record: Record<string, any>) => [
...acc,
...metricNames.map((name: string) => ({
...record,
metric: name,
value: record[name],
})),
],
[],
);

const [rows, cols] = transposePivot
? [groupbyColumns, ['metric', ...groupbyRows]]
: [groupbyRows, ['metric', ...groupbyColumns]];

return (
<Styles height={height} width={width}>
<PivotTable
data={unpivotedData}
rows={rows}
cols={cols}
aggregators={aggregators}
aggregatorName={aggregateFunction}
vals={['value']}
rendererName={tableRenderer}
colOrder={colOrder}
rowOrder={rowOrder}
sorters={{
metric: sortAs(metricNames),
}}
tableOptions={{
clickCallback: clickCellCallback,
clickRowHeaderCallback,
clickColumnHeaderCallback,
colTotals,
rowTotals,
}}
subtotalOptions={{
colSubtotalDisplay: { displayOnTop: colSubtotalPosition },
rowSubtotalDisplay: { displayOnTop: rowSubtotalPosition },
}}
/>
</Styles>
);
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// eslint-disable-next-line import/prefer-default-export
export { default as PivotTableChartPlugin } from './plugin';
/**
* Note: this file exports the default export from PivotTableChart.tsx.
* If you want to export multiple visualization modules, you will need to
* either add additional plugin folders (similar in structure to ./plugin)
* OR export multiple instances of `ChartPlugin` extensions in ./plugin/index.ts
* which in turn load exports from PivotTableChart.tsx
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { buildQueryContext, ensureIsArray } from '@superset-ui/core';
import { PivotTableQueryFormData } from '../types';

export default function buildQuery(formData: PivotTableQueryFormData) {
const { groupbyColumns = [], groupbyRows = [] } = formData;
const groupbySet = new Set([
...ensureIsArray<string>(groupbyColumns),
...ensureIsArray<string>(groupbyRows),
]);
return buildQueryContext(formData, baseQueryObject => [
{
...baseQueryObject,
columns: [...groupbySet],
},
]);
}

0 comments on commit 1e2dce6

Please sign in to comment.