diff --git a/__tests__/integration/snapshots/static/HeatmapHeatmapBasic.png b/__tests__/integration/snapshots/static/HeatmapHeatmapBasic.png index 5bcd9e9bed..48506c5d21 100644 Binary files a/__tests__/integration/snapshots/static/HeatmapHeatmapBasic.png and b/__tests__/integration/snapshots/static/HeatmapHeatmapBasic.png differ diff --git a/__tests__/integration/utils/createNodeGCanvas.ts b/__tests__/integration/utils/createNodeGCanvas.ts index df394cc1a9..4d0740d91c 100644 --- a/__tests__/integration/utils/createNodeGCanvas.ts +++ b/__tests__/integration/utils/createNodeGCanvas.ts @@ -1,4 +1,4 @@ -import { createCanvas } from 'canvas'; +import { createCanvas, Image } from 'canvas'; import { Canvas } from '@antv/g'; import { Renderer } from '@antv/g-canvas'; import { Plugin as DragAndDropPlugin } from '@antv/g-plugin-dragndrop'; @@ -22,5 +22,9 @@ export function createNodeGCanvas(width: number, height: number): Canvas { canvas: nodeCanvas as any, renderer, offscreenCanvas: offscreenNodeCanvas as any, + createImage: () => { + const image = new Image(); + return image as any; + }, }); } diff --git a/__tests__/integration/utils/renderSpec.ts b/__tests__/integration/utils/renderSpec.ts index 827a3584b3..1b67c132ac 100644 --- a/__tests__/integration/utils/renderSpec.ts +++ b/__tests__/integration/utils/renderSpec.ts @@ -1,4 +1,5 @@ import { Canvas } from '@antv/g'; +import { createCanvas } from 'canvas'; import { G2Context, G2Spec, render } from '../../../src'; import { renderToMountedElement } from '../../utils/renderToMountedElement'; import { createNodeGCanvas } from './createNodeGCanvas'; @@ -14,6 +15,11 @@ export async function renderSpec( const renderFunction = mounted ? renderToMountedElement : render; const options = preprocess({ ...raw, width, height }); context.canvas = gCanvas; + context.createCanvas = () => { + // The width attribute defaults to 300, and the height attribute defaults to 150. + // @see https://stackoverflow.com/a/12019582 + return createCanvas(300, 150) as unknown as HTMLCanvasElement; + }; await new Promise((resolve) => // @ts-ignore renderFunction({ theme: 'classic', ...options }, context, resolve), diff --git a/src/runtime/plot.ts b/src/runtime/plot.ts index ddfa7671ff..38e69373e8 100644 --- a/src/runtime/plot.ts +++ b/src/runtime/plot.ts @@ -188,12 +188,12 @@ export async function plot( .attr('id', (view) => view.key) .call(applyTranslate) .each(function (view) { - plotView(view, select(this), transitions, library); + plotView(view, select(this), transitions, library, context); enterContainer.set(view, this); }), (update) => update.call(applyTranslate).each(function (view) { - plotView(view, select(this), transitions, library); + plotView(view, select(this), transitions, library, context); updateContainer.set(view, this); }), (exit) => @@ -305,7 +305,7 @@ function createUpdateView( return async (newOptions) => { const transitions = []; const [newView, newChildren] = await initializeView(newOptions, library); - plotView(newView, selection, transitions, library); + plotView(newView, selection, transitions, library, context); updateTooltip(selection, newOptions, newView, library, context); for (const child of newChildren) { plot(child, selection, library, context); @@ -645,6 +645,7 @@ async function plotView( selection: Selection, transitions: GAnimation[], library: G2Library, + context: G2Context, ): Promise { const { components, theme, layout, markState, coordinate, key, style, clip } = view; @@ -783,7 +784,13 @@ async function plotView( const { data } = state; const { key, class: cls, type } = mark; const viewNode = selection.select(`#${key}`); - const shapeFunction = createMarkShapeFunction(mark, state, view, library); + const shapeFunction = createMarkShapeFunction( + mark, + state, + view, + library, + context, + ); const enterFunction = createEnterFunction(mark, state, view, library); const updateFunction = createUpdateFunction(mark, state, view, library); const exitFunction = createExitFunction(mark, state, view, library); @@ -1199,6 +1206,7 @@ function createMarkShapeFunction( state: G2MarkState, view: G2ViewDescriptor, library: G2Library, + context: G2Context, ): ( data: Record, index: number, @@ -1236,7 +1244,7 @@ function createMarkShapeFunction( ...visualStyle, type: shapeName(mark, shape), }); - return shapeFunction(points, value, coordinate, theme, point2d); + return shapeFunction(points, value, coordinate, theme, point2d, context); }; } diff --git a/src/runtime/types/component.ts b/src/runtime/types/component.ts index ec5ceec6aa..b01e514746 100644 --- a/src/runtime/types/component.ts +++ b/src/runtime/types/component.ts @@ -14,7 +14,7 @@ import { import { DataComponent } from './data'; import { Encode, EncodeComponent } from './encode'; import { Mark, MarkComponent } from './mark'; -import { G2ViewTree, G2Library, G2Mark } from './options'; +import { G2ViewTree, G2Library, G2Mark, G2Context } from './options'; import { Transform, TransformComponent } from './transform'; export type G2ComponentNamespaces = @@ -145,6 +145,7 @@ export type Shape = ( coordinate: Coordinate, theme: G2Theme, point2d?: Vector2[][], + context?: G2Context, ) => DisplayObject; export type ShapeProps = { defaultMarker?: string; diff --git a/src/runtime/types/options.ts b/src/runtime/types/options.ts index 6098a80acb..8bd20cafe4 100644 --- a/src/runtime/types/options.ts +++ b/src/runtime/types/options.ts @@ -57,6 +57,11 @@ export type G2Context = { group?: DisplayObject; animations?: GAnimation[]; views?: G2ViewDescriptor[]; + /** + * Tell G2 how to create a canvas-like element, some marks will use it later such as wordcloud & heatmap. + * Use `document.createElement('canvas')` instead if not provided. + */ + createCanvas?: () => HTMLCanvasElement; }; export type G2View = { diff --git a/src/shape/heatmap/heatmap.ts b/src/shape/heatmap/heatmap.ts index 6111fedc17..25555a585c 100644 --- a/src/shape/heatmap/heatmap.ts +++ b/src/shape/heatmap/heatmap.ts @@ -2,7 +2,7 @@ import { max as d3max, min as d3min } from 'd3-array'; import { Image as GImage } from '@antv/g'; import { applyStyle, getShapeTheme } from '../utils'; import { select } from '../../utils/selection'; -import { ShapeComponent as SC, Vector2 } from '../../runtime'; +import { ShapeComponent as SC } from '../../runtime'; import { HeatmapRenderer } from './renderer'; import type { HeatmapRendererOptions } from './renderer/types'; @@ -10,8 +10,8 @@ export type HeatmapOptions = HeatmapRendererOptions; export const Heatmap: SC = (options) => { const { ...style } = options; - return (points: number[][], value, coordinate, theme) => { - const { mark, shape, defaultShape, color, transform } = value; + return (points: number[][], value, coordinate, theme, _, context) => { + const { mark, shape, defaultShape, transform } = value; const { defaultColor, fill = defaultColor, @@ -29,7 +29,16 @@ export const Heatmap: SC = (options) => { const min = d3min(points, (p) => p[2]); const max = d3max(points, (p) => p[2]); - const ctx = HeatmapRenderer(width, height, min, max, data, { ...style }); + const { createCanvas } = context; + const ctx = HeatmapRenderer( + width, + height, + min, + max, + data, + { ...style }, + createCanvas, + ); return select(new GImage()) .call(applyStyle, shapeTheme) diff --git a/src/shape/heatmap/renderer/index.ts b/src/shape/heatmap/renderer/index.ts index dcfb8fc9dc..b0420c700a 100644 --- a/src/shape/heatmap/renderer/index.ts +++ b/src/shape/heatmap/renderer/index.ts @@ -6,8 +6,14 @@ import { HeatmapRendererData, HeatmapRendererOptions } from './types'; * @param blurFactor * @returns */ -function getPointTemplate(radius: number, blurFactor: number) { - const tplCanvas = document.createElement('canvas'); +function getPointTemplate( + radius: number, + blurFactor: number, + createCanvas?: () => HTMLCanvasElement, +) { + const tplCanvas = createCanvas + ? createCanvas() + : document.createElement('canvas'); const tplCtx = tplCanvas.getContext('2d'); const x = radius; const y = radius; @@ -67,6 +73,7 @@ function drawAlpha( max: number, data: HeatmapRendererData[], options: HeatmapRendererOptions, + createCanvas?: () => HTMLCanvasElement, ) { const { blur } = options; let len = data.length; @@ -78,7 +85,7 @@ function drawAlpha( const rectY = y - radius; // TODO: cache for performance. - const tpl = getPointTemplate(radius, blur); + const tpl = getPointTemplate(radius, blur, createCanvas); // Value from minimum / value range, => [0, 1]. const templateAlpha = (value - min) / (max - min); // Small values are not visible because globalAlpha < .01 cannot be read from imageData. @@ -139,6 +146,7 @@ export function HeatmapRenderer( max: number, data: HeatmapRendererData[], options: HeatmapRendererOptions, + createCanvas?: () => HTMLCanvasElement, ) { const opts = { blur: 0.5, @@ -154,15 +162,19 @@ export function HeatmapRenderer( minOpacity: (options.opacity || 0) * 255, }; - const canvas = document.createElement('canvas'); - const shadowCanvas = document.createElement('canvas'); + const canvas = createCanvas + ? createCanvas() + : document.createElement('canvas'); + const shadowCanvas = createCanvas + ? createCanvas() + : document.createElement('canvas'); const ctx = canvas.getContext('2d'); const shadowCtx = shadowCanvas.getContext('2d'); const palette = getColorPalette(opts.gradient); - drawAlpha(shadowCtx, min, max, data, opts); + drawAlpha(shadowCtx, min, max, data, opts, createCanvas); const img = colorize(shadowCtx, width, height, palette, opts); ctx.putImageData(img, 0, 0);