Skip to content
Snippets Groups Projects
Unverified Commit c612f7d1 authored by Aleksandr Lesnenko's avatar Aleksandr Lesnenko Committed by GitHub
Browse files

pie chart stacked tooltip (#27638)

parent 69f49f05
No related branches found
No related tags found
No related merge requests found
......@@ -34,4 +34,4 @@ export const formatNumber = (number: number, options?: NumberFormatOptions) => {
};
export const formatPercent = (percent: number) =>
`${(100 * percent).toFixed(2)} %`;
`${(100 * percent).toFixed(percent === 1 ? 0 : 2)} %`;
import React from "react";
import React, { useMemo } from "react";
import { TooltipRow, TooltipTotalRow } from "../TooltipRow";
import type { StackedTooltipModel } from "../types";
import {
......@@ -17,12 +17,24 @@ const StackedDataTooltip = ({
headerTitle,
headerRows,
bodyRows = [],
grandTotal,
showTotal,
showPercentages,
totalFormatter = (value: unknown) => String(value),
}: StackedDataTooltipProps) => {
const total = getTotalValue(headerRows, bodyRows);
const rowsTotal = useMemo(
() => getTotalValue(headerRows, bodyRows),
[headerRows, bodyRows],
);
const isShowingTotalSensible = headerRows.length + bodyRows.length > 1;
const hasColorIndicators = useMemo(
() => [...bodyRows, ...headerRows].some(row => row.color != null),
[headerRows, bodyRows],
);
// For some charts such as PieChart we intentionally show only certain data rows that do not represent the full data.
// In order to calculate percentages correctly we provide the grand total value
const percentCalculationTotal = grandTotal ?? rowsTotal;
return (
<DataPointRoot>
......@@ -38,7 +50,7 @@ const StackedDataTooltip = ({
key={index}
isHeader
percent={
showPercentages ? getPercent(total, row.value) : undefined
showPercentages ? getPercent(rowsTotal, row.value) : undefined
}
{...row}
/>
......@@ -51,7 +63,7 @@ const StackedDataTooltip = ({
<TooltipRow
key={index}
percent={
showPercentages ? getPercent(total, row.value) : undefined
showPercentages ? getPercent(rowsTotal, row.value) : undefined
}
{...row}
/>
......@@ -62,8 +74,13 @@ const StackedDataTooltip = ({
{showTotal && isShowingTotalSensible && (
<DataPointTableFooter>
<TooltipTotalRow
value={totalFormatter(total)}
showPercentages={showPercentages}
value={totalFormatter(rowsTotal)}
hasIcon={hasColorIndicators}
percent={
showPercentages
? getPercent(percentCalculationTotal, rowsTotal)
: undefined
}
/>
</DataPointTableFooter>
)}
......
......@@ -42,19 +42,23 @@ export const TooltipRow = ({
interface TotalTooltipRow {
value: string;
showPercentages?: boolean;
percent?: number;
hasIcon?: boolean;
}
export const TooltipTotalRow = ({
value,
showPercentages,
percent,
hasIcon,
}: TotalTooltipRow) => (
<TotalRowRoot>
<Cell>=</Cell>
{hasIcon && <Cell>=</Cell>}
<Cell data-testid="row-name">{t`Total`}</Cell>
<ValueCell data-testid="row-value">{value}</ValueCell>
{showPercentages && (
<PercentCell data-testid="row-percent">100%</PercentCell>
{percent != null && (
<PercentCell data-testid="row-percent">
{formatPercent(percent)}
</PercentCell>
)}
</TotalRowRoot>
);
......@@ -38,6 +38,7 @@ export interface StackedTooltipModel {
totalFormatter?: (value: unknown) => string;
showTotal?: boolean;
showPercentages?: boolean;
grandTotal?: number;
}
export interface HoveredObject {
......
......@@ -24,10 +24,10 @@ import { formatValue } from "metabase/lib/formatting";
import { color } from "metabase/lib/colors";
import { getColorsForValues } from "metabase/lib/colors/charts";
import ChartWithLegend from "../../components/ChartWithLegend";
import ChartTooltip from "../../components/ChartTooltip";
import styles from "./PieChart.css";
import { PieArc } from "./PieArc";
import { getTooltipModel } from "./utils";
const SIDE_PADDING = 24;
const MAX_LABEL_FONT_SIZE = 20;
......@@ -40,8 +40,6 @@ const PAD_ANGLE = (Math.PI / 180) * 1; // 1 degree in radians
const SLICE_THRESHOLD = 0.025; // approx 1 degree in percentage
const OTHER_SLICE_MIN_PERCENTAGE = 0.003;
const PERCENT_REGEX = /percent/i;
export default class PieChart extends Component {
constructor(props) {
super(props);
......@@ -312,10 +310,6 @@ export default class PieChart extends Component {
const total = rows.reduce((sum, row) => sum + row[metricIndex], 0);
const showPercentInTooltip =
!PERCENT_REGEX.test(cols[metricIndex].name) &&
!PERCENT_REGEX.test(cols[metricIndex].display_name);
const sliceThreshold =
typeof settings["pie.slice_threshold"] === "number"
? settings["pie.slice_threshold"] / 100
......@@ -415,37 +409,34 @@ export default class PieChart extends Component {
const slice = slices[index];
if (!slice || slice.noHover) {
return null;
} else if (slice === otherSlice && others.length > 1) {
}
if (slice === otherSlice && others.length > 1) {
return {
index,
event: event && event.nativeEvent,
data: others.map(o => ({
key: formatDimension(o.key, false),
value: formatMetric(o.displayValue, false),
})),
stackedTooltipModel: getTooltipModel(
others.map(o => ({
key: formatDimension(o.key, false),
value: o.displayValue,
})),
null,
getFriendlyName(cols[dimensionIndex]),
formatDimension,
formatMetric,
total,
),
};
} else {
return {
index,
event: event && event.nativeEvent,
data: [
{
key: getFriendlyName(cols[dimensionIndex]),
value: formatDimension(slice.key),
},
{
key: getFriendlyName(cols[metricIndex]),
value: formatMetric(slice.displayValue),
},
].concat(
showPercentInTooltip && slice.percentage != null
? [
{
key: t`Percentage`,
value: formatPercent(slice.percentage, legendDecimals),
},
]
: [],
stackedTooltipModel: getTooltipModel(
slices,
index,
getFriendlyName(cols[dimensionIndex]),
formatDimension,
formatMetric,
),
};
}
......@@ -584,7 +575,6 @@ export default class PieChart extends Component {
</svg>
</div>
</div>
<ChartTooltip series={series} hovered={hovered} />
</ChartWithLegend>
);
}
......
import _ from "underscore";
import { StackedTooltipModel } from "metabase/visualizations/components/ChartTooltip/types";
export function getMaxLabelDimension(
d3Arc: d3.svg.Arc<d3.svg.arc.Arc>,
slice: d3.svg.arc.Arc,
......@@ -25,3 +28,40 @@ export function getMaxLabelDimension(
return Math.min(innerRadiusArcDistance, donutWidth);
}
interface SliceData {
key: string;
value: number;
color: string;
}
export const getTooltipModel = (
slices: SliceData[],
hoveredIndex: number | null,
dimensionColumnName: string,
dimensionFormatter: (value: unknown) => string,
metricFormatter: (value: unknown) => string,
grandTotal?: number,
): StackedTooltipModel => {
const rows = slices.map(slice => ({
name: dimensionFormatter(slice.key),
value: slice.value,
color: slice.color,
formatter: metricFormatter,
}));
const [headerRows, bodyRows] = _.partition(
rows,
(_, index) => index === hoveredIndex,
);
return {
headerTitle: dimensionColumnName,
headerRows,
bodyRows,
totalFormatter: metricFormatter,
grandTotal,
showTotal: true,
showPercentages: true,
};
};
import { getTooltipModel } from "./utils";
const slices = [
{
key: "foo",
value: 100,
color: "green",
},
{
key: "bar",
value: 200,
color: "red",
},
];
const dimensionColumnName = "dimension_column";
const dimensionFormatter = (value: unknown) => `dimension:${value}`;
const metricFormatter = (value: unknown) => `metric:${value}`;
describe("utils", () => {
describe("getTooltipModel", () => {
it("creates tooltip model", () => {
const { headerTitle, headerRows, bodyRows, showTotal, showPercentages } =
getTooltipModel(
slices,
0,
dimensionColumnName,
dimensionFormatter,
metricFormatter,
);
expect(headerTitle).toBe(dimensionColumnName);
expect(headerRows).toStrictEqual([
{
color: "green",
formatter: metricFormatter,
name: "dimension:foo",
value: 100,
},
]);
expect(bodyRows).toStrictEqual([
{
color: "red",
formatter: metricFormatter,
name: "dimension:bar",
value: 200,
},
]);
expect(showTotal).toBe(true);
expect(showPercentages).toBe(true);
});
});
});
......@@ -115,14 +115,13 @@ describe("pie chart", () => {
const otherPath = paths[paths.length - 1];
// condensed tooltips display as "dimension: metric"
expect(screen.queryByText("baz:")).not.toBeInTheDocument();
expect(screen.queryByText("qux:")).not.toBeInTheDocument();
expect(screen.queryByText("baz")).not.toBeInTheDocument();
expect(screen.queryByText("qux")).not.toBeInTheDocument();
fireEvent.mouseMove(otherPath);
// these appear twice in the dom due to some popover weirdness
expect(screen.getAllByText("baz:")).toHaveLength(2);
expect(screen.getAllByText("qux:")).toHaveLength(2);
expect(screen.getByText("baz")).toBeInTheDocument();
expect(screen.getByText("qux")).toBeInTheDocument();
});
it("shouldn't show a condensed tooltip for just one squashed slice", () => {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment