Compare commits

...

1 Commits

Author SHA1 Message Date
Aleksandr Lesnenko
d82e8722a0 wip 2024-07-11 23:34:26 -04:00
28 changed files with 521 additions and 480 deletions

View File

@@ -120,8 +120,6 @@ export type SingleSeries = {
} & Pick<Dataset, "data" | "error">;
export type RawSeries = SingleSeries[];
export type TransformedSeries = RawSeries & { _raw: Series };
export type Series = RawSeries | TransformedSeries;
export type TemplateTagId = string;
export type TemplateTagName = string;

View File

@@ -12,7 +12,7 @@ import * as Lib from "metabase-lib";
import { MetabaseApi } from "metabase/services";
import {
extractRemappings,
getVisualizationTransformed,
getVisualizationDefinition,
} from "metabase/visualizations";
import { getComputedSettingsForSeries } from "metabase/visualizations/lib/settings/visualization";
@@ -717,7 +717,7 @@ export const getRawSeries = createSelector(
const _getVisualizationTransformed = createSelector(
[getRawSeries],
rawSeries =>
rawSeries && getVisualizationTransformed(extractRemappings(rawSeries)),
rawSeries && getVisualizationDefinition(extractRemappings(rawSeries)),
);
/**

View File

@@ -185,8 +185,7 @@ export const computeStaticComboChartSettings = (
);
const seriesDisplays = seriesModels.map(
seriesModel =>
settings.series(seriesModel.legacySeriesSettingsObjectKey).display,
seriesModel => settings.series(seriesModel).display,
);
fillWithDefaultValue(

View File

@@ -3,16 +3,12 @@ import { useCallback } from "react";
import type { IconProps } from "metabase/ui";
import type { OnChangeCardAndRun } from "metabase/visualizations/types";
import type {
Series,
TransformedSeries,
VisualizationSettings,
} from "metabase-types/api";
import type { RawSeries, VisualizationSettings } from "metabase-types/api";
import { ChartCaptionRoot } from "./ChartCaption.styled";
interface ChartCaptionProps {
series: Series;
series: RawSeries;
settings: VisualizationSettings;
icon?: IconProps;
actionButtons?: ReactNode;
@@ -32,9 +28,8 @@ const ChartCaption = ({
}: ChartCaptionProps) => {
const title = settings["card.title"] ?? series[0].card.name;
const description = settings["card.description"];
const data = (series as TransformedSeries)._raw || series;
const card = data[0].card;
const cardIds = new Set(data.map(s => s.card.id));
const card = series[0].card;
const cardIds = new Set(series.map(s => s.card.id));
const canSelectTitle = cardIds.size === 1 && onChangeCardAndRun;
const handleSelectTitle = useCallback(() => {

View File

@@ -9,10 +9,7 @@ import Button from "metabase/core/components/Button";
import Radio from "metabase/core/components/Radio";
import CS from "metabase/css/core/index.css";
import * as MetabaseAnalytics from "metabase/lib/analytics";
import {
getVisualizationTransformed,
extractRemappings,
} from "metabase/visualizations";
import { extractRemappings } from "metabase/visualizations";
import Visualization from "metabase/visualizations/components/Visualization";
import { updateSeriesColor } from "metabase/visualizations/lib/series";
import {
@@ -22,8 +19,10 @@ import {
getSettingsWidgets,
} from "metabase/visualizations/lib/settings";
import { getSettingDefinitionsForColumn } from "metabase/visualizations/lib/settings/column";
import { keyForSingleSeries } from "metabase/visualizations/lib/settings/series";
import { getSettingsWidgetsForSeries } from "metabase/visualizations/lib/settings/visualization";
import {
getSettingsWidgetsForSeries,
getSettingsModelForSeries,
} from "metabase/visualizations/lib/settings/visualization";
import { getColumnKey } from "metabase-lib/v1/queries/utils/get-column-key";
import {
@@ -162,11 +161,13 @@ class ChartSettings extends Component {
} else {
const { isDashboard, dashboard } = this.props;
const transformedSeries = this._getTransformedSeries();
const settingsModel = this._getSettingsModel();
return getSettingsWidgetsForSeries(
transformedSeries,
this.handleChangeSettings,
isDashboard,
settingsModel,
{ dashboardId: dashboard?.id },
);
}
@@ -185,16 +186,20 @@ class ChartSettings extends Component {
}
_getTransformedSeries() {
const rawSeries = this._getRawSeries();
const { series: transformedSeries } = getVisualizationTransformed(
extractRemappings(rawSeries),
);
return transformedSeries;
return extractRemappings(rawSeries);
}
_getSettingsModel() {
const { series } = this.props;
const settings = this._getSettings() || {};
return getSettingsModelForSeries(series, settings);
}
columnHasSettings(col) {
const { series } = this.props;
const settings = this._getSettings() || {};
const settingsDefs = getSettingDefinitionsForColumn(series, col);
const settingsModel = this._getSettingsModel();
const computedSettings = getComputedSettings(settingsDefs, col, settings);
return getSettingsWidgets(
@@ -203,6 +208,7 @@ class ChartSettings extends Component {
computedSettings,
col,
_.noop,
settingsModel,
{
series,
},
@@ -243,15 +249,16 @@ class ChartSettings extends Component {
}
});
if (singleSeriesForColumn) {
return {
...seriesSettingsWidget,
props: {
...seriesSettingsWidget.props,
initialKey: keyForSingleSeries(singleSeriesForColumn),
},
};
}
// TODO: fix and uncomment
// if (singleSeriesForColumn) {
// return {
// ...seriesSettingsWidget,
// props: {
// ...seriesSettingsWidget.props,
// initialKey: keyForSingleSeries(singleSeriesForColumn),
// },
// };
// }
}
return null;

View File

@@ -50,6 +50,7 @@ function getWidgets({
onChangeSetting(changedSettings);
}
},
null,
{ series, ...extraData },
);

View File

@@ -19,7 +19,7 @@ import { getIsEmbeddingSdk } from "metabase/selectors/embed";
import { getFont } from "metabase/styled-components/selectors";
import {
extractRemappings,
getVisualizationTransformed,
getVisualizationDefinition,
} from "metabase/visualizations";
import { Mode } from "metabase/visualizations/click-actions/Mode";
import { getMode } from "metabase/visualizations/click-actions/lib/modes";
@@ -145,10 +145,10 @@ class Visualization extends PureComponent {
transform(newProps) {
const transformed = newProps.rawSeries
? getVisualizationTransformed(extractRemappings(newProps.rawSeries))
? getVisualizationDefinition(extractRemappings(newProps.rawSeries))
: null;
const series = transformed && transformed.series;
const visualization = transformed && transformed.visualization;
const series = transformed?.series;
const visualization = transformed?.visualization;
const computedSettings = !this.isLoading(series)
? getComputedSettingsForSeries(series)
: {};

View File

@@ -2,7 +2,6 @@
import { t } from "ttag";
import _ from "underscore";
import { keyForSingleSeries } from "metabase/visualizations/lib/settings/series";
import { getColumnKey } from "metabase-lib/v1/queries/utils/get-column-key";
import {
@@ -43,9 +42,10 @@ const ChartSettingFieldPicker = ({
const metricColumn = single.data.cols[1];
return getColumnKey(metricColumn) === columnKey;
});
if (seriesForColumn) {
seriesKey = keyForSingleSeries(seriesForColumn);
}
// TODO: fix and uncomment
// if (seriesForColumn) {
// seriesKey = keyForSingleSeries(seriesForColumn);
// }
}
return (
<ChartSettingFieldPickerRoot

View File

@@ -67,7 +67,7 @@ const chartSettingNestedSettings =
};
render() {
const { series, objects, extra } = this.props;
const { series, objects, extra, settingsModel } = this.props;
const editingObjectKey = this.getEditingObjectKey();
if (editingObjectKey !== undefined) {
const editingObject = _.find(
@@ -82,6 +82,7 @@ const chartSettingNestedSettings =
editingObject,
objectSettings,
this.handleChangeSettingsForEditingObject,
settingsModel,
extra,
);
return (

View File

@@ -418,9 +418,7 @@ export const getChartPadding = (
// We handle non-categorical scatter plots differently, because echarts places
// the tick labels on the very edge of the x-axis for scatter plots only.
const isScatterPlot = chartModel.seriesModels.some(seriesModel => {
const seriesSettings = settings.series(
seriesModel.legacySeriesSettingsObjectKey,
);
const seriesSettings = settings.series(seriesModel);
return seriesSettings.display === "scatter";
});
if (isScatterPlot && chartModel.xAxisModel.axisType !== "category") {

View File

@@ -284,9 +284,7 @@ const getYAxisSplit = (
}
const axisBySeriesKey = seriesModels.reduce((acc, seriesModel) => {
const seriesSettings: SeriesSettings = settings.series(
seriesModel.legacySeriesSettingsObjectKey,
);
const seriesSettings: SeriesSettings = settings.series(seriesModel);
const seriesStack = stackModels.find(stackModel =>
stackModel.seriesKeys.includes(seriesModel.dataKey),

View File

@@ -281,10 +281,7 @@ export const getNullReplacerTransform = (
): TransformFn => {
const replaceNullsWithZeroDataKeys = seriesModels
.filter(
seriesModel =>
settings.series(seriesModel.legacySeriesSettingsObjectKey)[
"line.missing"
] === "zero",
seriesModel => settings.series(seriesModel)["line.missing"] === "zero",
)
.map(seriesModel => seriesModel.dataKey);
@@ -302,9 +299,7 @@ const hasInterpolatedAreaSeries = (
settings: ComputedVisualizationSettings,
) => {
return seriesModels.some(seriesModel => {
const seriesSettings = settings.series(
seriesModel.legacySeriesSettingsObjectKey,
);
const seriesSettings = settings.series(seriesModel);
return (
seriesSettings["line.missing"] !== "none" &&
seriesSettings.display === "area"
@@ -700,9 +695,7 @@ export const applyVisualizationSettingsDataTransformations = (
showWarning?: ShowWarning,
) => {
const barSeriesModels = seriesModels.filter(seriesModel => {
const seriesSettings = settings.series(
seriesModel.legacySeriesSettingsObjectKey,
);
const seriesSettings = settings.series(seriesModel);
return seriesSettings.display === "bar";
});
const seriesDataKeys = seriesModels.map(seriesModel => seriesModel.dataKey);

View File

@@ -121,7 +121,7 @@ export const getCardsSeriesModels = (
rawSeries: RawSeries,
cardsColumns: CartesianChartColumns[],
settings: ComputedVisualizationSettings,
renderingContext: RenderingContext,
renderingContext: Pick<RenderingContext, "formatValue">,
) => {
const hasMultipleCards = rawSeries.length > 1;
return rawSeries.flatMap((cardDataset, index) => {
@@ -155,7 +155,7 @@ export const getCardSeriesModels = (
hasMultipleCards: boolean,
isFirstCard: boolean,
settings: ComputedVisualizationSettings,
renderingContext: RenderingContext,
renderingContext: Pick<RenderingContext, "formatValue">,
): SeriesModel[] => {
const cardId = card.id ?? null;
const hasBreakout = "breakout" in columns;
@@ -535,9 +535,7 @@ export function getDisplaySeriesSettingsByDataKey(
settings: ComputedVisualizationSettings,
) {
const seriesSettingsByKey = seriesModels.reduce((acc, seriesModel) => {
acc[seriesModel.dataKey] = settings.series(
seriesModel.legacySeriesSettingsObjectKey,
);
acc[seriesModel.dataKey] = settings.series(seriesModel);
return acc;
}, {} as Record<DataKey, SeriesSettings>);
@@ -700,8 +698,7 @@ const getSeriesLabelsFormatters = (
}
const seriesModelsWithLabels = seriesModels.filter(seriesModel => {
const seriesSettings =
settings.series(seriesModel.legacySeriesSettingsObjectKey) ?? {};
const seriesSettings = settings.series(seriesModel) ?? {};
return !!seriesSettings["show_series_values"];
});

View File

@@ -15,8 +15,7 @@ export const getStackModels = (
const seriesModelsByDisplay = _.groupBy(
seriesModels,
seriesModel =>
settings.series(seriesModel.legacySeriesSettingsObjectKey).display,
seriesModel => settings.series(seriesModel).display,
);
return getObjectKeys(seriesModelsByDisplay)
@@ -29,9 +28,7 @@ export const getStackModels = (
axis = "left";
} else {
axis = stackSeriesModels.every(
seriesModel =>
settings.series(seriesModel.legacySeriesSettingsObjectKey)?.axis ===
"right",
seriesModel => settings.series(seriesModel)?.axis === "right",
)
? "right"
: "left";

View File

@@ -1,11 +1,7 @@
import { t } from "ttag";
import _ from "underscore";
import type {
DatasetData,
Series,
TransformedSeries,
} from "metabase-types/api";
import type { DatasetData, RawSeries } from "metabase-types/api";
import type { RemappingHydratedDatasetColumn } from "./types";
import type { Visualization } from "./types/visualization";
@@ -64,35 +60,8 @@ export function getVisualizationRaw(series: SeriesLike) {
return visualizations.get(series[0].card.display);
}
export function getVisualizationTransformed(series: TransformedSeries) {
// don't transform if we don't have the data
if (
_.any(series, s => s.data == null) ||
_.any(series, s => s.error != null)
) {
return {
series,
visualization: getVisualizationRaw(series),
};
}
// if a visualization has a transformSeries function, do the transformation until it returns the same visualization / series
let visualization, lastSeries;
do {
visualization = visualizations.get(series[0].card.display);
if (!visualization) {
throw new Error(t`No visualization for ${series[0].card.display}`);
}
lastSeries = series;
if (typeof visualization.transformSeries === "function") {
series = visualization.transformSeries(series);
}
if (series !== lastSeries) {
series = Object.assign([...series], { _raw: lastSeries });
}
} while (series !== lastSeries);
return { series, visualization };
export function getVisualizationDefinition(series: RawSeries) {
return { series, visualization: visualizations.get(series[0].card.display) };
}
export function getIconForVisualizationType(display: string) {
@@ -100,7 +69,7 @@ export function getIconForVisualizationType(display: string) {
return viz?.iconName ?? "unknown";
}
export const extractRemappings = (series: Series) => {
export const extractRemappings = (series: RawSeries) => {
const se = series.map(s => ({
...s,
data: s.data && extractRemappedColumns(s.data),

View File

@@ -54,12 +54,11 @@ function canDisplayNull(settings) {
export function getXValues({ settings, series }) {
// if _raw isn't set then we already have the raw series
const { _raw: rawSeries = series } = series;
const warn = () => {}; // no op since warning in handled by getDatas
const uniqueValues = new Set();
let isAscending = true;
let isDescending = true;
for (const { data } of rawSeries) {
for (const { data } of series) {
// In the raw series, the dimension isn't necessarily in the first element
// of each row. This finds the correct column index.
const columnIndex = getColumnIndex({ settings, data });

View File

@@ -30,11 +30,17 @@ const WIDGETS = {
colors: ChartSettingColorsPicker,
};
export function getComputedSettings(
settingsDefs,
object,
storedSettings,
extra = {},
export function getComputedSettings<
TObject,
TObjectSettings,
TSettingsModel,
TExtra,
>(
settingsDefs: $TODO,
object: TObject,
storedSettings: TObjectSettings,
settingsModel: TSettingsModel,
extra: TExtra = {},
) {
const computedSettings = {};
for (const settingId in settingsDefs) {
@@ -44,6 +50,7 @@ export function getComputedSettings(
settingId,
object,
storedSettings,
settingsModel,
extra,
);
}
@@ -56,6 +63,7 @@ function getComputedSetting(
settingId,
object,
storedSettings,
settingsModel,
extra = {},
) {
if (settingId in computedSettings) {
@@ -71,44 +79,55 @@ function getComputedSetting(
dependentId,
object,
storedSettings,
settingsModel,
extra,
);
}
if (settingDef.useRawSeries && object._raw) {
object = object._raw;
}
const settings = { ...storedSettings, ...computedSettings };
try {
if (settingDef.getValue) {
return (computedSettings[settingId] = settingDef.getValue(
computedSettings[settingId] = settingDef.getValue(
object,
settings,
settingsModel,
extra,
));
);
return;
}
if (storedSettings[settingId] !== undefined) {
if (!settingDef.isValid || settingDef.isValid(object, settings, extra)) {
return (computedSettings[settingId] = storedSettings[settingId]);
if (
!settingDef.isValid ||
settingDef.isValid(object, settings, settingsModel, extra)
) {
computedSettings[settingId] = storedSettings[settingId];
return;
}
}
if (settingDef.getDefault) {
const defaultValue = settingDef.getDefault(object, settings, extra);
const defaultValue = settingDef.getDefault(
object,
settings,
settingsModel,
extra,
);
return (computedSettings[settingId] = defaultValue);
computedSettings[settingId] = defaultValue;
return;
}
if ("default" in settingDef) {
return (computedSettings[settingId] = settingDef.default);
computedSettings[settingId] = settingDef.default;
return;
}
} catch (e) {
console.warn("Error getting setting", settingId, e);
}
return (computedSettings[settingId] = undefined);
computedSettings[settingId] = undefined;
}
function getSettingWidget(
@@ -118,6 +137,7 @@ function getSettingWidget(
computedSettings,
object,
onChangeSettings,
settingsModel,
extra = {},
) {
const settingDef = settingDefs[settingId];
@@ -133,33 +153,42 @@ function getSettingWidget(
onChangeSettings(newSettings, question);
settingDef.onUpdate?.(value, extra);
};
if (settingDef.useRawSeries && object._raw) {
extra.transformedSeries = object;
object = object._raw;
}
return {
...settingDef,
id: settingId,
value: value,
section: settingDef.getSection
? settingDef.getSection(object, computedSettings, extra)
? settingDef.getSection(object, computedSettings, settingsModel, extra)
: settingDef.section,
title: settingDef.getTitle
? settingDef.getTitle(object, computedSettings, extra)
? settingDef.getTitle(object, computedSettings, settingsModel, extra)
: settingDef.title,
hidden: settingDef.getHidden
? settingDef.getHidden(object, computedSettings, extra)
? settingDef.getHidden(object, computedSettings, settingsModel, extra)
: settingDef.hidden || false,
marginBottom: settingDef.getMarginBottom
? settingDef.getMarginBottom(object, computedSettings, extra)
? settingDef.getMarginBottom(
object,
computedSettings,
settingsModel,
extra,
)
: settingDef.marginBottom,
disabled: settingDef.getDisabled
? settingDef.getDisabled(object, computedSettings, extra)
? settingDef.getDisabled(object, computedSettings, settingsModel, extra)
: settingDef.disabled || false,
props: {
settingsModel,
...(settingDef.props ? settingDef.props : {}),
...(settingDef.getProps
? settingDef.getProps(object, computedSettings, onChange, extra)
? settingDef.getProps(
object,
computedSettings,
onChange,
settingsModel,
extra,
)
: {}),
},
set: settingId in storedSettings,
@@ -172,13 +201,19 @@ function getSettingWidget(
};
}
export function getSettingsWidgets(
settingDefs,
storedSettings,
computedSettings,
object,
onChangeSettings,
extra = {},
export function getSettingsWidgets<
TObject,
TObjectSettings,
TSettingsModel,
TExtra,
>(
settingDefs: $TODO,
storedSettings: TObjectSettings,
computedSettings: $TODO,
object: TObject,
onChangeSettings: $TODO,
settingsModel: TSettingsModel,
extra: TExtra = {},
) {
return Object.keys(settingDefs)
.map(settingId =>
@@ -189,6 +224,7 @@ export function getSettingsWidgets(
computedSettings,
object,
onChangeSettings,
settingsModel,
extra,
),
)

View File

@@ -8,10 +8,7 @@ import {
import { ChartSettingOrderedSimple } from "metabase/visualizations/components/settings/ChartSettingOrderedSimple";
import { dimensionIsNumeric } from "metabase/visualizations/lib/numeric";
import { columnSettings } from "metabase/visualizations/lib/settings/column";
import {
seriesSetting,
keyForSingleSeries,
} from "metabase/visualizations/lib/settings/series";
import { seriesSetting } from "metabase/visualizations/lib/settings/series";
import { getOptionFromColumn } from "metabase/visualizations/lib/settings/utils";
import { dimensionIsTimeseries } from "metabase/visualizations/lib/timeseries";
import { columnsAreValid, MAX_SERIES } from "metabase/visualizations/lib/utils";
@@ -47,8 +44,8 @@ import {
} from "metabase/visualizations/shared/settings/cartesian-chart";
import { isNumeric } from "metabase-lib/v1/types/utils/isa";
export const getSeriesDisplays = (transformedSeries, settings) => {
return transformedSeries.map(single => settings.series(single).display);
export const getSeriesDisplays = (seriesModels, settings) => {
return seriesModels.map(seriesModel => settings.series(seriesModel).display);
};
export function getDefaultDimensionLabel(multipleSeries) {
@@ -73,19 +70,17 @@ export const GRAPH_DATA_SETTINGS = {
}),
"graph._dimension_filter": {
getDefault: ([{ card }]) => getDefaultDimensionFilter(card.display),
useRawSeries: true,
},
"graph._metric_filter": {
getDefault: ([{ card }]) => getDefaultMetricFilter(card.display),
useRawSeries: true,
},
"graph.dimensions": {
section: t`Data`,
title: t`X-axis`,
widget: "fields",
getMarginBottom: (series, vizSettings) =>
getMarginBottom: (_series, vizSettings, settingsModel) =>
vizSettings["graph.dimensions"]?.length === 2 &&
series.length <= MAX_SERIES
settingsModel.seriesModels.length <= MAX_SERIES
? "0.5rem"
: "1rem",
isValid: (series, vizSettings) =>
@@ -120,7 +115,6 @@ export const GRAPH_DATA_SETTINGS = {
writeDependencies: ["graph.metrics"],
eraseDependencies: ["graph.series_order_dimension", "graph.series_order"],
dashboard: false,
useRawSeries: true,
},
"graph.series_order_dimension": {
getValue: (_series, settings) => settings["graph.dimensions"][1],
@@ -132,14 +126,14 @@ export const GRAPH_DATA_SETTINGS = {
section: t`Data`,
widget: ChartSettingOrderedSimple,
marginBottom: "1rem",
getValue: (series, settings) => {
const seriesKeys = series.map(s => keyForSingleSeries(s));
getValue: (series, settings, settingsModel) => {
const seriesKeys = settingsModel.seriesModels.map(s => s.vizSettingsKey);
return getSeriesOrderVisibilitySettings(settings, seriesKeys);
},
getHidden: (series, settings) => {
getHidden: (series, settings, settingsModel) => {
return (
settings["graph.dimensions"]?.length < 2 || series.length > MAX_SERIES
settings["graph.dimensions"]?.length < 2 ||
settingsModel.seriesModels.length > MAX_SERIES
);
},
dashboard: false,
@@ -178,7 +172,7 @@ export const GRAPH_DATA_SETTINGS = {
showColumnSetting: true,
showColorPicker: !hasBreakout,
colors: vizSettings["series_settings.colors"],
series: extra.transformedSeries,
series: extra.transformedSeries, // put here series models
};
},
readDependencies: [
@@ -188,7 +182,7 @@ export const GRAPH_DATA_SETTINGS = {
],
writeDependencies: ["graph.dimensions"],
dashboard: false,
useRawSeries: true,
// useRawSeries: true,
},
...seriesSetting(),
};
@@ -216,7 +210,6 @@ export const GRAPH_BUBBLE_SETTINGS = {
},
writeDependencies: ["graph.dimensions"],
dashboard: false,
useRawSeries: true,
},
};
@@ -245,8 +238,11 @@ export const STACKABLE_SETTINGS = {
{ name: t`Stack - 100%`, value: "normalized" },
],
},
isValid: (series, settings) => {
const seriesDisplays = getSeriesDisplays(series, settings);
isValid: (series, settings, settingsModel) => {
const seriesDisplays = getSeriesDisplays(
settingsModel.seriesModels,
settings,
);
return isStackingValueValid(
series[0].card.display,
@@ -297,7 +293,6 @@ export const GRAPH_TREND_SETTINGS = {
const { insights } = series[0].data;
return !insights || insights.length === 0;
},
useRawSeries: true,
inline: true,
marginBottom: "1rem",
},
@@ -308,7 +303,8 @@ export const GRAPH_DISPLAY_VALUES_SETTINGS = {
section: t`Display`,
title: t`Show values on data points`,
widget: "toggle",
getHidden: (series, vizSettings) => !canHaveDataLabels(series, vizSettings),
getHidden: (series, vizSettings, settingsModel) =>
!canHaveDataLabels(settingsModel.seriesModels, vizSettings),
getDefault: getDefaultShowDataLabels,
inline: true,
marginBottom: "1rem",
@@ -317,26 +313,28 @@ export const GRAPH_DISPLAY_VALUES_SETTINGS = {
section: t`Display`,
title: t`Values to show`,
widget: "segmentedControl",
getHidden: (series, vizSettings) => {
getHidden: (series, vizSettings, settingsModel) => {
if (!vizSettings["graph.show_values"]) {
return true;
}
const areAllBars = getSeriesDisplays(series, vizSettings).every(
display => display === "bar",
);
const areAllBars = getSeriesDisplays(
settingsModel.seriesModels,
vizSettings,
).every(display => display === "bar");
if (areAllBars && vizSettings["graph.show_stack_values"] === "series") {
return true;
}
const hasLines = getSeriesDisplays(series, vizSettings).some(
display => display === "line",
);
const hasLines = getSeriesDisplays(
settingsModel.seriesModels,
vizSettings,
).some(display => display === "line");
if (vizSettings["stackable.stack_type"] === "normalized" && !hasLines) {
return true;
}
return !canHaveDataLabels(series, vizSettings);
return !canHaveDataLabels(settingsModel.seriesModels, vizSettings);
},
props: {
options: [
@@ -351,19 +349,20 @@ export const GRAPH_DISPLAY_VALUES_SETTINGS = {
section: t`Display`,
title: t`Stack values to show`,
widget: "segmentedControl",
getHidden: (series, vizSettings) => {
const hasBars = getSeriesDisplays(series, vizSettings).some(
display => display === "bar",
);
getHidden: (series, vizSettings, settingsModel) => {
const hasBars = getSeriesDisplays(
settingsModel.seriesModels,
vizSettings,
).some(display => display === "bar");
return (
vizSettings["stackable.stack_type"] !== "stacked" ||
vizSettings["graph.show_values"] !== true ||
!hasBars
);
},
isValid: (series, vizSettings) => {
isValid: (series, vizSettings, settingsModel) => {
return isShowStackValuesValid(
getSeriesDisplays(series, vizSettings),
getSeriesDisplays(settingsModel.seriesModels, vizSettings),
vizSettings,
);
},
@@ -504,12 +503,18 @@ export const GRAPH_AXIS_SETTINGS = {
widget: "toggle",
index: 5,
inline: true,
isValid: (series, settings) => {
const seriesDisplays = getSeriesDisplays(series, settings);
isValid: (series, settings, settingsModel) => {
const seriesDisplays = getSeriesDisplays(
settingsModel.seriesModels,
settings,
);
return isYAxisUnpinFromZeroValid(seriesDisplays, settings);
},
getHidden: (series, settings) => {
const seriesDisplays = getSeriesDisplays(series, settings);
getHidden: (series, settings, settingsModel) => {
const seriesDisplays = getSeriesDisplays(
settingsModel.seriesModels,
settings,
);
return !isYAxisUnpinFromZeroValid(seriesDisplays, settings);
},
getDefault: series => {
@@ -554,7 +559,8 @@ export const GRAPH_AXIS_SETTINGS = {
widget: "toggle",
inline: true,
getDefault: getDefaultIsAutoSplitEnabled,
getHidden: series => series.length < 2,
getHidden: (series, settings, settingsModel) =>
settingsModel.seriesModels.length < 2,
},
"graph.x_axis.labels_enabled": {
section: t`Axes`,

View File

@@ -1,12 +1,38 @@
import type React from "react";
import { t } from "ttag";
import _ from "underscore";
import chartSettingNestedSettings from "metabase/visualizations/components/settings/ChartSettingNestedSettings";
import type { ComputedVisualizationSettings } from "metabase/visualizations/types";
import type { RawSeries } from "metabase-types/api";
import { getComputedSettings, getSettingsWidgets } from "../settings";
export function nestedSettings(
id,
import { getSettingsModelForSeries } from "./visualization";
type NestedSettingsDefinition<TObject> = {
objectName: string;
getObjects: (
series: RawSeries,
settings: ComputedVisualizationSettings,
) => TObject[];
getObjectKey: (object: TObject) => string;
getSettingDefinitionsForObject: (
rawSeries: RawSeries,
object: TObject,
) => $TODO;
getInheritedSettingsForObject: (object: TObject) => $TODO;
component: React.ReactNode;
getExtraProps: () => $TODO;
};
export function nestedSettings<
TObject,
TObjectSettings extends Record<string, unknown>,
TExtra,
TSettingsModel,
>(
id: string,
{
objectName = "object",
getObjects,
@@ -15,34 +41,43 @@ export function nestedSettings(
getInheritedSettingsForObject = () => ({}),
component,
...def
} = {},
}: NestedSettingsDefinition<TObject>,
) {
function getComputedSettingsForObject(series, object, storedSettings, extra) {
function getComputedSettingsForObject(
series: RawSeries,
object: TObject,
storedSettings: TObjectSettings,
settingsModel: TSettingsModel,
extra: TExtra,
): Partial<TObjectSettings> {
const settingsDefs = getSettingDefinitionsForObject(series, object);
const inheritedSettings = getInheritedSettingsForObject(object);
const computedSettings = getComputedSettings(
settingsDefs,
object,
{ ...inheritedSettings, ...storedSettings },
settingsModel,
extra,
);
) as TObjectSettings;
// remove undefined settings since they override other settings when merging object
return _.pick(computedSettings, value => value !== undefined);
}
function getComputedSettingsForAllObjects(
series,
objects,
allStoredSettings,
extra,
series: RawSeries,
objects: TObject[],
allStoredSettings: Record<string, TObjectSettings>,
settingsModel: unknown,
extra: TExtra,
) {
const allComputedSettings = {};
const allComputedSettings: Record<string, Partial<TObjectSettings>> = {};
for (const object of objects) {
const key = getObjectKey(object);
allComputedSettings[key] = getComputedSettingsForObject(
series,
object,
allStoredSettings[key] || {},
settingsModel,
extra,
);
}
@@ -50,17 +85,19 @@ export function nestedSettings(
}
function getSettingsWidgetsForObject(
series,
object,
storedSettings,
onChangeSettings,
extra,
series: RawSeries,
object: TObject,
storedSettings: TObjectSettings,
onChangeSettings: (settings: TObjectSettings) => void,
settingsModel,
extra: TExtra,
) {
const settingsDefs = getSettingDefinitionsForObject(series, object);
const computedSettings = getComputedSettingsForObject(
series,
object,
storedSettings,
settingsModel,
extra,
);
const widgets = getSettingsWidgets(
@@ -69,6 +106,7 @@ export function nestedSettings(
computedSettings,
object,
onChangeSettings,
settingsModel,
extra,
);
return widgets.map(widget => ({ ...widget, noPadding: true }));
@@ -84,12 +122,19 @@ export function nestedSettings(
[id]: {
section: t`Display`,
default: {},
getProps: (series, settings, onChange, extra) => {
const objects = getObjects(series, settings);
getProps: (
series: RawSeries,
settings: ComputedVisualizationSettings,
onChange,
settingsModel,
extra: TExtra,
) => {
const objects = getObjects(series, settings, settingsModel);
const allComputedSettings = getComputedSettingsForAllObjects(
series,
objects,
settings[id],
settingsModel,
{ series, settings },
);
return {
@@ -106,10 +151,15 @@ export function nestedSettings(
...def,
},
[objectName]: {
getDefault(series, settings) {
getDefault(
series: RawSeries,
settings: ComputedVisualizationSettings,
settingsModel: TSettingsModel,
) {
const cache = new Map();
return object => {
return (object: TObject) => {
const key = getObjectKey(object);
if (!cache.has(key)) {
const inheritedSettings = getInheritedSettingsForObject(object);
const storedSettings = settings[id][key] || {};
@@ -121,6 +171,7 @@ export function nestedSettings(
...inheritedSettings,
...storedSettings,
},
settingsModel,
{ series, settings },
),
});

View File

@@ -1,8 +1,10 @@
import { getIn } from "icepick";
import type React from "react";
import { t } from "ttag";
import _ from "underscore";
import ChartNestedSettingSeries from "metabase/visualizations/components/settings/ChartNestedSettingSeries";
import type { SeriesModel } from "metabase/visualizations/echarts/cartesian/model/types";
import {
SERIES_COLORS_SETTING_KEY,
getSeriesDefaultLinearInterpolate,
@@ -15,35 +17,61 @@ import {
getSeriesDefaultLineStyle,
getSeriesDefaultLineSize,
} from "metabase/visualizations/shared/settings/series";
import { getNameForCard } from "../series";
import type { RawSeries, SeriesSettings } from "metabase-types/api";
import { nestedSettings } from "./nested";
export function keyForSingleSeries(single) {
// _seriesKey is sometimes set by transformSeries
return single.card._seriesKey || String(single.card.name);
}
const LINE_DISPLAY_TYPES = new Set(["line", "area"]);
type ComputedSeriesSettings = NonNullableProps<
SeriesSettings,
"title" | "display"
>;
type NestedSettingsDefinition<
TObject,
TObjectSettings,
TValue,
TSettingsModel,
TWidgetProps,
TExtra,
> = {
widget?: string | React.ReactNode;
title?: string;
props?: TWidgetProps;
getDefault?: (
object: TObject,
settings: TObjectSettings,
settingsModel: TSettingsModel,
extra: TExtra,
) => TValue;
getHidden?: (
object: TObject,
settings: TObjectSettings,
settingsModel: TSettingsModel,
extra: TExtra,
) => TValue;
readDependencies?: string[];
};
type NestedSettingsDefinitions = Record<
string,
NestedSettingsDefinition<SeriesModel, SeriesSettings, unknown, unknown>
>;
export function seriesSetting({
readDependencies = [],
noPadding,
...def
} = {}) {
const COMMON_SETTINGS = {
} = {}): Record<
string,
NestedSettingsDefinition<SeriesModel, SeriesSettings, unknown, unknown>
> {
const COMMON_SETTINGS: NestedSettingsDefinitions = {
// title, and color don't need widgets because they're handled directly in ChartNestedSettingSeries
title: {
getDefault: (single, settings, { series, settings: vizSettings }) => {
const legacyTitles = vizSettings["graph.series_labels"];
if (legacyTitles) {
const index = series.indexOf(single); // TODO: pass in series index so we don't have to search for it
if (index >= 0 && index < legacyTitles.length) {
return legacyTitles[index];
}
}
return single.card.name;
getDefault: (single, settings, settingsModel) => {
return single.name;
},
},
display: {
@@ -56,32 +84,25 @@ export function seriesSetting({
{ value: "bar", icon: "bar" },
],
},
getHidden: (single, settings, { series }) => {
getHidden: (single, settings, settingsModel) => {
return (
!["line", "area", "bar", "combo"].includes(single.card.display) ||
!["line", "area", "bar", "combo"].includes(settingsModel.display) ||
settings["stackable.stack_type"] != null
);
},
getDefault: (single, settings, { series }) => {
// FIXME: will move to Cartesian series model further, but now this code is used by other legacy charts
const transformedSeriesIndex = series.findIndex(
s => keyForSingleSeries(s) === keyForSingleSeries(single),
getDefault: (single, settings, settingsModel) => {
const index = settingsModel.seriesModels.find(
sm => sm.vizSettingsKey === single.vizSettingsKey,
);
return getSeriesDefaultDisplay(
series[transformedSeriesIndex].card.display,
transformedSeriesIndex,
);
return getSeriesDefaultDisplay(settingsModel.display, index);
},
},
color: {
getDefault: (single, settings, { settings: vizSettings }) =>
// get the color for series key, computed in the setting
getIn(vizSettings, [
SERIES_COLORS_SETTING_KEY,
keyForSingleSeries(single),
]),
getIn(vizSettings, [SERIES_COLORS_SETTING_KEY, single.vizSettingsKey]),
},
"line.interpolate": {
title: t`Line shape`,
@@ -95,7 +116,12 @@ export function seriesSetting({
},
getHidden: (single, settings) =>
!LINE_DISPLAY_TYPES.has(settings["display"]),
getDefault: (single, settings, { settings: vizSettings }) =>
getDefault: (
single,
settings,
_settingsModel,
{ settings: vizSettings },
) =>
// use legacy global line.interpolate setting if present
getSeriesDefaultLinearInterpolate(vizSettings),
readDependencies: ["display"],
@@ -110,9 +136,10 @@ export function seriesSetting({
{ icon: "line_style_dotted", value: "dotted" },
],
},
getDefault: (series, settings) => getSeriesDefaultLineStyle(settings),
getHidden: (single, settings) =>
!LINE_DISPLAY_TYPES.has(settings["display"]),
getDefault: (series: SeriesModel, settings: SeriesSettings) =>
getSeriesDefaultLineStyle(settings),
getHidden: (seriesModel: SeriesModel, settings: SeriesSettings) =>
!LINE_DISPLAY_TYPES.has(settings.display),
readDependencies: ["display"],
},
"line.size": {
@@ -157,9 +184,14 @@ export function seriesSetting({
{ name: t`Linear Interpolated`, value: "interpolate" },
],
},
getHidden: (single, settings) =>
getHidden: (seriesModel: SeriesModel, settings: SeriesSettings) =>
!LINE_DISPLAY_TYPES.has(settings["display"]),
getDefault: (single, settings, { settings: vizSettings }) =>
getDefault: (
single,
settings,
settingsModel,
{ settings: vizSettings },
) =>
// use legacy global line.missing setting if present
getSeriesDefaultLineMissing(vizSettings),
readDependencies: ["display"],
@@ -168,7 +200,8 @@ export function seriesSetting({
title: t`Y-axis position`,
widget: "segmentedControl",
default: null,
getHidden: (single, settings) => single.card.display === "row",
getHidden: (single, settings, settingsModel) =>
settingsModel.display === "row",
props: {
options: [
{ name: t`Auto`, value: null },
@@ -182,17 +215,22 @@ export function seriesSetting({
title: t`Show values for this series`,
widget: "toggle",
inline: true,
getHidden: (single, seriesSettings, { settings, series }) =>
getHidden: (
single,
seriesSettings,
settingsModel,
{ settings, series },
) =>
series.length <= 1 || // no need to show series-level control if there's only one series
!settings["graph.show_values"] || // don't show it unless this chart has a global setting
settings["stackable.stack_type"], // hide series controls if the chart is stacked
getDefault: (single, seriesSettings, { settings }) =>
getDefault: (single, seriesSettings, settingsModel, { settings }) =>
getSeriesDefaultShowSeriesValues(settings),
readDependencies: ["graph.show_values", "stackable.stack_type"],
},
};
function getSettingDefinitionsForSingleSeries(series, object, settings) {
function getSettingDefinitionsForSingleSeries() {
return COMMON_SETTINGS;
}
@@ -203,26 +241,21 @@ export function seriesSetting({
getSection: (series, settings, { isDashboard }) =>
isDashboard ? t`Display` : t`Style`,
objectName: "series",
getObjects: (series, settings) => series,
getObjectKey: keyForSingleSeries,
getObjects: (series, settings, settingsModel) => {
return settingsModel.seriesModels;
},
getObjectKey: seriesModel => seriesModel.vizSettingsKey,
getSettingDefinitionsForObject: getSettingDefinitionsForSingleSeries,
component: ChartNestedSettingSeries,
readDependencies: [SERIES_COLORS_SETTING_KEY, ...readDependencies],
noPadding: true,
getExtraProps: series => ({
seriesCardNames: series.reduce((memo, singleSeries) => {
memo[keyForSingleSeries(singleSeries)] = getNameForCard(
singleSeries.card,
);
return memo;
}, {}),
}),
...def,
}),
// colors must be computed as a whole rather than individually
[SERIES_COLORS_SETTING_KEY]: {
getValue(series, settings) {
const keys = series.map(single => keyForSingleSeries(single));
getValue(series, settings, settingsModel) {
// const keys = series.map(single => keyForSingleSeries(single));
const keys = settingsModel.seriesModels.map(s => s.vizSettingsKey);
return getSeriesColors(keys, settings);
},
},

View File

@@ -45,6 +45,14 @@ const COMMON_SETTINGS = {
click_behavior: {},
};
export function getSettingsModelForSeries(series, settings) {
if (!series) {
return null;
}
const visualization = getVisualizationRaw(series);
return visualization.getSettingsModel?.(series, settings);
}
function getSettingDefintionsForSeries(series) {
if (!series) {
return {};
@@ -94,7 +102,14 @@ export function getComputedSettingsForSeries(series) {
}
const settingsDefs = getSettingDefintionsForSeries(series);
const storedSettings = getStoredSettingsForSeries(series);
return getComputedSettings(settingsDefs, series, storedSettings);
const settingsModel = getSettingsModelForSeries(series, storedSettings);
return getComputedSettings(
settingsDefs,
series,
storedSettings,
settingsModel,
{},
);
}
export function getPersistableDefaultSettingsForSeries(series) {
@@ -109,6 +124,7 @@ export function getSettingsWidgetsForSeries(
series,
onChangeSettings,
isDashboard = false,
settingsModel,
extra = {},
) {
const settingsDefs = getSettingDefintionsForSeries(series);
@@ -121,6 +137,7 @@ export function getSettingsWidgetsForSeries(
computedSettings,
series,
onChangeSettings,
settingsModel,
{ isDashboard, ...extra },
).filter(
widget =>

View File

@@ -11,10 +11,8 @@ import type {
DatasetColumn,
DatasetData,
RawSeries,
Series,
TimelineEvent,
TimelineEventId,
TransformedSeries,
VisualizationSettings,
} from "metabase-types/api";
@@ -71,7 +69,7 @@ export interface StaticVisualizationProps {
}
export interface VisualizationProps {
series: Series;
series: RawSeries;
card: Card;
href: string | undefined;
data: DatasetData;
@@ -141,41 +139,85 @@ export type ColumnSettingDefinition<TValue, TProps = unknown> = {
col: DatasetColumn,
settings: OptionsType,
onChange: (value: TValue) => void,
extra: { series: Series },
extra: { series: RawSeries },
) => TProps;
};
export type VisualizationSettingDefinition<TValue, TProps = void> = {
export type VisualizationSettingDefinition<
TObject,
TObjectSettings,
TValue,
TSettingsModel,
TWidgetProps,
TExtra,
> = {
section?: string;
title?: string;
group?: string;
widget?: string | React.ComponentType<TProps>;
isValid?: (series: Series, settings: VisualizationSettings) => boolean;
getHidden?: (series: Series, settings: VisualizationSettings) => boolean;
getDefault?: (series: Series, settings: VisualizationSettings) => TValue;
getValue?: (series: Series, settings: VisualizationSettings) => TValue;
widget?: string | React.ComponentType<TWidgetProps>;
isValid?: (
series: TObject,
settings: TObjectSettings,
settingsModel: TSettingsModel,
extra: TExtra,
) => boolean;
getHidden?: (
series: TObject,
settings: TObjectSettings,
settingsModel: TSettingsModel,
extra: TExtra,
) => boolean;
getDefault?: (
series: TObject,
settings: TObjectSettings,
settingsModel: TSettingsModel,
extra: TExtra,
) => TValue;
getValue?: (
series: TObject,
settings: TObjectSettings,
settingsModel: TSettingsModel,
extra: TExtra,
) => TValue;
default?: TValue;
marginBottom?: string;
getMarginBottom?: (series: Series, settings: VisualizationSettings) => string;
getMarginBottom?: (
series: TObject,
settings: TObjectSettings,
settingsModel: TSettingsModel,
extra: TExtra,
) => string;
persistDefault?: boolean;
inline?: boolean;
props?: TProps;
props?: TWidgetProps;
getProps?: (
series: Series,
vizSettings: VisualizationSettings,
series: TObject,
vizSettings: TObjectSettings,
onChange: (value: TValue) => void,
extra: unknown,
) => TProps;
) => TWidgetProps;
readDependencies?: string[];
writeDependencies?: string[];
eraseDependencies?: string[];
// is the setting visible in the dashboard card viz settings
dashboard?: boolean;
useRawSeries?: boolean;
dashboard?: boolean; // is the setting visible in the dashboard card viz settings
};
export type VisualizationSettingsDefinitions = {
[key: string]: VisualizationSettingDefinition<unknown, unknown>;
export type VisualizationSettingsDefinitions<
TObject,
TObjectSettings,
TValue,
TSettingsModel,
TWidgetProps,
TExtra,
> = {
[key: string]: VisualizationSettingDefinition<
TObject,
TObjectSettings,
TValue,
TSettingsModel,
TWidgetProps,
TExtra
>;
};
export type VisualizationGridSize = {
@@ -186,42 +228,56 @@ export type VisualizationGridSize = {
};
// TODO: add component property for the react component instead of the intersection
export type Visualization = React.ComponentType<VisualizationProps> & {
name: string;
noun: string;
uiName: string;
identifier: string;
aliases?: string[];
iconName: IconName;
export type Visualization<TSettingsModel = unknown> =
React.ComponentType<VisualizationProps> & {
name: string;
noun: string;
uiName: string;
identifier: string;
aliases?: string[];
iconName: IconName;
maxMetricsSupported: number;
maxDimensionsSupported: number;
maxMetricsSupported: number;
maxDimensionsSupported: number;
disableClickBehavior?: boolean;
canSavePng?: boolean;
noHeader: boolean;
hidden?: boolean;
disableSettingsConfig?: boolean;
supportPreviewing?: boolean;
supportsSeries?: boolean;
disableClickBehavior?: boolean;
canSavePng?: boolean;
noHeader: boolean;
hidden?: boolean;
disableSettingsConfig?: boolean;
supportPreviewing?: boolean;
supportsSeries?: boolean;
minSize: VisualizationGridSize;
defaultSize: VisualizationGridSize;
minSize: VisualizationGridSize;
defaultSize: VisualizationGridSize;
settings: VisualizationSettingsDefinitions;
settings: VisualizationSettingsDefinitions<
RawSeries,
VisualizationSettings,
unknown,
TSettingsModel,
unknown,
unknown
>;
placeHolderSeries: Series;
placeHolderSeries: RawSeries;
transformSeries: (series: Series) => TransformedSeries;
// TODO: remove dependency on metabase-lib
isSensible: (data: DatasetData, query?: Query) => boolean;
// checkRenderable throws an error if a visualization is not renderable
checkRenderable: (
series: Series,
settings: VisualizationSettings,
query: Query,
) => void | never;
isLiveResizable: (series: Series) => boolean;
onDisplayUpdate?: (settings: VisualizationSettings) => VisualizationSettings;
placeholderSeries: RawSeries;
};
getSettingsModel?: (
rawSeries: RawSeries,
settings: VisualizationSettings,
) => TSettingsModel;
// TODO: remove dependency on metabase-lib
isSensible: (data: DatasetData, query?: Query) => boolean;
// checkRenderable throws an error if a visualization is not renderable
checkRenderable: (
series: RawSeries,
settings: VisualizationSettings,
query: Query,
) => void | never;
isLiveResizable: (series: RawSeries) => boolean;
onDisplayUpdate?: (
settings: VisualizationSettings,
) => VisualizationSettings;
placeholderSeries: RawSeries;
};

View File

@@ -1,159 +0,0 @@
import _ from "underscore";
import { NULL_DISPLAY_VALUE } from "metabase/lib/constants";
import { formatValue } from "metabase/lib/formatting";
import { isEmpty } from "metabase/lib/validate";
import { getComputedSettingsForSeries } from "metabase/visualizations/lib/settings/visualization";
import { getFriendlyName } from "metabase/visualizations/lib/utils";
// TODO: this series transformation is used only for the visualization settings computation which is excessive.
// Replace this with defining settings models per visualization type which will contain all necessary info
// about the dataset sufficient to compute settings defaults like series settings.
export function transformSeries(series) {
const newSeries = [].concat(
...series.map((s, seriesIndex) =>
transformSingleSeries(s, series, seriesIndex),
),
);
if (_.isEqual(series, newSeries) || newSeries.length === 0) {
return series;
} else {
return newSeries;
}
}
function transformSingleSeries(s, series, seriesIndex) {
const { card, data } = s;
// HACK: prevents cards from being transformed too many times
if (data._transformed) {
return [s];
}
const { cols, rows } = data;
const settings = getComputedSettingsForSeries([s]);
const dimensions = (settings["graph.dimensions"] || []).filter(
d => d != null,
);
const metrics = (settings["graph.metrics"] || []).filter(d => d != null);
const dimensionColumnIndexes = dimensions.map(dimensionName =>
_.findIndex(cols, col => col.name === dimensionName),
);
const metricColumnIndexes = metrics.map(metricName =>
_.findIndex(cols, col => col.name === metricName),
);
const bubbleColumnIndex =
settings["scatter.bubble"] &&
_.findIndex(cols, col => col.name === settings["scatter.bubble"]);
const extraColumnIndexes =
bubbleColumnIndex != null && bubbleColumnIndex >= 0
? [bubbleColumnIndex]
: [];
if (dimensions.length > 1) {
const [dimensionColumnIndex, seriesColumnIndex] = dimensionColumnIndexes;
const rowColumnIndexes = [dimensionColumnIndex].concat(
metricColumnIndexes,
extraColumnIndexes,
);
const breakoutValues = [];
const breakoutRowsByValue = new Map();
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
const row = rows[rowIndex];
const seriesValue = row[seriesColumnIndex];
let seriesRows = breakoutRowsByValue.get(seriesValue);
if (!seriesRows) {
breakoutRowsByValue.set(seriesValue, (seriesRows = []));
breakoutValues.push(seriesValue);
}
const newRow = rowColumnIndexes.map(columnIndex => row[columnIndex]);
newRow._origin = { seriesIndex, rowIndex, row, cols };
seriesRows.push(newRow);
}
return breakoutValues.map(breakoutValue => ({
card: {
...card,
// if multiseries include the card title as well as the breakout value
name: [
// show series title if it's multiseries
series.length > 1 && card.name,
// always show grouping value
formatValue(
isEmpty(breakoutValue) ? NULL_DISPLAY_VALUE : breakoutValue,
{ column: cols[seriesColumnIndex] },
),
]
.filter(n => n)
.join(": "),
originalCardName: card.name,
_breakoutValue: breakoutValue,
_breakoutColumn: cols[seriesColumnIndex],
},
data: {
rows: breakoutRowsByValue.get(breakoutValue),
cols: rowColumnIndexes.map(i => cols[i]),
results_timezone: data.results_timezone,
_rawCols: cols,
_transformed: true,
},
// for when the legend header for the breakout is clicked
clicked: {
dimensions: [
{
value: breakoutValue,
column: cols[seriesColumnIndex],
},
],
},
}));
} else {
// dimensions.length <= 1
const dimensionColumnIndex = dimensionColumnIndexes[0];
return metricColumnIndexes.map(metricColumnIndex => {
const col = cols[metricColumnIndex];
const rowColumnIndexes = [dimensionColumnIndex].concat(
metricColumnIndex,
extraColumnIndexes,
);
const name = [
// show series title if it's multiseries
series.length > 1 && card.name,
// show column name if there are multiple metrics or sigle series
(metricColumnIndexes.length > 1 || series.length === 1) &&
col &&
getFriendlyName(col),
]
.filter(n => n)
.join(": ");
return {
card: {
...card,
name: name,
originalCardName: card.name,
_seriesIndex: seriesIndex,
// use underlying column name as the seriesKey since it should be unique
// EXCEPT for dashboard multiseries, so check seriesIndex == 0
_seriesKey: seriesIndex === 0 && col ? col.name : name,
},
data: {
rows: rows.map((row, rowIndex) => {
const newRow = rowColumnIndexes.map(i => row[i]);
newRow._origin = { seriesIndex, rowIndex, row, cols };
return newRow;
}),
cols: rowColumnIndexes.map(i => cols[i]),
results_timezone: data.results_timezone,
_transformed: true,
_rawCols: cols,
},
};
});
}
}

View File

@@ -1,6 +1,17 @@
import { t } from "ttag";
import _ from "underscore";
import { formatValue } from "metabase/lib/formatting";
import { getCardsColumns } from "metabase/visualizations/echarts/cartesian/model";
import {
getCardsSeriesModels,
getDimensionModel,
} from "metabase/visualizations/echarts/cartesian/model/series";
import type {
DimensionModel,
SeriesModel,
} from "metabase/visualizations/echarts/cartesian/model/types";
import type { CartesianChartColumns } from "metabase/visualizations/lib/graph/columns";
import { GRAPH_GOAL_SETTINGS } from "metabase/visualizations/lib/settings/goal";
import {
GRAPH_AXIS_SETTINGS,
@@ -24,13 +35,23 @@ import type {
VisualizationSettingsDefinitions,
} from "metabase/visualizations/types";
import { isDimension, isMetric } from "metabase-lib/v1/types/utils/isa";
import type { RawSeries, SeriesSettings } from "metabase-types/api";
import type {
RawSeries,
SeriesSettings,
VisualizationSettings,
} from "metabase-types/api";
import { transformSeries } from "./chart-definition-legacy";
export type CartesianChartSettingsModel = {
cardsColumns: CartesianChartColumns[];
dimensionModel: DimensionModel;
seriesModels: SeriesModel[];
rawSeries: RawSeries;
display: string;
};
export const getCartesianChartDefinition = (
props: Partial<Visualization>,
): Partial<Visualization> => {
props: Partial<Visualization<CartesianChartSettingsModel>>,
): Partial<Visualization<CartesianChartSettingsModel>> => {
return {
noHeader: true,
supportsSeries: true,
@@ -82,7 +103,29 @@ export const getCartesianChartDefinition = (
},
] as RawSeries,
transformSeries,
getSettingsModel: (
rawSeries: RawSeries,
settings: VisualizationSettings,
): CartesianChartSettingsModel => {
const cardsColumns = getCardsColumns(rawSeries, settings);
const dimensionModel = getDimensionModel(rawSeries, cardsColumns);
const seriesModels = getCardsSeriesModels(
rawSeries,
cardsColumns,
settings,
{
formatValue: (value, options) => String(formatValue(value, options)),
},
);
return {
cardsColumns,
dimensionModel,
seriesModels,
rawSeries,
display: rawSeries[0].card.display,
};
},
onDisplayUpdate: settings => {
if (settings[SERIES_SETTING_KEY] == null) {

View File

@@ -18,9 +18,9 @@ export function useChartDebug({
chartModel: BaseCartesianChartModel;
}) {
useEffect(() => {
if (!isQueryBuilder || !isChartsDebugLoggingEnabled) {
return;
}
// if (!isQueryBuilder || !isChartsDebugLoggingEnabled) {
// return;
// }
console.log("-------------- ECHARTS DEBUG INFO START --------------");
console.log("rawSeries", rawSeries);
console.log("option", option);

View File

@@ -229,8 +229,7 @@ export const useChartEvents = (
// all the data labels show
const isBarSeries =
seriesModel != null
? settings.series(seriesModel.legacySeriesSettingsObjectKey)
.display === "bar"
? settings.series(seriesModel).display === "bar"
: false;
const shouldHighlightEntireSeries =
isBarSeries && chartModel.seriesModels.length > 1;

View File

@@ -256,6 +256,7 @@ class Table extends Component<TableProps, TableState> {
column,
settings,
onChange,
seriesModel,
{
series: [
{
@@ -282,6 +283,7 @@ class Table extends Component<TableProps, TableState> {
column,
settings,
onChange,
seriesModel,
{
series: [
{

View File

@@ -19,3 +19,8 @@ declare module "*.css" {
}
type Nullable<T> = T | null;
type NonNullableProps<T, K extends keyof T> = Omit<T, K> &
Required<{ [P in K]: NonNullable<T[P]> }>;
type $TODO = any;