Skip to content

Commit

Permalink
feat(heatmap): reduce font size to fit label within cells (#1352)
Browse files Browse the repository at this point in the history
This commit introduces the ability to scale the font size of numeric values within the heatmap cells if the label doesn't fit within the label using the default font size. The resulting font size is constrained within a user-configured range. If the label doesn't fit using the min font size, the label is not rendered. The default configuration uses a global min font size to avoid too many fontSize changes.

BREAKING CHANGE: the `config.label.fontSize` prop is replaced by `config.label.minFontSize` and `config.label.maxFontSize`. You can specify the same value for both properties to have a fixed font size. The `config.label.align` and `config.label.baseline` props are removed from the `HeatmapConfig` object.
  • Loading branch information
markov00 authored Sep 8, 2021
1 parent 4c267f4 commit 16b5546
Show file tree
Hide file tree
Showing 14 changed files with 110 additions and 34 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions integration/tests/heatmap_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,14 @@ describe('Heatmap stories', () => {
{ left: 300, top: 300 },
);
});
it('should maximize the label with an unique fontSize', async () => {
await page.setViewport({ width: 450, height: 600 });
await common.expectChartAtUrlToMatchScreenshot('http://localhost:9001/?path=/story/heatmap-alpha--categorical');
});
it('should maximize the label fontSize', async () => {
await page.setViewport({ width: 420, height: 600 });
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/heatmap-alpha--categorical&knob-use global min fontSize_labels=false',
);
});
});
13 changes: 8 additions & 5 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,8 @@ export interface Cell {
// (undocumented)
fill: Fill;
// (undocumented)
fontSize: Pixels;
// (undocumented)
formatted: string;
// (undocumented)
height: number;
Expand Down Expand Up @@ -958,10 +960,10 @@ export interface HeatmapConfig {
maxHeight: Pixels | 'fill';
align: 'center';
label: Font & {
fontSize: Pixels;
minFontSize: Pixels;
maxFontSize: Pixels;
useGlobalMinFontSize: boolean;
maxWidth: Pixels | 'fill';
align: TextAlign;
baseline: TextBaseline;
visible: boolean;
};
border: {
Expand Down Expand Up @@ -1007,8 +1009,6 @@ export interface HeatmapConfig {
onBrushEnd?: (brushArea: HeatmapBrushEvent) => void;
// (undocumented)
timeZone: string;
// Warning: (ae-forgotten-export) The symbol "Pixels" needs to be exported by the entry point index.d.ts
//
// (undocumented)
width: Pixels;
// Warning: (ae-forgotten-export) The symbol "Font" needs to be exported by the entry point index.d.ts
Expand Down Expand Up @@ -1506,6 +1506,9 @@ export const PATH_KEY = "path";
// @public (undocumented)
export function pathAccessor(n: ArrayEntry): LegendPath;

// @public (undocumented)
export type Pixels = number;

// @public
export const Placement: Readonly<{
Top: "top";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ export const config: Config = {
label: {
visible: true,
maxWidth: 'fill',
fontSize: 10,
minFontSize: 8,
maxFontSize: 12,
fontFamily: 'Sans-Serif',
fontStyle: 'normal',
textColor: 'black',
fontVariant: 'normal',
fontWeight: 'normal',
textOpacity: 1,
align: 'center' as CanvasTextAlign,
baseline: 'verticalAlign' as CanvasTextBaseline,
useGlobalMinFontSize: true,
},
border: {
strokeWidth: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ export interface Config {
maxHeight: Pixels | 'fill';
align: 'center';
label: Font & {
fontSize: Pixels;
minFontSize: Pixels;
maxFontSize: Pixels;
useGlobalMinFontSize: boolean;
maxWidth: Pixels | 'fill';
align: TextAlign;
baseline: TextBaseline;
visible: boolean;
};
border: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface Cell {
formatted: string;
visible: boolean;
datum: HeatmapCellDatum;
fontSize: Pixels;
}

/** @internal */
Expand All @@ -57,6 +58,7 @@ export interface HeatmapViewModel {
stroke: Stroke;
};
cells: Cell[];
cellFontSize: (c: Cell) => Pixels;
xValues: Array<TextBox>;
yValues: Array<TextBox>;
pageSize: number;
Expand Down Expand Up @@ -116,6 +118,7 @@ export const nullHeatmapViewModel: HeatmapViewModel = {
xValues: [],
yValues: [],
pageSize: 0,
cellFontSize: () => 0,
};

/** @internal */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { scaleBand, scaleQuantize } from 'd3-scale';

import { stringToRGB } from '../../../../common/color_library_wrappers';
import { Pixels } from '../../../../common/geometry';
import { Box, TextMeasure } from '../../../../common/text_utils';
import { Box, maximiseFontSize, TextMeasure } from '../../../../common/text_utils';
import { ScaleContinuous } from '../../../../scales';
import { ScaleType } from '../../../../scales/constants';
import { SettingsSpec } from '../../../../specs';
Expand Down Expand Up @@ -177,6 +177,9 @@ export function shapeViewModel(
};
});

const cellWidthInner = cellWidth - gridStrokeWidth * 2;
const cellHeightInner = cellHeight - gridStrokeWidth * 2;

// compute each available cell position, color and value
const cellMap = table.reduce<Record<string, Cell>>((acc, d) => {
const x = xScale(String(d.x));
Expand All @@ -187,13 +190,27 @@ export function shapeViewModel(
return acc;
}
const cellKey = getCellKey(d.x, d.y);

const formattedValue = spec.valueFormatter(d.value);

const fontSize = maximiseFontSize(
textMeasure,
formattedValue,
config.cell.label,
config.cell.label.minFontSize,
config.cell.label.maxFontSize,
// adding 3px padding per side to avoid that text touches the edges
cellWidthInner - 6,
cellHeightInner - 6,
);

acc[cellKey] = {
x:
(config.cell.maxWidth !== 'fill' ? x + xScale.bandwidth() / 2 - config.cell.maxWidth / 2 : x) + gridStrokeWidth,
y,
yIndex,
width: cellWidth - gridStrokeWidth * 2,
height: cellHeight - gridStrokeWidth * 2,
width: cellWidthInner,
height: cellHeightInner,
datum: d,
fill: {
color: stringToRGB(color),
Expand All @@ -204,7 +221,8 @@ export function shapeViewModel(
},
value: d.value,
visible: !isValueHidden(d.value, bandsToHide),
formatted: spec.valueFormatter(d.value),
formatted: formattedValue,
fontSize,
};
return acc;
}, {});
Expand Down Expand Up @@ -355,6 +373,9 @@ export function shapeViewModel(
yLines.push({ x1: chartDimensions.left, y1: y, x2: chartDimensions.width + chartDimensions.left, y2: y });
}

const cells = Object.values(cellMap);
const tableMinFontSize = cells.reduce((acc, { fontSize }) => Math.min(acc, fontSize), Infinity);

return {
config,
heatmapViewModel: {
Expand All @@ -371,7 +392,8 @@ export function shapeViewModel(
},
},
pageSize,
cells: Object.values(cellMap),
cells,
cellFontSize: (cell: Cell) => (config.cell.label.useGlobalMinFontSize ? tableMinFontSize : cell.fontSize),
xValues: textXValues,
yValues: textYValues,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,14 @@ export function renderCanvas2d(
const { x, y } = heatmapViewModel.gridOrigin;
ctx.translate(x, y);
filteredCells.forEach((cell) => {
if (cell.visible)
renderText(
ctx,
{ x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 },
cell.formatted,
config.cell.label,
);
const fontSize = heatmapViewModel.cellFontSize(cell);
if (cell.visible && Number.isFinite(fontSize))
renderText(ctx, { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 }, cell.formatted, {
...config.cell.label,
fontSize,
align: 'center',
baseline: 'middle',
});
});
}),

Expand Down
19 changes: 19 additions & 0 deletions packages/charts/src/common/text_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,22 @@ export function fitText(
const { width } = measure(fontSize, [{ ...font, text }])[0];
return { width, text };
}

/** @internal */
export function maximiseFontSize(
measure: TextMeasure,
text: string,
font: Font,
minFontSize: Pixels,
maxFontSize: Pixels,
boxWidth: Pixels,
boxHeight: Pixels,
): Pixels {
const response = (fontSize: number) => {
const [{ width }] = measure(fontSize, [{ text, ...font }]);
const widthDiff = boxWidth - width;
const heightDiff = boxHeight - fontSize;
return -Math.min(widthDiff, heightDiff);
};
return monotonicHillClimb(response, maxFontSize, 0, integerSnap, minFontSize);
}
2 changes: 1 addition & 1 deletion packages/charts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,6 @@ export { DataGenerator } from './utils/data_generators/data_generator';
export * from './utils/themes/merge_utils';
export { MODEL_KEY } from './chart_types/partition_chart/layout/config';
export { LegendStrategy } from './chart_types/partition_chart/layout/utils/highlighted_geoms';
export { Ratio } from './common/geometry';
export { Pixels, Ratio } from './common/geometry';
export { AdditiveNumber } from './utils/accessor';
export { FontStyle, FONT_STYLES } from './common/text_utils';
3 changes: 3 additions & 0 deletions storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ module.exports = {
reactOptions: {
fastRefresh: true,
},
typescript: {
reactDocgen: false,
},
};
35 changes: 25 additions & 10 deletions storybook/stories/heatmap/2_categorical.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { action } from '@storybook/addon-actions';
import { boolean } from '@storybook/addon-knobs';
import { boolean, number } from '@storybook/addon-knobs';
import React from 'react';

import { Chart, Heatmap, Settings } from '@elastic/charts';
Expand All @@ -16,8 +16,17 @@ import { BABYNAME_DATA } from '@elastic/charts/src/utils/data_samples/babynames'
import { useBaseTheme } from '../../use_base_theme';

export const Example = () => {
const filterData = boolean('filter dataset', true);
const data = filterData ? BABYNAME_DATA.filter(([year]) => year > 1950 && year < 1960) : BABYNAME_DATA;
const data = boolean('filter dataset', true)
? BABYNAME_DATA.filter(([year]) => year > 1950 && year < 1960)
: BABYNAME_DATA;
const showLabels = boolean('show', true, 'labels');
const useGlobalMinFontSize = boolean('use global min fontSize', true, 'labels');

const minFontSize = number('min fontSize', 6, { step: 1, min: 4, max: 10, range: true }, 'labels');
const maxFontSize = number('max fontSize', 12, { step: 1, min: 10, max: 64, range: true }, 'labels');

const minCellHeight = number('min cell height', 10, { step: 1, min: 3, max: 8, range: true }, 'grid');
const maxCellHeight = number('max cell height', 30, { step: 1, min: 8, max: 45, range: true }, 'grid');

return (
<Chart>
Expand All @@ -33,11 +42,11 @@ export const Example = () => {
colorScale={{
type: 'bands',
bands: [
{ start: -Infinity, end: 1000, color: '#ffffcc' },
{ start: 1000, end: 5000, color: '#a1dab4' },
{ start: 5000, end: 10000, color: '#41b6c4' },
{ start: 10000, end: 50000, color: '#2c7fb8' },
{ start: 50000, end: Infinity, color: '#253494' },
{ start: -Infinity, end: 1000, color: '#AADC32' },
{ start: 1000, end: 5000, color: '#35B779' },
{ start: 5000, end: 10000, color: '#24868E' },
{ start: 10000, end: 50000, color: '#3B528B' },
{ start: 50000, end: Infinity, color: '#471164' },
],
}}
data={data}
Expand All @@ -51,12 +60,18 @@ export const Example = () => {
stroke: {
width: 0,
},
cellHeight: {
min: minCellHeight,
max: maxCellHeight,
},
},
cell: {
maxWidth: 'fill',
maxHeight: 20,
label: {
visible: true,
minFontSize,
maxFontSize,
visible: showLabels,
useGlobalMinFontSize,
},
border: {
stroke: 'transparent',
Expand Down

0 comments on commit 16b5546

Please sign in to comment.