antvis/G2

SSR (Server-Side Rendering & Canvas) #5641

JonasJonny posted onGitHub

G2 Version: "@antv/g2": "^5.1.5",

Good evening, I wanted to try SSR with Canvas but without luck. I followed test code

function createNodeGCanvas(width, height) {
  // Create a node-canvas instead of HTMLCanvasElement
  const nodeCanvas = createCanvas(width, height);
  // A standalone offscreen canvas for text metrics
  const offscreenNodeCanvas = createCanvas(1, 1);

  // Create a renderer, unregister plugin relative to DOM.
  const renderer = new Renderer();
  // Remove html plugin to ssr.
  const htmlRendererPlugin = renderer.getPlugin('html-renderer');
  renderer.unregisterPlugin(htmlRendererPlugin);
  const domInteractionPlugin = renderer.getPlugin('dom-interaction');
  renderer.unregisterPlugin(domInteractionPlugin);

  renderer.registerPlugin(
    new DragAndDropPlugin.Plugin({ dragstartDistanceThreshold: 10 }),
  );

  return new Canvas({
    width,
    height,
    canvas: nodeCanvas,
    renderer,
    offscreenCanvas: offscreenNodeCanvas,
  });
}

async function renderG2BySSR() {
  const width = 600;
  const height = 400;

  const gCanvas = createNodeGCanvas(width, height);

  // A tabular data to be visualized.
  const data = [
    { genre: 'Sports', sold: 275 },
    { genre: 'Strategy', sold: 115 },
    { genre: 'Action', sold: 120 },
    { genre: 'Shooter', sold: 350 },
    { genre: 'Other', sold: 150 },
  ];

  const chart = new Chart({
    width,
    height,
    canvas: gCanvas,
    createCanvas: () => {
      return createCanvas(width, height);
    },
  });

  chart
    .interval()
    .data(data)
    .encode('x', 'genre')
    .encode('y', 'sold')
    .encode('color', 'genre');

  await chart.render();
}

from https://github.com/antvis/G2/blob/10a4b51e65fe58c68105118736a65870acc91550/__tests__/unit/ssr/index.spec.ts but receiving

ReferenceError: document is not defined
at normalizeContainer (/***/node_modules/@antv/g2/lib/api/src/api/utils.ts:40:23)

I tried await canvas.ready; as https://github.com/antvis/G/blob/next/__tests__/integration/__node__tests__/canvas/text.spec.js#L39 with the same result :(.

I want to export chart to PDF for report purpose and I need to somehow get the canvas/SVG code on server.


There is something missing in the doc. You should also inject JSDOM to the global. If you still have troubles with JSDOM, I can give you more information.

posted by pearmini over 1 year ago

I am sorry @pearmini but I need your help.

  1. I tried to inspire here but TypeError: container.appendChild is not a function ... /node_modules/@antv/g-canvas/src/Canvas2DContextService.ts:15:27
  2. Then I tried global.window = dom.window; global.document = dom.window.document; but `ReferenceError: Node is not defined ... /node_modules/@antv/g-plugin-canvas-renderer/dist/index.js:224
         !(object.compareDocumentPosition(parent) & Node.DOCUMENT_POSITION_CONTAINS)`
  3. I tried more but again ReferenceError: document is not defined

As I wrote, my goal is to get SVG or Image for PDF export. I guess many people wants to achieve this so your contribution will be appreciated. Thank you.

posted by JonasJonny over 1 year ago

I took some code from https://github.com/antvis/G2/pull/4123/files.

function createNodeGCanvas(width, height) {
  const nodeCanvas = createCanvas(width, height);
  ... // Renderer() code above

  // create JSDOM
  const dom = new JSDOM(`<div id="container"></div>`);
  global.window = dom.window;
  global.document = dom.window.document;

  if (global.window) {
    Object.defineProperty(global.window, 'TextEncoder', {
      writable: true,
      value: util.TextEncoder,
    });

    Object.defineProperty(global.window, 'TextDecoder', {
      writable: true,
      value: util.TextDecoder,
    });
  }

  return new Canvas({
    container: 'container',
    width,
    height,
    canvas: nodeCanvas,
    renderer,
    offscreenCanvas: offscreenNodeCanvas,
    document: dom.window.document,
    requestAnimationFrame: dom.window.requestAnimationFrame,
    cancelAnimationFrame: dom.window.cancelAnimationFrame,
  });
}

=>

TypeError: container.appendChild is not a function
    at Canvas2DContextService.init (/node_modules/@antv/g-canvas/src/Canvas2DContextService.ts:15:27)
    at Canvas.initRenderer (/node_modules/@antv/g-lite/src/Canvas.ts:352:41)
    at new Canvas (/node_modules/@antv/g-lite/src/Canvas.ts:128:15)
posted by JonasJonny over 1 year ago

@JonasJonny, is it OK that I provide a demo or a tool to help you in this week?

posted by pearmini over 1 year ago

Is it OK that I provide a demo or a tool to help you in this week?

Sure, that would help. Thanks

posted by JonasJonny over 1 year ago

@JonasJonny, I wrote an example using JSDOM in SSR: https://stackblitz.com/edit/stackblitz-starters-6zfeng?file=index.js

Here are the steps to implement it:

  • Since the latest d3 supports ESM only, we have to downgrade the relative packages to 2.x with overrides in package.json.
  • Create a SVG renderer called @antv/g-svg.
  • Create a JSDOM as container and pass it to G2 Chart's constructor.
  • Render the chart as usual.
  • Get the DOM(SVG) from container and serialize it to string with xmlserializer.

If you want to generate PNG instead of SVG, here's another example doing SSR with node-canvas: https://stackblitz.com/edit/stackblitz-starters-evrvef?file=index.js But it looks like SVG is good enough for what you're looking for.

posted by xiaoiver over 1 year ago

@JonasJonny Sorry for the late reply. Here is SSR tool for G2: https://github.com/pearmini/g2-ssr-node

posted by pearmini over 1 year ago

Thank you @xiaoiver and @pearmini. Moreover big one for robust example as https://github.com/pearmini/g2-ssr-node.

Both approaches still require a downgrade to 2.x version of d3 because of "The awkward valley to ESM: Node.js, Victory, and D3". Am I right in thinking that a downgrade will not be needed if a server will be ES6?

posted by JonasJonny over 1 year ago

Both approaches still require a downgrade to 2.x version of D2.

What is D2?

posted by pearmini over 1 year ago

Both approaches still require a downgrade to 2.x version of d3 because of "The awkward valley to ESM: Node.js, Victory, and D3".

I can build a ESM version for https://github.com/pearmini/g2-ssr-node

posted by pearmini over 1 year ago

@pearmini I think you already invested a lot of your time. My question is simply about the possibility. If I refactor my server to ESM then if I can use G2 SSR and newest d3 packages?

posted by JonasJonny over 1 year ago

@pearmini I think you already invested a lot of your time. My question is simply about the possibility. If I refactor my server to ESM then if I can use G2 SSR and newest d3 packages?

Sure, but I think the refactor is not required. You server can remain commonjs if you build a commonjs version of G2 and G.

posted by pearmini over 1 year ago

Your server can remain commonjs if you build a commonjs version of G2 and G.

Unfortunately security of my project cannot accept Vulnerabilities of d3 2.x packages (To build CommonJS). When I tried your 0.1.0 version yesterday, "downgrade" as @xiaoiver pointed out was needed too.

In the end I wrote script to replace all require('d3-****') paths in node_modules/@antv to https://www.npmjs.com/package/victory-vendor. Now my code works as expected with newer versions of d3 which are compiled by Victory to CommonJS.

Once again Thank you @pearmini for your time. I learned a lot.

posted by JonasJonny over 1 year ago

Fund this Issue

$0.00
Funded

Pull requests