/** * 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) { /** * 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 = {}; 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, 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, }; }