Skip to content

Commit

Permalink
feat(plugin-chart-pivot-table): column, date and conditional formatti…
Browse files Browse the repository at this point in the history
…ng (#1217)

* feat(plugin-chart-pivot-table): implement conditional and date formatting

* Use custom icons for expand/collapse

* Fix tests

* Revert changes to ControlForm

* Fix tests

* Rename variable
  • Loading branch information
kgabryje authored and zhaoyongjie committed Nov 26, 2021
1 parent 89474f8 commit fe5f9b0
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
"dependencies": {
"@superset-ui/chart-controls": "0.17.67",
"@superset-ui/core": "0.17.64",
"@superset-ui/react-pivottable": "^0.12.8"
"@superset-ui/react-pivottable": "^0.12.9"
},
"peerDependencies": {
"react": "^16.13.1"
"react": "^16.13.1",
"@ant-design/icons": "^4.2.2"
},
"devDependencies": {
"@babel/types": "^7.13.12",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import { styled, AdhocMetric, getNumberFormatter, DataRecordValue } from '@superset-ui/core';
import React, { useCallback, useMemo } from 'react';
import { PlusSquareOutlined, MinusSquareOutlined } from '@ant-design/icons';
import {
styled,
AdhocMetric,
getNumberFormatter,
DataRecordValue,
NumberFormatter,
} from '@superset-ui/core';
// @ts-ignore
import PivotTable from '@superset-ui/react-pivottable/PivotTable';
// @ts-ignore
Expand All @@ -40,6 +47,52 @@ const Styles = styled.div<PivotTableStylesProps>`
`;

const METRIC_KEY = 'metric';
const iconStyle = { stroke: 'black', strokeWidth: '16px' };

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

export default function PivotTableChart(props: PivotTableProps) {
const {
Expand All @@ -49,11 +102,11 @@ export default function PivotTableChart(props: PivotTableProps) {
groupbyRows,
groupbyColumns,
metrics,
tableRenderer,
colOrder,
rowOrder,
aggregateFunction,
transposePivot,
combineMetric,
rowSubtotalPosition,
colSubtotalPosition,
colTotals,
Expand All @@ -63,54 +116,51 @@ export default function PivotTableChart(props: PivotTableProps) {
setDataMask,
selectedFilters,
verboseMap,
columnFormats,
metricsLayout,
metricColorFormatters,
dateFormatters,
} = 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 defaultFormatter = getNumberFormatter(valueFormat);
const columnFormatsArray = Object.entries(columnFormats);
const hasCustomMetricFormatters = columnFormatsArray.length > 0;
const metricFormatters =
hasCustomMetricFormatters &&
Object.fromEntries(
columnFormatsArray.map(([metric, format]) => [metric, getNumberFormatter(format)]),
);

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

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

let [rows, cols] = transposePivot ? [groupbyColumns, groupbyRows] : [groupbyRows, groupbyColumns];

if (metricsLayout === MetricsLayoutEnum.ROWS) {
rows = [METRIC_KEY, ...rows];
rows = combineMetric ? [...rows, METRIC_KEY] : [METRIC_KEY, ...rows];
} else {
cols = [METRIC_KEY, ...cols];
cols = combineMetric ? [...cols, METRIC_KEY] : [METRIC_KEY, ...cols];
}

const handleChange = useCallback(
Expand Down Expand Up @@ -144,11 +194,6 @@ export default function PivotTableChart(props: PivotTableProps) {
[setDataMask],
);

const isActiveFilterValue = useCallback(
(key: string, val: DataRecordValue) => !!selectedFilters && selectedFilters[key]?.includes(val),
[selectedFilters],
);

const toggleFilter = useCallback(
(
e: MouseEvent,
Expand All @@ -162,6 +207,9 @@ export default function PivotTableChart(props: PivotTableProps) {
return;
}

const isActiveFilterValue = (key: string, val: DataRecordValue) =>
!!selectedFilters && selectedFilters[key]?.includes(val);

const filtersCopy = { ...filters };
delete filtersCopy[METRIC_KEY];

Expand Down Expand Up @@ -201,10 +249,14 @@ export default function PivotTableChart(props: PivotTableProps) {
data={unpivotedData}
rows={rows}
cols={cols}
aggregators={aggregators}
aggregatorsFactory={aggregatorsFactory}
defaultFormatter={defaultFormatter}
customFormatters={
hasCustomMetricFormatters ? { [METRIC_KEY]: metricFormatters } : undefined
}
aggregatorName={aggregateFunction}
vals={['value']}
rendererName={tableRenderer}
rendererName="Table With Subtotal"
colOrder={colOrder}
rowOrder={rowOrder}
sorters={{
Expand All @@ -218,10 +270,14 @@ export default function PivotTableChart(props: PivotTableProps) {
highlightHeaderCellsOnHover: emitFilter,
highlightedHeaderCells: selectedFilters,
omittedHighlightHeaderGroups: [METRIC_KEY],
cellColorFormatters: { [METRIC_KEY]: metricColorFormatters },
dateFormatters,
}}
subtotalOptions={{
colSubtotalDisplay: { displayOnTop: colSubtotalPosition },
rowSubtotalDisplay: { displayOnTop: rowSubtotalPosition },
arrowCollapsed: <PlusSquareOutlined style={iconStyle} />,
arrowExpanded: <MinusSquareOutlined style={iconStyle} />,
}}
namesMapping={verboseMap}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import { FeatureFlag, isFeatureEnabled, t, validateNonEmpty } from '@superset-ui/core';
import {
FeatureFlag,
isFeatureEnabled,
QueryFormMetric,
smartDateFormatter,
t,
validateNonEmpty,
} from '@superset-ui/core';
import {
ControlPanelConfig,
D3_TIME_FORMAT_OPTIONS,
formatSelectOptions,
sections,
sharedControls,
Expand Down Expand Up @@ -140,6 +148,21 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'combineMetric',
config: {
type: 'CheckboxControl',
label: t('Combine metrics'),
default: false,
description: t(
'Display metrics side by side within each column, as ' +
'opposed to each column being displayed side by side for each metric.',
),
renderTrigger: true,
},
},
],
],
},
{
Expand All @@ -154,23 +177,16 @@ const config: ControlPanelConfig = {
],
[
{
name: 'tableRenderer',
name: 'date_format',
config: {
type: 'SelectControl',
label: t('Pivot table type'),
default: 'Table With Subtotal',
choices: [
// [value, label]
['Table With Subtotal', t('Table')],
['Table With Subtotal Heatmap', t('Table Heatmap')],
['Table With Subtotal Col Heatmap', t('Table Col Heatmap')],
['Table With Subtotal Row Heatmap', t('Table Row Heatmap')],
['Table With Subtotal Barchart', t('Table Barchart')],
['Table With Subtotal Col Barchart', t('Table Col Barchart')],
['Table With Subtotal Row Barchart', t('Table Row Barchart')],
],
freeForm: true,
label: t('Date format'),
default: smartDateFormatter.id,
renderTrigger: true,
description: t('The type of pivot table visualization'),
clearable: false,
choices: D3_TIME_FORMAT_OPTIONS,
description: t('D3 time format for datetime columns'),
},
},
],
Expand Down Expand Up @@ -280,6 +296,31 @@ const config: ControlPanelConfig = {
},
]
: [],
[
{
name: 'conditional_formatting',
config: {
type: 'ConditionalFormattingControl',
renderTrigger: true,
label: t('Customize metrics'),
description: t('Apply conditional color formatting to metrics'),
mapStateToProps(explore) {
const values = (explore?.controls?.metrics?.value as QueryFormMetric[]) ?? [];
const verboseMap = explore?.datasource?.verbose_map ?? {};
const metricColumn = values.map(value => {
if (typeof value === 'string') {
return { value, label: verboseMap[value] ?? value };
}
return { value: value.label, label: value.label };
});
return {
columnOptions: metricColumn,
verboseMap,
};
},
},
},
],
],
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
import {
t,
ChartMetadata,
ChartPlugin,
Behavior,
ChartProps,
QueryFormData,
} from '@superset-ui/core';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import thumbnail from '../images/thumbnail.png';
import { PivotTableQueryFormData } from '../types';

export default class PivotTableChartPlugin extends ChartPlugin<PivotTableQueryFormData> {
export default class PivotTableChartPlugin extends ChartPlugin<
PivotTableQueryFormData,
ChartProps<QueryFormData>
> {
/**
* The constructor is used to pass relevant metadata and callbacks that get
* registered in respective registries that are used throughout the library
Expand Down

0 comments on commit fe5f9b0

Please sign in to comment.