mirror of
https://github.com/apache/superset.git
synced 2026-08-31 04:21:16 +00:00
284 lines
9.4 KiB
TypeScript
284 lines
9.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[];
|
|
// The rows that conditional formatting derives its color scale from. Only the
|
|
// leaf (detail) cells belong in that domain: the totals are aggregates of the
|
|
// very cells being shaded, so letting them in makes the grand total the max
|
|
// and leaves every detail cell nearly unshaded.
|
|
let colorScaleRows: DataRecord[];
|
|
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,
|
|
}));
|
|
// The query returned the leaf rows and nothing else, so no totals can leak
|
|
// into the domain. Use those raw rows rather than the synthesized leaf
|
|
// level, whose reduction coerces values through `Number` and drops
|
|
// non-numeric ones -- that would shift the domain for additive metrics.
|
|
colorScaleRows = leafRows;
|
|
} 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,
|
|
}));
|
|
// This result *does* carry the rollup levels, so pick out the leaf level --
|
|
// the one grouping every dimension, same definition the splitter uses.
|
|
const leafIndex = levelLabels.findIndex(labels => {
|
|
const grouped = new Set(labels);
|
|
return allGroupbyLabels.every(label => grouped.has(label));
|
|
});
|
|
colorScaleRows = (splitRows[leafIndex] as DataRecord[]) ?? [];
|
|
}
|
|
// 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,
|
|
colorScaleRows,
|
|
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,
|
|
};
|
|
}
|