in packages/charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts [21:200]
export function renderHeatmapCanvas2d(ctx: CanvasRenderingContext2D, dpr: number, props: ReactiveChartStateProps) {
const { theme } = props.geometries;
const { heatmapViewModels } = props.geometries;
const {
theme: { sharedStyle: sharedGeometryStyle, chartPaddings: paddings, chartMargins: margins },
background,
elementSizes,
highlightedLegendBands,
chartContainerDimensions: container,
chartDimensions: chart,
debug,
} = props;
if (heatmapViewModels.length === 0) return;
withContext(ctx, () => {
// set some defaults for the overall rendering
// let's set the devicePixelRatio once and for all; then we'll never worry about it again
ctx.scale(dpr, dpr);
// all texts are currently center-aligned because
// - the calculations manually compute and lay out text (word) boxes, so we can choose whatever
// - but center/middle has mathematical simplicity and the most unassuming thing
// - due to using the math x/y convention (+y is up) while Canvas uses screen convention (+y is down)
// text rendering must be y-flipped, which is a bit easier this way
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.lineCap = 'square';
// ctx.translate(chartCenter.x, chartCenter.y);
// this applies the mathematical x/y conversion (+y is North) which is easier when developing geometry
// functions - also, all renderers have flexibility (eg. SVG scale) and WebGL NDC is also +y up
// - in any case, it's possible to refactor for a -y = North convention if that's deemed preferable
// ctx.scale(1, -1);
renderLayers(ctx, [
() => clearCanvas(ctx, background),
() => {
// Grid
heatmapViewModels.forEach(({ gridOrigin: { x, y }, gridLines }) => {
withContext(ctx, () => {
ctx.translate(x, y);
renderMultiLine(ctx, gridLines.x, gridLines.stroke);
renderMultiLine(ctx, gridLines.y, gridLines.stroke);
});
});
},
() =>
// Cells
heatmapViewModels.forEach(({ gridOrigin: { x, y }, cells }) => {
withContext(ctx, () => {
ctx.translate(x, y);
cells.forEach((cell) => {
if (cell.visible) {
const geometryStateStyle = getGeometryStateStyle(cell, sharedGeometryStyle, highlightedLegendBands);
const style = getColorBandStyle(cell, geometryStateStyle);
renderRect(ctx, cell, style.fill, style.stroke);
}
});
});
}),
// Text on cells
() => {
if (!theme.cell.label.visible) return;
heatmapViewModels.forEach(({ cellFontSize, gridOrigin: { x, y }, cells }) => {
withContext(ctx, () => {
ctx.translate(x, y);
cells.forEach((cell) => {
const fontSize = cellFontSize(cell);
if (cell.visible && Number.isFinite(fontSize))
renderText(ctx, { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 }, cell.formatted, {
...theme.cell.label,
fontSize,
align: 'center',
baseline: 'middle',
textColor: cell.textColor,
});
});
});
});
},
// render text on Y axis
() => {
if (!theme.yAxisLabel.visible) return;
heatmapViewModels.forEach(({ yValues, gridOrigin: { x, y } }) => {
withContext(ctx, () => {
ctx.translate(x, y);
const font: TextFont = {
...theme.yAxisLabel,
baseline: 'middle' /* fixed */,
align: 'right' /* fixed */,
};
const { padding } = theme.yAxisLabel;
const horizontalPadding = horizontalPad(padding);
yValues.forEach(({ x, y, text }) => {
const textLines = wrapLines(
ctx,
text,
font,
theme.yAxisLabel.fontSize,
Math.max(elementSizes.yAxis.width - horizontalPadding, 0),
theme.yAxisLabel.fontSize,
{ shouldAddEllipsis: true, wrapAtWord: false },
).lines;
// TODO improve the `wrapLines` code to handle results with short width
if (textLines[0]) renderText(ctx, { x, y }, textLines[0], font);
});
});
});
},
// render text on X axis
() => {
if (!theme.xAxisLabel.visible) return;
heatmapViewModels.forEach(({ xValues, gridOrigin: { x, y } }) => {
withContext(ctx, () => {
ctx.translate(x, y + elementSizes.xAxis.top);
xValues
.filter((_, i) => i % elementSizes.xAxisTickCadence === 0)
.forEach(({ x, y, text, align }) => {
const textLines = wrapLines(
ctx,
text,
theme.xAxisLabel,
theme.xAxisLabel.fontSize,
// TODO wrap into multilines
Infinity,
16,
{ shouldAddEllipsis: true, wrapAtWord: false },
).lines;
if (textLines[0]) {
renderText(
ctx,
{ x, y },
textLines[0],
{ ...theme.xAxisLabel, baseline: 'middle', align },
// negative rotation due to the canvas rotation direction
radToDeg(-elementSizes.xLabelRotation),
);
}
});
});
});
},
// render axes and panel titles
() =>
heatmapViewModels
.filter(({ titles }) => titles.length > 0)
.forEach(({ titles, gridOrigin: { x, y } }) => {
withContext(ctx, () => {
ctx.translate(x, y);
titles
.filter((t) => t.visible && t.text !== '')
.forEach((title) => {
renderText(
ctx,
title.origin,
title.text,
{
...title,
baseline: 'middle',
align: 'center',
},
title.rotation,
);
});
});
}),
() => debug && renderHeatmapDebugElements({ ctx, container, chart, margins, paddings }),
]);
});
}