Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix data labels too dense with large single series #42985

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
76e87c9
remove unused variable
JesseSDevaney May 21, 2024
e197cf8
conditionally render labels sparsely if large amount of data points
JesseSDevaney May 21, 2024
08081dc
update loki snapshots
JesseSDevaney May 21, 2024
f0cd7ca
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 22, 2024
42caaa2
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 22, 2024
04d01eb
refactor data label formatter conditions
JesseSDevaney May 22, 2024
dca5e2a
setup intelligent label plotting
JesseSDevaney May 24, 2024
6b56809
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 24, 2024
e80cb6d
handle the case when there are no labels
JesseSDevaney May 24, 2024
ea45246
add sparse data label rendering to bar charts
JesseSDevaney May 24, 2024
c7ba7ac
do not compute if no labels need to be shown or hidden
JesseSDevaney May 24, 2024
e21e1dc
add sparse data labeling to waterfall charts
JesseSDevaney May 24, 2024
9eec1cd
adjustment for large series waterfall labels
JesseSDevaney May 24, 2024
7f3dd27
revert change
JesseSDevaney May 24, 2024
fb99ab8
refactor name
JesseSDevaney May 24, 2024
7381172
add sparse labels to stacked bar and area
JesseSDevaney May 24, 2024
f6263b5
improve scaleFactor
JesseSDevaney May 24, 2024
306d32f
fix type errors
JesseSDevaney May 25, 2024
c09f9ea
remove unused labelFormatter
JesseSDevaney May 25, 2024
36247a9
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 28, 2024
23835f0
update snapshots
JesseSDevaney May 28, 2024
e3679a1
patch ECharts
JesseSDevaney May 28, 2024
c8e45d4
update loki snapshots
JesseSDevaney May 29, 2024
c6f5040
only loop over dataset once for getWaterfallChartDataDensity
JesseSDevaney May 29, 2024
4f1ec92
improve performance of chart data density calculations
JesseSDevaney May 29, 2024
43cfe0d
update loki snapshots
JesseSDevaney May 29, 2024
97de1b1
update E2E spec
JesseSDevaney May 29, 2024
d0aeb4a
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 29, 2024
6744b29
update E2E spec
JesseSDevaney May 29, 2024
952190e
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 30, 2024
c8a7b41
increase cartesian label density allotment
JesseSDevaney May 30, 2024
1e6068d
update loki snapshots
JesseSDevaney May 30, 2024
aad7461
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 30, 2024
9d1c2c2
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 30, 2024
ea82287
[ci nocache] update loki snapshots
JesseSDevaney May 30, 2024
6dce268
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 30, 2024
a9891f2
comment out node_modules cache since it cannot be skipped by commit a…
JesseSDevaney May 30, 2024
371c7c0
fix type errors
JesseSDevaney May 31, 2024
417753a
Reset node modules if patches are changed
uladzimirdev May 31, 2024
cc4662a
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
uladzimirdev May 31, 2024
89f90d2
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 31, 2024
7e225ae
refactor type naming
JesseSDevaney May 31, 2024
f863c24
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney May 31, 2024
60f4344
Merge branch 'master' into fix-40196/data-labels-too-dense-with-large…
JesseSDevaney Jun 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { X_AXIS_DATA_KEY } from "metabase/visualizations/echarts/cartesian/const
import { CHART_STYLE } from "metabase/visualizations/echarts/cartesian/constants/style";
import type {
AxisFormatter,
CartesianChartModel,
BaseCartesianChartModel,
ChartDataset,
NumericAxisScaleTransforms,
XAxisModel,
Expand Down Expand Up @@ -200,7 +200,7 @@ const X_LABEL_ROTATE_45_THRESHOLD_FACTOR = 2.1;
const X_LABEL_ROTATE_90_THRESHOLD_FACTOR = 1.2;

const getAutoAxisEnabledSetting = (
chartModel: CartesianChartModel,
chartModel: BaseCartesianChartModel,
settings: ComputedVisualizationSettings,
boundaryWidth: number,
maxXTickWidth: number,
Expand Down Expand Up @@ -250,7 +250,7 @@ const getAutoAxisEnabledSetting = (
};

const getTicksDimensions = (
chartModel: CartesianChartModel,
chartModel: BaseCartesianChartModel,
chartWidth: number,
outerHeight: number,
settings: ComputedVisualizationSettings,
Expand Down Expand Up @@ -348,7 +348,7 @@ const getTicksDimensions = (
const TICK_OVERFLOW_BUFFER = 4;

export const getChartPadding = (
chartModel: CartesianChartModel,
chartModel: BaseCartesianChartModel,
settings: ComputedVisualizationSettings,
ticksDimensions: TicksDimensions,
axisEnabledSetting: ComputedVisualizationSettings["graph.x_axis.axis_enabled"],
Expand Down Expand Up @@ -478,7 +478,7 @@ export const getChartBounds = (
};

const getDimensionWidth = (
chartModel: CartesianChartModel,
chartModel: BaseCartesianChartModel,
boundaryWidth: number,
) => {
const { xAxisModel } = chartModel;
Expand Down Expand Up @@ -525,7 +525,7 @@ const areHorizontalXAxisTicksOverlapping = (
};

export const getChartMeasurements = (
chartModel: CartesianChartModel,
chartModel: BaseCartesianChartModel,
settings: ComputedVisualizationSettings,
hasTimelineEvents: boolean,
width: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "metabase/visualizations/echarts/cartesian/model/dataset";
import {
getCardsSeriesModels,
getCartesianChartDataDensity,
getDimensionModel,
getSeriesLabelsFormatters,
getStackedLabelsFormatters,
Expand Down Expand Up @@ -151,6 +152,16 @@ export const getCartesianChartModel = (
renderingContext,
);

const dataDensity = getCartesianChartDataDensity(
seriesModels,
stackModels,
transformedDataset,
seriesLabelsFormatters,
stackedLabelsFormatters,
settings,
renderingContext,
);

const { leftAxisModel, rightAxisModel } = getYAxesModels(
seriesModels,
transformedDataset,
Expand Down Expand Up @@ -187,5 +198,6 @@ export const getCartesianChartModel = (
trendLinesModel,
seriesLabelsFormatters,
stackedLabelsFormatters,
dataDensity,
};
};
257 changes: 257 additions & 0 deletions frontend/src/metabase/visualizations/echarts/cartesian/model/series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ import _ from "underscore";
import { NULL_DISPLAY_VALUE } from "metabase/lib/constants";
import { getDatasetKey } from "metabase/visualizations/echarts/cartesian/model/dataset";
import type {
CartesianChartDataDensity,
ChartDataset,
DataKey,
Datum,
DimensionModel,
LabelFormatter,
LegacySeriesSettingsObjectKey,
RawValueFormatter,
SeriesFormatters,
SeriesModel,
StackDisplay,
StackModel,
StackTotalDataKey,
StackedSeriesFormatters,
VizSettingsKey,
WaterFallChartDataDensity,
} from "metabase/visualizations/echarts/cartesian/model/types";
import type { CartesianChartColumns } from "metabase/visualizations/lib/graph/columns";
import { getFriendlyName } from "metabase/visualizations/lib/utils";
Expand All @@ -33,12 +37,14 @@ import type {
DatasetColumn,
RawSeries,
CardId,
SeriesSettings,
} from "metabase-types/api";

import {
NEGATIVE_STACK_TOTAL_DATA_KEY,
POSITIVE_STACK_TOTAL_DATA_KEY,
} from "../constants/dataset";
import { CHART_STYLE } from "../constants/style";
import { cachedFormatter } from "../utils/formatter";
import { WATERFALL_VALUE_KEY } from "../waterfall/constants";

Expand Down Expand Up @@ -327,6 +333,257 @@ function shouldRenderCompact(
return getAvgLength(true) + 3 < getAvgLength(false);
}

export function getWaterfallChartDataDensity(
dataset: ChartDataset,
waterfallLabelFormatter: RawValueFormatter | undefined,
settings: ComputedVisualizationSettings,
renderingContext: RenderingContext,
): WaterFallChartDataDensity {
const type = "waterfall";
if (
!settings["graph.show_values"] ||
settings["graph.label_value_frequency"] === "all"
) {
return {
type,
averageLabelWidth: 0,
totalNumberOfLabels: 0,
};
}

const totalNumberOfLabels = dataset.reduce((sum, datum) => {
const labelCount = datum[WATERFALL_VALUE_KEY] != null ? 1 : 0;

return sum + labelCount;
}, 0);

const fontStyle = {
family: renderingContext.fontFamily,
weight: CHART_STYLE.seriesLabels.weight,
size: CHART_STYLE.seriesLabels.size,
};
const sumOfLabelWidths = dataset.reduce((sum, datum) => {
const value = datum[WATERFALL_VALUE_KEY];

if (!waterfallLabelFormatter) {
return sum;
}

const labelWidth = renderingContext.measureText(
waterfallLabelFormatter(value),
fontStyle,
);

return sum + labelWidth;
}, 0);
const averageLabelWidth =
totalNumberOfLabels > 0 ? sumOfLabelWidths / totalNumberOfLabels : 0;

return {
type,
averageLabelWidth,
totalNumberOfLabels,
};
}

export function getCartesianChartDataDensity(
seriesModels: SeriesModel[],
stackModels: StackModel[],
dataset: ChartDataset,
seriesLabelsFormatters: SeriesFormatters,
stackedLabelsFormatters: StackedSeriesFormatters,
settings: ComputedVisualizationSettings,
renderingContext: RenderingContext,
): CartesianChartDataDensity {
const type = "cartesian";
const seriesSettingsByDataKey = getDisplaySeriesSettingsByDataKey(
seriesModels,
stackModels,
settings,
);
const seriesWithSymbols = seriesModels.filter(seriesModel => {
const seriesSettings = seriesSettingsByDataKey[seriesModel.dataKey];
return ["area", "line"].includes(seriesSettings.display ?? "");
});
const totalNumberOfDots = dataset.reduce((sum, datum) => {
const numOfDotsPerDatum = seriesWithSymbols.filter(
seriesModel => datum[seriesModel.dataKey] != null,
).length;

return sum + numOfDotsPerDatum;
}, 0);

const seriesDataKeysWithLabels: DataKey[] = [];
const stackedDisplayWithLabels: StackDisplay[] = [];
if (
!settings["graph.show_values"] ||
settings["graph.label_value_frequency"] === "all"
) {
return {
type,
seriesDataKeysWithLabels,
stackedDisplayWithLabels,
totalNumberOfDots,
averageLabelWidth: 0,
totalNumberOfLabels: 0,
};
}

const seriesWithLabels = seriesModels.filter(seriesModel => {
const seriesSettings = seriesSettingsByDataKey[seriesModel.dataKey];
if (
["area", "bar"].includes(seriesSettings.display ?? "") &&
settings["stackable.stack_type"] != null
) {
return false;
}

return seriesSettings["show_series_values"];
});
const totalNumberOfSeriesLabels =
seriesWithLabels.length > 0
? dataset.reduce((sum, datum) => {
const numOfLabelsPerDatum = seriesWithLabels.filter(
seriesModel => datum[seriesModel.dataKey] != null,
).length;

return sum + numOfLabelsPerDatum;
}, 0)
: 0;

const totalNumberOfStackedLabels =
settings["stackable.stack_type"] !== "normalized"
? dataset.reduce((sum, datum) => {
const numOfStackedLabelsPerDatum = stackModels.reduce(
(sum, stackModel) => {
const positiveStackCount =
getStackTotalValue(
datum,
stackModel.seriesKeys,
POSITIVE_STACK_TOTAL_DATA_KEY,
) !== null
? 1
: 0;
const negativeStackCount =
getStackTotalValue(
datum,
stackModel.seriesKeys,
NEGATIVE_STACK_TOTAL_DATA_KEY,
) !== null
? 1
: 0;

return sum + positiveStackCount + negativeStackCount;
},
0,
);

return sum + numOfStackedLabelsPerDatum;
}, 0)
: 0;
const totalNumberOfLabels =
totalNumberOfSeriesLabels + totalNumberOfStackedLabels;

seriesDataKeysWithLabels.push(
...seriesWithLabels.map(series => series.dataKey),
);

if (settings["stackable.stack_type"] !== "normalized") {
stackedDisplayWithLabels.push(
...stackModels.map(stackModel => stackModel.display),
);
}

const fontStyle = {
family: renderingContext.fontFamily,
weight: CHART_STYLE.seriesLabels.weight,
size: CHART_STYLE.seriesLabels.size,
};
const sumOfLabelWidths = dataset.reduce((sum, datum) => {
const sumOfSeriesLabelsWidths = seriesWithLabels.reduce(
(seriesSum, seriesModel) => {
const value = datum[seriesModel.dataKey];
const formatter = seriesLabelsFormatters[seriesModel.dataKey];

if (!formatter) {
return sum;
}

const labelWidth = renderingContext.measureText(
formatter(value),
fontStyle,
);

return seriesSum + labelWidth;
},
0,
);

const sumOfStackedSeriesLabelsWidths = stackModels.reduce(
(stackSum, stackModel) => {
const formatter = stackedLabelsFormatters[stackModel.display];
if (!formatter) {
return 0;
}

const stackValues = [
POSITIVE_STACK_TOTAL_DATA_KEY,
NEGATIVE_STACK_TOTAL_DATA_KEY,
].map(signKey => {
return getStackTotalValue(datum, stackModel.seriesKeys, signKey) ?? 0;
});
const sumOfLabelWidths = stackValues.reduce((sum, value) => {
const labelWidth = renderingContext.measureText(
formatter(value),
fontStyle,
);

return sum + labelWidth;
}, 0);

return stackSum + sumOfLabelWidths;
},
0,
);

return sum + sumOfSeriesLabelsWidths + sumOfStackedSeriesLabelsWidths;
}, 0);

const averageLabelWidth =
totalNumberOfLabels > 0 ? sumOfLabelWidths / totalNumberOfLabels : 0;

return {
type,
seriesDataKeysWithLabels,
stackedDisplayWithLabels,
totalNumberOfDots,
averageLabelWidth,
totalNumberOfLabels,
};
}

export function getDisplaySeriesSettingsByDataKey(
seriesModels: SeriesModel[],
stackModels: StackModel[] | null,
settings: ComputedVisualizationSettings,
) {
const seriesSettingsByKey = seriesModels.reduce((acc, seriesModel) => {
acc[seriesModel.dataKey] = settings.series(
seriesModel.legacySeriesSettingsObjectKey,
);
return acc;
}, {} as Record<DataKey, SeriesSettings>);

if (stackModels != null) {
stackModels.forEach(({ display, seriesKeys }) => {
seriesKeys.forEach(seriesKey => {
seriesSettingsByKey[seriesKey].display = display;
});
});
}

return seriesSettingsByKey;
}
export const getStackedLabelsFormatters = (
seriesModels: SeriesModel[],
stackModels: StackModel[],
Expand Down
Loading
Loading