mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
refactor(monorepo): move superset-ui to superset(stage 2) (#17552)
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import { PlusSquareOutlined, MinusSquareOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
AdhocMetric,
|
||||
DataRecordValue,
|
||||
getColumnLabel,
|
||||
getNumberFormatter,
|
||||
isPhysicalColumn,
|
||||
NumberFormatter,
|
||||
styled,
|
||||
useTheme,
|
||||
} from '@superset-ui/core';
|
||||
// @ts-ignore
|
||||
import PivotTable from '@superset-ui/react-pivottable/PivotTable';
|
||||
import {
|
||||
sortAs,
|
||||
aggregatorTemplates,
|
||||
// @ts-ignore
|
||||
} from '@superset-ui/react-pivottable/Utilities';
|
||||
import '@superset-ui/react-pivottable/pivottable.css';
|
||||
import { isAdhocColumn } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
FilterType,
|
||||
MetricsLayoutEnum,
|
||||
PivotTableProps,
|
||||
PivotTableStylesProps,
|
||||
SelectedFiltersType,
|
||||
} from './types';
|
||||
|
||||
const Styles = styled.div<PivotTableStylesProps>`
|
||||
${({ height, width, margin }) => `
|
||||
margin: ${margin}px;
|
||||
height: ${height - margin * 2}px;
|
||||
width: ${
|
||||
typeof width === 'string' ? parseInt(width, 10) : width - margin * 2
|
||||
}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
const PivotTableWrapper = styled.div`
|
||||
height: 100%;
|
||||
max-width: fit-content;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
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(', ', formatter),
|
||||
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(formatter),
|
||||
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,
|
||||
),
|
||||
});
|
||||
|
||||
/* If you change this logic, please update the corresponding Python
|
||||
* function (https://github.com/apache/superset/blob/master/superset/charts/post_processing.py),
|
||||
* or reach out to @betodealmeida.
|
||||
*/
|
||||
export default function PivotTableChart(props: PivotTableProps) {
|
||||
const {
|
||||
data,
|
||||
height,
|
||||
width,
|
||||
groupbyRows: groupbyRowsRaw,
|
||||
groupbyColumns: groupbyColumnsRaw,
|
||||
metrics,
|
||||
colOrder,
|
||||
rowOrder,
|
||||
aggregateFunction,
|
||||
transposePivot,
|
||||
combineMetric,
|
||||
rowSubtotalPosition,
|
||||
colSubtotalPosition,
|
||||
colTotals,
|
||||
rowTotals,
|
||||
valueFormat,
|
||||
emitFilter,
|
||||
setDataMask,
|
||||
selectedFilters,
|
||||
verboseMap,
|
||||
columnFormats,
|
||||
metricsLayout,
|
||||
metricColorFormatters,
|
||||
dateFormatters,
|
||||
} = props;
|
||||
|
||||
const theme = useTheme();
|
||||
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 = useMemo(
|
||||
() =>
|
||||
data.reduce(
|
||||
(acc: Record<string, any>[], record: Record<string, any>) => [
|
||||
...acc,
|
||||
...metricNames
|
||||
.map((name: string) => ({
|
||||
...record,
|
||||
[METRIC_KEY]: name,
|
||||
value: record[name],
|
||||
}))
|
||||
.filter(record => record.value !== null),
|
||||
],
|
||||
[],
|
||||
),
|
||||
[data, metricNames],
|
||||
);
|
||||
const groupbyRows = groupbyRowsRaw.map(getColumnLabel);
|
||||
const groupbyColumns = groupbyColumnsRaw.map(getColumnLabel);
|
||||
|
||||
let [rows, cols] = transposePivot
|
||||
? [groupbyColumns, groupbyRows]
|
||||
: [groupbyRows, groupbyColumns];
|
||||
|
||||
if (metricsLayout === MetricsLayoutEnum.ROWS) {
|
||||
rows = combineMetric ? [...rows, METRIC_KEY] : [METRIC_KEY, ...rows];
|
||||
} else {
|
||||
cols = combineMetric ? [...cols, METRIC_KEY] : [METRIC_KEY, ...cols];
|
||||
}
|
||||
|
||||
const handleChange = useCallback(
|
||||
(filters: SelectedFiltersType) => {
|
||||
const filterKeys = Object.keys(filters);
|
||||
const groupby = [...groupbyRowsRaw, ...groupbyColumnsRaw];
|
||||
setDataMask({
|
||||
extraFormData: {
|
||||
filters:
|
||||
filterKeys.length === 0
|
||||
? undefined
|
||||
: filterKeys.map(key => {
|
||||
const val = filters?.[key];
|
||||
const col =
|
||||
groupby.find(item => {
|
||||
if (isPhysicalColumn(item)) {
|
||||
return item === key;
|
||||
}
|
||||
if (isAdhocColumn(item)) {
|
||||
return item.label === key;
|
||||
}
|
||||
return false;
|
||||
}) ?? '';
|
||||
if (val === null || val === undefined)
|
||||
return {
|
||||
col,
|
||||
op: 'IS NULL',
|
||||
};
|
||||
return {
|
||||
col,
|
||||
op: 'IN',
|
||||
val: val as (string | number | boolean)[],
|
||||
};
|
||||
}),
|
||||
},
|
||||
filterState: {
|
||||
value:
|
||||
filters && Object.keys(filters).length
|
||||
? Object.values(filters)
|
||||
: null,
|
||||
selectedFilters:
|
||||
filters && Object.keys(filters).length ? filters : null,
|
||||
},
|
||||
});
|
||||
},
|
||||
[setDataMask],
|
||||
);
|
||||
|
||||
const toggleFilter = useCallback(
|
||||
(
|
||||
e: MouseEvent,
|
||||
value: string,
|
||||
filters: FilterType,
|
||||
pivotData: Record<string, any>,
|
||||
isSubtotal: boolean,
|
||||
isGrandTotal: boolean,
|
||||
) => {
|
||||
if (isSubtotal || isGrandTotal || !emitFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isActiveFilterValue = (key: string, val: DataRecordValue) =>
|
||||
!!selectedFilters && selectedFilters[key]?.includes(val);
|
||||
|
||||
const filtersCopy = { ...filters };
|
||||
delete filtersCopy[METRIC_KEY];
|
||||
|
||||
const filtersEntries = Object.entries(filtersCopy);
|
||||
if (filtersEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [key, val] = filtersEntries[filtersEntries.length - 1];
|
||||
|
||||
let updatedFilters = { ...(selectedFilters || {}) };
|
||||
// multi select
|
||||
// if (selectedFilters && isActiveFilterValue(key, val)) {
|
||||
// updatedFilters[key] = selectedFilters[key].filter((x: DataRecordValue) => x !== val);
|
||||
// } else {
|
||||
// updatedFilters[key] = [...(selectedFilters?.[key] || []), val];
|
||||
// }
|
||||
// single select
|
||||
if (selectedFilters && isActiveFilterValue(key, val)) {
|
||||
updatedFilters = {};
|
||||
} else {
|
||||
updatedFilters = {
|
||||
[key]: [val],
|
||||
};
|
||||
}
|
||||
if (
|
||||
Array.isArray(updatedFilters[key]) &&
|
||||
updatedFilters[key].length === 0
|
||||
) {
|
||||
delete updatedFilters[key];
|
||||
}
|
||||
handleChange(updatedFilters);
|
||||
},
|
||||
[emitFilter, selectedFilters, handleChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Styles height={height} width={width} margin={theme.gridUnit * 4}>
|
||||
<PivotTableWrapper>
|
||||
<PivotTable
|
||||
data={unpivotedData}
|
||||
rows={rows}
|
||||
cols={cols}
|
||||
aggregatorsFactory={aggregatorsFactory}
|
||||
defaultFormatter={defaultFormatter}
|
||||
customFormatters={
|
||||
hasCustomMetricFormatters
|
||||
? { [METRIC_KEY]: metricFormatters }
|
||||
: undefined
|
||||
}
|
||||
aggregatorName={aggregateFunction}
|
||||
vals={['value']}
|
||||
rendererName="Table With Subtotal"
|
||||
colOrder={colOrder}
|
||||
rowOrder={rowOrder}
|
||||
sorters={{
|
||||
metric: sortAs(metricNames),
|
||||
}}
|
||||
tableOptions={{
|
||||
clickRowHeaderCallback: toggleFilter,
|
||||
clickColumnHeaderCallback: toggleFilter,
|
||||
colTotals,
|
||||
rowTotals,
|
||||
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}
|
||||
/>
|
||||
</PivotTableWrapper>
|
||||
</Styles>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 387 KiB |
@@ -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
|
||||
*/
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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,
|
||||
getMetricLabel,
|
||||
normalizeOrderBy,
|
||||
QueryFormColumn,
|
||||
} from '@superset-ui/core';
|
||||
import { PivotTableQueryFormData } from '../types';
|
||||
|
||||
export default function buildQuery(formData: PivotTableQueryFormData) {
|
||||
const {
|
||||
groupbyColumns = [],
|
||||
groupbyRows = [],
|
||||
order_desc = true,
|
||||
legacy_order_by,
|
||||
} = formData;
|
||||
// TODO: add deduping of AdhocColumns
|
||||
const groupbySet = new Set([
|
||||
...ensureIsArray<QueryFormColumn>(groupbyColumns),
|
||||
...ensureIsArray<QueryFormColumn>(groupbyRows),
|
||||
]);
|
||||
return buildQueryContext(formData, baseQueryObject => {
|
||||
const queryObject = normalizeOrderBy({
|
||||
...baseQueryObject,
|
||||
order_desc,
|
||||
legacy_order_by,
|
||||
});
|
||||
const { metrics } = queryObject;
|
||||
const orderBy = ensureIsArray(legacy_order_by);
|
||||
if (
|
||||
orderBy.length &&
|
||||
!metrics?.find(
|
||||
metric => getMetricLabel(metric) === getMetricLabel(orderBy[0]),
|
||||
)
|
||||
) {
|
||||
metrics?.push(orderBy[0]);
|
||||
}
|
||||
return [
|
||||
{
|
||||
...queryObject,
|
||||
columns: [...groupbySet],
|
||||
metrics,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* 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 {
|
||||
QueryFormMetric,
|
||||
smartDateFormatter,
|
||||
t,
|
||||
validateNonEmpty,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
formatSelectOptions,
|
||||
sections,
|
||||
sharedControls,
|
||||
emitFilterControl,
|
||||
legacySortBy,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { MetricsLayoutEnum } from '../types';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{ ...sections.legacyTimeseriesTime, expanded: false },
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'groupbyRows',
|
||||
config: {
|
||||
...sharedControls.groupby,
|
||||
label: t('Rows'),
|
||||
description: t('Columns to group by on the rows'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'groupbyColumns',
|
||||
config: {
|
||||
...sharedControls.groupby,
|
||||
label: t('Columns'),
|
||||
description: t('Columns to group by on the columns'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'metrics',
|
||||
config: {
|
||||
...sharedControls.metrics,
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'metricsLayout',
|
||||
config: {
|
||||
type: 'RadioButtonControl',
|
||||
renderTrigger: true,
|
||||
label: t('Apply metrics on'),
|
||||
default: MetricsLayoutEnum.COLUMNS,
|
||||
options: [
|
||||
[MetricsLayoutEnum.COLUMNS, t('Columns')],
|
||||
[MetricsLayoutEnum.ROWS, t('Rows')],
|
||||
],
|
||||
description: t(
|
||||
'Use metrics as a top level group for columns or for rows',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
['adhoc_filters'],
|
||||
emitFilterControl,
|
||||
[
|
||||
{
|
||||
name: 'row_limit',
|
||||
config: {
|
||||
...sharedControls.row_limit,
|
||||
},
|
||||
},
|
||||
],
|
||||
...legacySortBy,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Options'),
|
||||
expanded: true,
|
||||
tabOverride: 'data',
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'aggregateFunction',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Aggregation function'),
|
||||
clearable: false,
|
||||
choices: formatSelectOptions([
|
||||
'Count',
|
||||
'Count Unique Values',
|
||||
'List Unique Values',
|
||||
'Sum',
|
||||
'Average',
|
||||
'Median',
|
||||
'Sample Variance',
|
||||
'Sample Standard Deviation',
|
||||
'Minimum',
|
||||
'Maximum',
|
||||
'First',
|
||||
'Last',
|
||||
'Sum as Fraction of Total',
|
||||
'Sum as Fraction of Rows',
|
||||
'Sum as Fraction of Columns',
|
||||
'Count as Fraction of Total',
|
||||
'Count as Fraction of Rows',
|
||||
'Count as Fraction of Columns',
|
||||
]),
|
||||
default: 'Sum',
|
||||
description: t(
|
||||
'Aggregate function to apply when pivoting and computing the total rows and columns',
|
||||
),
|
||||
renderTrigger: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'rowTotals',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show rows total'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
description: t('Display row level total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'colTotals',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show columns total'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
description: t('Display column level total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'transposePivot',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Transpose pivot'),
|
||||
default: false,
|
||||
description: t('Swap rows and columns'),
|
||||
renderTrigger: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'valueFormat',
|
||||
config: {
|
||||
...sharedControls.y_axis_format,
|
||||
label: t('Value format'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'date_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Date format'),
|
||||
default: smartDateFormatter.id,
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: t('D3 time format for datetime columns'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'rowOrder',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Rows sort by'),
|
||||
default: 'key_a_to_z',
|
||||
choices: [
|
||||
// [value, label]
|
||||
['key_a_to_z', t('key a-z')],
|
||||
['key_z_to_a', t('key z-a')],
|
||||
['value_a_to_z', t('value ascending')],
|
||||
['value_z_to_a', t('value descending')],
|
||||
],
|
||||
renderTrigger: true,
|
||||
description: t('Order of rows'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'colOrder',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Cols sort by'),
|
||||
default: 'key_a_to_z',
|
||||
choices: [
|
||||
// [value, label]
|
||||
['key_a_to_z', t('key a-z')],
|
||||
['key_z_to_a', t('key z-a')],
|
||||
['value_a_to_z', t('value ascending')],
|
||||
['value_z_to_a', t('value descending')],
|
||||
],
|
||||
renderTrigger: true,
|
||||
description: t('Order of columns'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'rowSubtotalPosition',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Rows subtotal position'),
|
||||
default: false,
|
||||
choices: [
|
||||
// [value, label]
|
||||
[true, t('Top')],
|
||||
[false, t('Bottom')],
|
||||
],
|
||||
renderTrigger: true,
|
||||
description: t('Position of row level subtotal'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'colSubtotalPosition',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Cols subtotal position'),
|
||||
default: false,
|
||||
choices: [
|
||||
// [value, label]
|
||||
[true, t('Left')],
|
||||
[false, t('Right')],
|
||||
],
|
||||
renderTrigger: true,
|
||||
description: t('Position of column level subtotal'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'conditional_formatting',
|
||||
config: {
|
||||
type: 'ConditionalFormattingControl',
|
||||
renderTrigger: true,
|
||||
label: t('Conditional formatting'),
|
||||
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,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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 {
|
||||
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,
|
||||
ChartProps<QueryFormData>
|
||||
> {
|
||||
/**
|
||||
* The constructor is used to pass relevant metadata and callbacks that get
|
||||
* registered in respective registries that are used throughout the library
|
||||
* and application. A more thorough description of each property is given in
|
||||
* the respective imported file.
|
||||
*
|
||||
* It is worth noting that `buildQuery` and is optional, and only needed for
|
||||
* advanced visualizations that require either post processing operations
|
||||
* (pivoting, rolling aggregations, sorting etc) or submitting multiple queries.
|
||||
*/
|
||||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
||||
category: t('Table'),
|
||||
description: t(
|
||||
'Used to summarize a set of data by grouping together multiple statistics along two axes. Examples: Sales numbers by region and month, tasks by status and assignee, active users by age and location. Not the most visually stunning visualization, but highly informative and versatile.',
|
||||
),
|
||||
name: t('Pivot Table v2'),
|
||||
tags: [t('Additive'), t('Report'), t('Tabular'), t('Popular')],
|
||||
thumbnail,
|
||||
});
|
||||
|
||||
super({
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
loadChart: () => import('../PivotTableChart'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* 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 {
|
||||
ChartProps,
|
||||
DataRecord,
|
||||
extractTimegrain,
|
||||
GenericDataType,
|
||||
getTimeFormatter,
|
||||
getTimeFormatterForGranularity,
|
||||
QueryFormData,
|
||||
smartDateFormatter,
|
||||
TimeFormats,
|
||||
} from '@superset-ui/core';
|
||||
import { getColorFormatters } from '@superset-ui/chart-controls';
|
||||
import { DateFormatter } from '../types';
|
||||
|
||||
const { DATABASE_DATETIME } = TimeFormats;
|
||||
|
||||
function isNumeric(key: string, data: DataRecord[] = []) {
|
||||
return data.every(
|
||||
record =>
|
||||
record[key] === null ||
|
||||
record[key] === undefined ||
|
||||
typeof record[key] === 'number',
|
||||
);
|
||||
}
|
||||
|
||||
export default function transformProps(chartProps: ChartProps<QueryFormData>) {
|
||||
/**
|
||||
* This function is called after a successful response has been
|
||||
* received from the chart data endpoint, and is used to transform
|
||||
* the incoming data prior to being sent to the Visualization.
|
||||
*
|
||||
* The transformProps function is also quite useful to return
|
||||
* additional/modified props to your data viz component. The formData
|
||||
* can also be accessed from your PivotTableChart.tsx file, but
|
||||
* doing supplying custom props here is often handy for integrating third
|
||||
* party libraries that rely on specific props.
|
||||
*
|
||||
* A description of properties in `chartProps`:
|
||||
* - `height`, `width`: the height/width of the DOM element in which
|
||||
* the chart is located
|
||||
* - `formData`: the chart data request payload that was sent to the
|
||||
* backend.
|
||||
* - `queriesData`: the chart data response payload that was received
|
||||
* from the backend. Some notable properties of `queriesData`:
|
||||
* - `data`: an array with data, each row with an object mapping
|
||||
* the column/alias to its value. Example:
|
||||
* `[{ col1: 'abc', metric1: 10 }, { col1: 'xyz', metric1: 20 }]`
|
||||
* - `rowcount`: the number of rows in `data`
|
||||
* - `query`: the query that was issued.
|
||||
*
|
||||
* Please note: the transformProps function gets cached when the
|
||||
* application loads. When making changes to the `transformProps`
|
||||
* function during development with hot reloading, changes won't
|
||||
* be seen until restarting the development server.
|
||||
*/
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
queriesData,
|
||||
formData,
|
||||
rawFormData,
|
||||
hooks: { setDataMask = () => {} },
|
||||
filterState,
|
||||
datasource: { verboseMap = {}, columnFormats = {} },
|
||||
} = chartProps;
|
||||
const { data, colnames, coltypes } = queriesData[0];
|
||||
const {
|
||||
groupbyRows,
|
||||
groupbyColumns,
|
||||
metrics,
|
||||
tableRenderer,
|
||||
colOrder,
|
||||
rowOrder,
|
||||
aggregateFunction,
|
||||
transposePivot,
|
||||
combineMetric,
|
||||
rowSubtotalPosition,
|
||||
colSubtotalPosition,
|
||||
colTotals,
|
||||
rowTotals,
|
||||
valueFormat,
|
||||
dateFormat,
|
||||
emitFilter,
|
||||
metricsLayout,
|
||||
conditionalFormatting,
|
||||
} = formData;
|
||||
const { selectedFilters } = filterState;
|
||||
const granularity = extractTimegrain(rawFormData);
|
||||
|
||||
const dateFormatters = colnames
|
||||
.filter(
|
||||
(colname: string, index: number) =>
|
||||
coltypes[index] === GenericDataType.TEMPORAL,
|
||||
)
|
||||
.reduce(
|
||||
(
|
||||
acc: Record<string, DateFormatter | undefined>,
|
||||
temporalColname: string,
|
||||
) => {
|
||||
let formatter: DateFormatter | undefined;
|
||||
if (dateFormat === smartDateFormatter.id) {
|
||||
if (granularity) {
|
||||
// time column use formats based on granularity
|
||||
formatter = getTimeFormatterForGranularity(granularity);
|
||||
} else if (isNumeric(temporalColname, data)) {
|
||||
formatter = getTimeFormatter(DATABASE_DATETIME);
|
||||
} else {
|
||||
// if no column-specific format, print cell as is
|
||||
formatter = String;
|
||||
}
|
||||
} else if (dateFormat) {
|
||||
formatter = getTimeFormatter(dateFormat);
|
||||
}
|
||||
if (formatter) {
|
||||
acc[temporalColname] = formatter;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const metricColorFormatters = getColorFormatters(conditionalFormatting, data);
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
groupbyRows,
|
||||
groupbyColumns,
|
||||
metrics,
|
||||
tableRenderer,
|
||||
colOrder,
|
||||
rowOrder,
|
||||
aggregateFunction,
|
||||
transposePivot,
|
||||
combineMetric,
|
||||
rowSubtotalPosition,
|
||||
colSubtotalPosition,
|
||||
colTotals,
|
||||
rowTotals,
|
||||
valueFormat,
|
||||
emitFilter,
|
||||
setDataMask,
|
||||
selectedFilters,
|
||||
verboseMap,
|
||||
columnFormats,
|
||||
metricsLayout,
|
||||
metricColorFormatters,
|
||||
dateFormatters,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 {
|
||||
QueryFormData,
|
||||
DataRecord,
|
||||
SetDataMaskHook,
|
||||
DataRecordValue,
|
||||
JsonObject,
|
||||
TimeFormatter,
|
||||
NumberFormatter,
|
||||
QueryFormMetric,
|
||||
QueryFormColumn,
|
||||
} from '@superset-ui/core';
|
||||
import { ColorFormatters } from '@superset-ui/chart-controls';
|
||||
|
||||
export interface PivotTableStylesProps {
|
||||
height: number;
|
||||
width: number | string;
|
||||
margin: number;
|
||||
}
|
||||
|
||||
export type FilterType = Record<string, DataRecordValue>;
|
||||
export type SelectedFiltersType = Record<string, DataRecordValue[]>;
|
||||
|
||||
export type DateFormatter =
|
||||
| TimeFormatter
|
||||
| NumberFormatter
|
||||
| ((value: DataRecordValue) => string);
|
||||
export enum MetricsLayoutEnum {
|
||||
ROWS = 'ROWS',
|
||||
COLUMNS = 'COLUMNS',
|
||||
}
|
||||
|
||||
interface PivotTableCustomizeProps {
|
||||
groupbyRows: QueryFormColumn[];
|
||||
groupbyColumns: QueryFormColumn[];
|
||||
metrics: QueryFormMetric[];
|
||||
tableRenderer: string;
|
||||
colOrder: string;
|
||||
rowOrder: string;
|
||||
aggregateFunction: string;
|
||||
transposePivot: boolean;
|
||||
combineMetric: boolean;
|
||||
rowSubtotalPosition: boolean;
|
||||
colSubtotalPosition: boolean;
|
||||
colTotals: boolean;
|
||||
rowTotals: boolean;
|
||||
valueFormat: string;
|
||||
setDataMask: SetDataMaskHook;
|
||||
emitFilter?: boolean;
|
||||
selectedFilters?: SelectedFiltersType;
|
||||
verboseMap: JsonObject;
|
||||
columnFormats: JsonObject;
|
||||
metricsLayout?: MetricsLayoutEnum;
|
||||
metricColorFormatters: ColorFormatters;
|
||||
dateFormatters: Record<string, DateFormatter | undefined>;
|
||||
legacy_order_by: QueryFormMetric[] | QueryFormMetric | null;
|
||||
order_desc: boolean;
|
||||
}
|
||||
|
||||
export type PivotTableQueryFormData = QueryFormData &
|
||||
PivotTableStylesProps &
|
||||
PivotTableCustomizeProps;
|
||||
|
||||
export type PivotTableProps = PivotTableStylesProps &
|
||||
PivotTableCustomizeProps & {
|
||||
data: DataRecord[];
|
||||
};
|
||||
Reference in New Issue
Block a user