Skip to content
Snippets Groups Projects
Unverified Commit f53f2f79 authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Dynamically-sized pivot-table header cells (#27594)

* add PivotTable unit tests

* allow dynamic pivot table header width

* access settings using metabase/lib

* fix comments
parent 40cfd021
No related branches found
No related tags found
No related merge requests found
......@@ -8,6 +8,7 @@ import { Grid, Collection, ScrollSync, AutoSizer } from "react-virtualized";
import { findDOMNode } from "react-dom";
import { connect } from "react-redux";
import { getScrollBarSize } from "metabase/lib/dom";
import { getSetting } from "metabase/selectors/settings";
import ChartSettingsTableFormatting from "metabase/visualizations/components/settings/ChartSettingsTableFormatting";
import {
......@@ -34,7 +35,6 @@ import { Cell } from "./PivotTableCell";
import {
PivotTableRoot,
PivotTableTopLeftCellsContainer,
CELL_HEIGHT,
} from "./PivotTable.styled";
import { partitions } from "./partitions";
......@@ -43,16 +43,14 @@ import {
isColumnValid,
isFormattablePivotColumn,
updateValueWithCurrentColumns,
getLeftHeaderWidths,
} from "./utils";
// cell width and height for normal body cells
const CELL_WIDTH = 100;
// the left header has a wider cell width and some additional spacing on the left to align with the title
const LEFT_HEADER_LEFT_SPACING = 24;
const LEFT_HEADER_CELL_WIDTH = 145;
import { CELL_WIDTH, CELL_HEIGHT, LEFT_HEADER_LEFT_SPACING } from "./constants";
const mapStateToProps = state => ({
hasCustomColors: PLUGIN_SELECTORS.getHasCustomColors(state),
fontFamily: getSetting(state, "application-font"),
});
class PivotTable extends Component {
......@@ -316,6 +314,7 @@ class PivotTable extends Component {
onUpdateVisualizationSettings,
isNightMode,
isDashboard,
fontFamily,
} = this.props;
if (data == null || !data.cols.some(isPivotGroupColumn)) {
return null;
......@@ -358,6 +357,12 @@ class PivotTable extends Component {
valueIndexes,
} = pivoted;
const { leftHeaderWidths, totalHeaderWidths } = getLeftHeaderWidths({
rowIndexes: rowIndexes ?? [],
getColumnTitle: idx => this.getColumnTitle(idx),
fontFamily: fontFamily,
});
const leftHeaderCellRenderer = ({ index, key, style }) => {
const { value, isSubtotal, hasSubtotal, depth, path, clicked } =
leftHeaderItems[index];
......@@ -392,13 +397,23 @@ class PivotTable extends Component {
};
const leftHeaderCellSizeAndPositionGetter = ({ index }) => {
const { offset, span, depth, maxDepthBelow } = leftHeaderItems[index];
const columnsToSpan = rowIndexes.length - depth - maxDepthBelow;
// add up all the widths of the columns, other than itself, that this cell spans
const spanWidth = leftHeaderWidths
.slice(depth + 1, depth + columnsToSpan)
.reduce((acc, cellWidth) => acc + cellWidth, 0);
const columnPadding = depth === 0 ? LEFT_HEADER_LEFT_SPACING : 0;
const columnWidth = leftHeaderWidths[depth];
return {
height: span * CELL_HEIGHT,
width:
(rowIndexes.length - depth - maxDepthBelow) * LEFT_HEADER_CELL_WIDTH +
(depth === 0 ? LEFT_HEADER_LEFT_SPACING : 0),
width: columnWidth + spanWidth + columnPadding,
x:
depth * LEFT_HEADER_CELL_WIDTH +
leftHeaderWidths
.slice(0, depth)
.reduce((acc, cellWidth) => acc + cellWidth, 0) +
(depth > 0 ? LEFT_HEADER_LEFT_SPACING : 0),
y: offset * CELL_HEIGHT,
};
......@@ -437,9 +452,7 @@ class PivotTable extends Component {
};
const leftHeaderWidth =
rowIndexes.length > 0
? LEFT_HEADER_LEFT_SPACING + rowIndexes.length * LEFT_HEADER_CELL_WIDTH
: 0;
rowIndexes.length > 0 ? LEFT_HEADER_LEFT_SPACING + totalHeaderWidths : 0;
// These are tied to the `multiLevelPivot` call, so they're awkwardly shoved in render for now
......@@ -490,7 +503,10 @@ class PivotTable extends Component {
isNightMode={isNightMode}
value={this.getColumnTitle(rowIndex)}
style={{
width: LEFT_HEADER_CELL_WIDTH,
flex: "0 0 auto",
width:
leftHeaderWidths?.[index] +
(index === 0 ? LEFT_HEADER_LEFT_SPACING : 0),
...(index === 0
? { paddingLeft: LEFT_HEADER_LEFT_SPACING }
: {}),
......
......@@ -2,7 +2,7 @@ import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { color, alpha, darken } from "metabase/lib/colors";
export const CELL_HEIGHT = 30;
import { CELL_HEIGHT, PIVOT_TABLE_FONT_SIZE } from "./constants";
export const RowToggleIconRoot = styled.div`
display: flex;
......@@ -102,7 +102,7 @@ interface PivotTableRootProps {
export const PivotTableRoot = styled.div<PivotTableRootProps>`
height: 100%;
font-size: 0.875em;
font-size: ${PIVOT_TABLE_FONT_SIZE};
${props =>
props.isDashboard
......
// cell width and height for normal body cells
export const CELL_WIDTH = 100;
export const CELL_HEIGHT = 30;
// styling and cell text measurement depend on this font size
export const PIVOT_TABLE_FONT_SIZE = "0.75rem";
// values for computing header width
export const ROW_TOGGLE_ICON_WIDTH = 24;
export const CELL_PADDING = 16;
export const MIN_HEADER_CELL_WIDTH = 80;
const MAX_HEADER_CONTENT_WIDTH = 200;
export const MAX_HEADER_CELL_WIDTH =
MAX_HEADER_CONTENT_WIDTH + CELL_PADDING + ROW_TOGGLE_ICON_WIDTH;
// the left header has some additional padding on the left to align with the title
export const LEFT_HEADER_LEFT_SPACING = 24;
......@@ -2,12 +2,21 @@ import _ from "underscore";
import { getIn } from "icepick";
import { isPivotGroupColumn } from "metabase/lib/data_grid";
import { measureText } from "metabase/lib/measure-text";
import type { Column } from "metabase-types/types/Dataset";
import type { Card } from "metabase-types/types/Card";
import type { PivotSetting, FieldOrAggregationReference } from "./types";
import { partitions } from "./partitions";
import {
ROW_TOGGLE_ICON_WIDTH,
CELL_PADDING,
MIN_HEADER_CELL_WIDTH,
MAX_HEADER_CELL_WIDTH,
PIVOT_TABLE_FONT_SIZE,
} from "./constants";
// adds or removes columns from the pivot settings based on the current query
export function updateValueWithCurrentColumns(
storedValue: PivotSetting,
......@@ -75,3 +84,42 @@ export function isColumnValid(col: Column) {
export function isFormattablePivotColumn(column: Column) {
return column.source === "aggregation";
}
interface GetLeftHeaderWidthsProps {
rowIndexes: number[];
getColumnTitle: (columnIndex: number) => string;
fontFamily?: string;
}
export function getLeftHeaderWidths({
rowIndexes,
getColumnTitle,
fontFamily = "Lato",
}: GetLeftHeaderWidthsProps) {
const widths = rowIndexes.map(rowIndex => {
const computedWidth =
Math.ceil(
measureText(getColumnTitle(rowIndex), {
weight: "bold",
family: fontFamily,
size: PIVOT_TABLE_FONT_SIZE,
}),
) +
ROW_TOGGLE_ICON_WIDTH +
CELL_PADDING;
if (computedWidth > MAX_HEADER_CELL_WIDTH) {
return MAX_HEADER_CELL_WIDTH;
}
if (computedWidth < MIN_HEADER_CELL_WIDTH) {
return MIN_HEADER_CELL_WIDTH;
}
return computedWidth;
});
const total = widths.reduce((acc, width) => acc + width, 0);
return { leftHeaderWidths: widths, totalHeaderWidths: total };
}
......@@ -7,8 +7,11 @@ import {
isFormattablePivotColumn,
updateValueWithCurrentColumns,
addMissingCardBreakouts,
getLeftHeaderWidths,
} from "./utils";
import { MAX_HEADER_CELL_WIDTH, MIN_HEADER_CELL_WIDTH } from "./constants";
describe("Visualizations > Visualizations > PivotTable > utils", () => {
const cols = [
{ source: "field", field_ref: ["field", 123, null], name: "field-123" },
......@@ -227,4 +230,41 @@ describe("Visualizations > Visualizations > PivotTable > utils", () => {
expect(result).toEqual(newPivotSettings);
});
});
describe("getLeftHeaderWidths", () => {
it("should return an array of widths", () => {
const { leftHeaderWidths } = getLeftHeaderWidths({
rowIndexes: [0, 1, 2],
getColumnTitle: () => "test-123",
});
// jest-dom thinks all characters are 1px wide, so we get the minimum
expect(leftHeaderWidths).toEqual([
MIN_HEADER_CELL_WIDTH,
MIN_HEADER_CELL_WIDTH,
MIN_HEADER_CELL_WIDTH,
]);
});
it("should return the total of all widths", () => {
const { totalHeaderWidths } = getLeftHeaderWidths({
rowIndexes: [0, 1, 2],
getColumnTitle: () => "test-123",
});
expect(totalHeaderWidths).toEqual(MIN_HEADER_CELL_WIDTH * 3);
});
it("should not exceed the max width", () => {
const { leftHeaderWidths } = getLeftHeaderWidths({
rowIndexes: [0, 1, 2],
// jest-dom thinks characters are 1px wide
getColumnTitle: () => "x".repeat(MAX_HEADER_CELL_WIDTH),
});
expect(leftHeaderWidths).toEqual([
MAX_HEADER_CELL_WIDTH,
MAX_HEADER_CELL_WIDTH,
MAX_HEADER_CELL_WIDTH,
]);
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment