Loading...
The G API is as consistent as possible with the DOM API, so some of the DOM API-oriented libraries in the Web ecosystem can be accessed at very low cost, for example using the Hammer.js gesture library, using the Interact.js drag-and-drop library. For them, G's event API and the DOM Events API are identical:
import Hammer from 'hammerjs';// Give Hammer.js the Circle as a DOM element directlyconst hammer = new Hammer(circle);hammer.on('press', (e) => {console.log("You're pressing me!");console.log(e.target); // circle});
Similarly, for D3, it is perfectly possible to take over its internal default SVG (which is also part of the DOM API) rendering, done using Canvas or WebGL, while retaining its data-driven capabilities.
In the following example, we use several tutorial examples from Fullstack D3 to switch the rendering API with a "one-line" code change while retaining most of the D3 style code We'll use a few examples from Fullstack D3 to implement D3 data processing + G rendering, while keeping most of the D3 style code. You can switch between Canvas, WebGL and SVG rendering at runtime.
Some stylized rendering plugins can also be used directly, such as g-plugin-rough-canvas-renderer for hand-drawn styling of the bar chart above.
See: https://observablehq.com/@xiaoiver/d3-rough-barchart
It's worth mentioning that I first saw this idea in Sprite.js, but at the time it was not quite finished with the DOM API, so some of the D3 APIs (e.g. join) didn't work properly.
In theory, this could also solve the performance problems of other SVG-based drawing libraries, but there are some "limitations" to this solution.
Example bar chart from Fullstack D3
The "one line of code" is indeed a bit of a headline, after all the steps to create the G canvas and renderer cannot be missing.
const canvasRenderer = new CanvasRenderer();const canvas = new Canvas({container: 'container',width: 600,height: 500,renderer: canvasRenderer,});
Now we don't need D3 to create <svg>
, we just need to give D3 the root node of the G scene graph, the size of the canvas is already specified at creation time.
// Before the change: D3 uses the DOM API to create `<svg>`const wrapper = d3.select('#wrapper').append('svg').attr('width', dimensions.width).attr('height', dimensions.height);// After the change: Give the root node of the G scene graph to D3const wrapper = d3.select(canvas.document.documentElement);
That's all that's been changed, and you can now use the D3 syntax in full. For example, creating a <g>
and setting the style, G will make D3 think it is still manipulating the DOM API.
const bounds = wrapper.append('g').style('transform',`translate(${dimensions.margin.left}px, ${dimensions.margin.top}px)`,);
Or use D3's event mechanism to add some event interaction, such as modifying the column color in response to a mouse event.
binGroups.on('mouseenter', function (e) {d3.select(e.target).attr('fill', 'red');}).on('mouseleave', function (e) {d3.select(e.target).attr('fill', 'cornflowerblue');});
When using D3 and third-party extensions, it is often necessary to use CSS selectors, for example d3-annotation would use the following syntax.
var group = selection.select('g.annotations');
In order to get CSS selectors like g.annotations
to work properly, the g-plugin-css-selector plugin is required, registered as follows.
import { Plugin } from '@antv/g-plugin-css-select';renderer.registerPlugin(new Plugin());
In addition, CSS style sheets are often used in D3 projects, such as this example, which uses d3-annotation
to set the stroke color.
.annotation path {stroke: var(--accent-color);}
We can use the G-like DOM API's element query method querySelectorAll.
const paths = canvas.document.querySelectorAll('.annotation path');paths.forEach(() => {});
Or continue using D3's selector syntax.
svg.select('.annotation path').style('stroke', 'purple');
Before explaining the limitations of the program, it is necessary to understand the rationale behind it. We will expand on the following.
First of all, not all DOM API-based libraries are as "seamlessly accessible" to G as Hammer.js, interact.js, [D3](https://github. com/d3/d3). Or rather, the libraries that are suitable for access have in common that they do not assume that they are in a real browser DOM environment.
Let's take D3 as an example, d3-selection does not use window.document
directly when creating a DOM element, but takes it from the element's [ownerDocument](/en/api/ builtin-objects/node#ownerdocument) attribute of the element, so as long as the G graphic also has that attribute of the same name, it will work without calling the browser's real DOM API.
// @see https://github.com/d3/d3-selection/blob/main/src/creator.js#L6// get documentvar document = this.ownerDocument;// create element with documentdocument.createElement(name);
These libraries have the added benefit of being suitable for running test cases on the node side with jsdom (D3's [practice](https://github.com/d3/d3-selection/blob/main/test /jsdom.js)).
Therefore, if X6 wants to access G in this way in the future, it needs to make sure that there is no usage like window.document
as well.
Finally G needs to avoid the assumption of a "browser real DOM environment" in its own internal implementation, e.g. when creating canvas, so that it can run in a WebWorker or even an applet environment.
With the appropriate access to the class library, whether it works properly depends on the extent to which the G for DOM API is implemented. Still using D3 as an example, before inserting an element into a document it will use compareDocumentPosition to compare positions. If G does not implement this API, the runtime will report an error.
The core API of G is actually a lightweight version of jsdom. Why "lightweight"? Because many functions such as HTML parsing and non-inline CSS styles have been omitted.
The current G implementation of the DOM API is as follows.
wrapper.append('g')
actually creates a Group of Gel.style('font-size', '1em')
in D3 that contains relative units will work.D3's ecosystem is very large, and seamless access means that many capabilities are available out of the box.
In this example, we use d3-shape and [d3-transition](https://github.com/d3/ d3-transition) to implement the shape animation.
This leads us to think about another question, whether G's animation capabilities should also be pluggable. In the above example, G only needs to provide rendering capabilities, and the animation of the d
attribute of path
is left entirely to D3.
We have selectively implemented most of the DOM API, which means that APIs such as innerHTML
are dropped.
el.innerHTML = '<div></div>';
So selection.html() in D3 does not work for now.
If we want to implement this feature, G needs to think about "mixed" rendering. Currently only one renderer can be selected to render all graphics at the same time, and blended rendering requires HTML to co-exist with graphics rendered using Canvas / WebGL. Considering the order of rendering and interaction between these hybrid contents is not an easy task.
It is worth mentioning that the new Google Docs, from the official sample document, contains two SVG and one Canvas on the first page. The main part (mainly text) is drawn with Canvas / WebGL and supports text selection, while the image (top right corner, inside the document) is drawn with SVG.
Currently, G does not support external style sheets either, so external style sheets in D3 applications will not work.
However, inline usage is valid, such as the following usage in the example.
const barText = binGroups.filter(yAccessor).append('text').attr('x', (d) => xScale(d.x0) + (xScale(d.x1) - xScale(d.x0)) / 2).attr('y', (d) => yScale(yAccessor(d)) - 5).text(yAccessor).attr('fill', 'darkgrey').style('text-anchor', 'middle').style('font-size', '12px').style('font-family', 'sans-serif');
The foreignObject in SVG allows for embedded HTML, which, like innerHTML, is not supported at this time.