Files
superset2/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts
T

267 lines
8.4 KiB
TypeScript

/**
* 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,
ensureIsArray,
extractTimegrain,
getColumnLabel,
getMetricLabel,
getTimeFormatter,
getTimeFormatterForGranularity,
QueryFormData,
SMART_DATE_ID,
TimeFormats,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import {
ColorSchemeEnum,
ConditionalFormattingConfig,
getColorFormatters,
} from '@superset-ui/chart-controls';
import { DateFormatter, PivotTableQueryFormData, QueryData } from '../types';
import buildGroupbyCombinations, {
additiveReducerFor,
allMetricsAdditive,
RollupReducer,
splitGroupingSetsResult,
synthesizeAdditiveLevels,
} from './utilities';
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 = () => {}, onContextMenu },
filterState,
datasource: {
verboseMap = {},
columnFormats = {},
currencyFormats = {},
currencyCodeColumn,
},
emitCrossFilters,
theme,
} = chartProps;
const groupbyCombinations = buildGroupbyCombinations(
formData as PivotTableQueryFormData,
);
const metricsArr = ensureIsArray(formData.metrics);
let data: QueryData[];
if (allMetricsAdditive(metricsArr)) {
// Additive fast-path: a single full-detail query was issued; synthesize
// each rollup level by reducing the leaf rows on the client (see SIP.md).
const leafRows = queriesData[0].data;
const metricReducers: Record<string, RollupReducer> = {};
metricsArr.forEach(metric => {
metricReducers[getMetricLabel(metric)] = additiveReducerFor(metric);
});
const labelLevels = groupbyCombinations.map(combination => ({
rows: combination.rows.map(getColumnLabel),
columns: combination.columns.map(getColumnLabel),
}));
const synthesized = synthesizeAdditiveLevels(
leafRows,
labelLevels,
metricReducers,
);
data = groupbyCombinations.map((combination, i) => ({
data: synthesized[i] as DataRecord[],
groupby: combination,
}));
} else {
// Non-additive: a single GROUPING SETS query returned all rollup levels
// tagged with GROUPING() markers; split the combined result back into one
// frame per level (same order as buildQuery's grouping_sets). See SIP.md.
const levelLabels = groupbyCombinations.map(combination =>
[...combination.rows, ...combination.columns].map(getColumnLabel),
);
const allGroupbyLabels = Array.from(new Set(levelLabels.flat()));
const splitRows = splitGroupingSetsResult(
queriesData[0].data,
levelLabels,
allGroupbyLabels,
);
data = groupbyCombinations.map((combination, i) => ({
data: splitRows[i] as DataRecord[],
groupby: combination,
}));
}
// The full-granularity query has the most colnames -- use it for column/type
// metadata and formatters.
const mainQuery = queriesData.reduce((main, query) =>
query.colnames.length > main.colnames.length ? query : main,
);
const { colnames, coltypes, detected_currency: detectedCurrency } = mainQuery;
const {
groupbyRows,
groupbyColumns,
metrics,
tableRenderer,
colOrder,
rowOrder,
transposePivot,
combineMetric,
rowSubtotalPosition,
colSubtotalPosition,
colTotals,
colSubTotals,
rowTotals,
rowSubTotals,
valueFormat,
dateFormat,
metricsLayout,
showValuesAs,
conditionalFormatting,
timeGrainSqla,
currencyFormat,
allowRenderHtml,
} = 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 === SMART_DATE_ID) {
if (granularity) {
// time column use formats based on granularity
formatter = getTimeFormatterForGranularity(granularity);
} else if (isNumeric(temporalColname, mainQuery.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;
},
{},
);
// The "Green"/"Red" trend-color tokens are resolved by the Table chart's
// own comparison-aware formatter, which this renderer does not implement.
// Filter them out so a stale config (e.g. carried over from switching viz
// types) doesn't leak the raw token name through as a literal CSS color.
const pivotConditionalFormatting = conditionalFormatting?.filter(
(config: ConditionalFormattingConfig) =>
config.colorScheme !== ColorSchemeEnum.Green &&
config.colorScheme !== ColorSchemeEnum.Red,
);
const metricColorFormatters = getColorFormatters(
pivotConditionalFormatting,
mainQuery.data,
theme,
);
// AUTO symbol passed through - PivotTableChart handles per-cell currency detection
return {
width,
height,
data,
groupbyRows,
groupbyColumns,
metrics,
tableRenderer,
colOrder,
rowOrder,
transposePivot,
combineMetric,
rowSubtotalPosition,
colSubtotalPosition,
colTotals,
colSubTotals,
rowTotals,
rowSubTotals,
valueFormat,
currencyFormat,
currencyCodeColumn,
detectedCurrency,
emitCrossFilters,
setDataMask,
selectedFilters,
verboseMap,
columnFormats,
currencyFormats,
metricsLayout,
showValuesAs,
metricColorFormatters,
dateFormatters,
onContextMenu,
timeGrainSqla,
allowRenderHtml,
};
}