export function renderHeatmapCanvas2d()

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 }),
    ]);
  });
}