d3 canvas

by lars verspohl

由拉斯·韦斯波尔

D3和Canvas分3个步骤 (D3 and Canvas in 3 steps)

绑定,平局和互​​动 (The bind, the draw and the interactivity)

Let’s say you’re building a data visualization using D3 and SVG. You may hit a ceiling when you try to display several thousand elements at the same time. Your browser may start to puff under the weight of all those DOM elements.

假设您正在使用D3和SVG构建数据可视化。 当您尝试同时显示数千个元素时,可能会达到上限。 您的浏览器可能开始受所有那些DOM元素的压制 。

Well here comes HTML5 Canvas to the rescue! It’s much faster, so it can solve your browser’s puffing problems.

HTML5 Canvas来了! 它的速度要快得多,因此可以解决浏览器的膨胀问题。

But you may quickly find yourself daunted. Because D3 and Canvas works a bit differently from D3 and SVG — especially when it comes to drawing and adding interactivity.

但是您可能很快会发现自己感到恐惧。 因为D3和Canvas与D3和SVG的工作方式有些不同-尤其是在绘制和添加交互性方面。

But fear not — it’s not that complicated. Any experience you’ve had with building visuals with D3 and SVG — or approaching D3 with a different renderer — will help you tremendously.

但是不要担心-它并不那么复杂。 您使用D3和SVG构建视觉效果的任何经验(或使用其他渲染器接近D3的经验)都将极大地帮助您。

This tutorial built on the shoulders of giants who have already covered Canvas well. I learned these three tutorials by heart and I recommend you do, too:

本教程建立在已经很好地涵盖了Canvas的巨人的肩膀上。 我很认真地学习了这三个教程,我也建议您这样做:

  • Working with D3.js and Canvas: When and How from Irene Ros

    使用D3.js和Canvas:来自Irene Ros的时间和方式

  • Needles, Haystacks, and the Canvas API from Yannick Assogba

    Yannick Assogba的针,干草堆和Canvas API

  • Learnings from a D3.js addict on starting with Canvas from Nadieh Bremer

    D3.js瘾君子从Nadieh Bremer 开始使用Canvas的学习

So why continue reading this, then? Well, when I want to learn something new, it helps me a great deal to look at the same subject from slightly different angles. And this tutorial is a slightly different angle.

那么,为什么还要继续阅读呢? 好吧,当我想学习新的东西时,从稍微不同的角度看同一主题对我很有帮助。 本教程一个稍微不同的角度。

Also, this tutorial covers the three key steps: binding data, drawing elements, and adding interactivity — and it does all this in one go, with an added step-by-step manual to set you up.

此外,本教程涵盖了三个关键步骤: 绑定数据图形元素添加交互性 ,并且一步一步地完成了所有这些,并添加了逐步的手册来进行设置。

我们建造什么? (What do we build?)

A grid of (many) squares. Their colours aren’t of any deep meaning but don’t they look pretty? The important bit is that you can update it (to cover binding and updating data), that it has many elements (up to 10,000 in order for canvas to pay out), and that you can hover over each square to show square-specific information (interactivity). You can play with it here on a full screen or here with all the code

(许多)正方形的网格。 它们的颜色没有任何深层含义,但是看起来不漂亮吗? 重要的一点是,您可以对其进行更新(以覆盖绑定和更新数据),其中包含许多元素(为了支付画布,最多可以包含10,000个),并且可以将鼠标悬停在每个正方形上以显示特定于正方形的信息(互动)。 您可以在这里全屏播放,也可以在 此处查看所有代码

心理模型 (The mental model)

Before we actually dive in, let’s quickly step back and grasp conceptually what we do when we create elements with D3 to draw them to the screen. Skip this if you just want to make things.

在我们真正涉足之前,让我们快速退后一步,从概念上掌握使用D3创建元素以将其绘制到屏幕上时的操作。 如果您只想制作东西,请跳过此步骤。

The first step when using D3 usually doesn’t involve drawing — it involves preparing all your elements you want to draw. It’s a bit like building some LEGO. You can rip open the box and start building something or you can look at the manual first and build it according to the blueprint. The manual is your mental model, a blueprint or recipe of what you want to build.

使用D3的第一步通常不涉及绘制-它涉及准备要绘制的所有元素。 这有点像建立一些乐高玩具。 您可以撕开盒子并开始制作东西,也可以先阅读手册并根据图纸进行制作。 该手册是您的心智模型,您想要构建的蓝图或配方。

What is D3’s model? Apart from the large number of helpful functions and methods that calculate positions, re-shape datasets (the layouts) and generate functions that draw, for example, paths for us, D3 has a model for how the elements’ lives should evolve on the screen. It has a certain way to think about the lifecycle of each element.

D3的型号是什么? 除了大量有用的函数和方法来计算位置,重整数据集(布局)并生成例如为我们绘制路径的函数之外,D3还提供了一个模型,用于说明元素的生活应如何在屏幕上演化。 它有一定的方式来考虑每个元素的生命周期。

Less ethereally, you inject data into a yet non-existent DOM, and D3 creates new elements of your choice as per the data you inject. Usually one element per datapoint. If you want to inject new data into the DOM you can do so and D3 identifies which elements have to be newly created, which elements are allowed to stay and which elements should pack up and leave the screen.

以较少的麻烦,您将数据注入到一个尚不存在的DOM中,并且D3根据您注入的数据创建您选择的新元素。 通常每个数据点一个元素。 如果要向DOM中注入新数据,则可以这样做,D3标识必须重新创建的元素,允许保留的元素以及应打包并离开屏幕的元素。

D3 is usually used in conjunction with SVG or sometimes with HTML-elements. In this orthodox case, you can see the data in the DOM when you choose to look at it through the console, for example. You can grab it, you can move it up or down the DOM and you can — importantly — add interactivity to each element you like to show, for example, a tooltip.

D3通常与SVG或HTML元素一起使用。 在这种传统情况下,例如,当您选择通过控制台查看数据时,便可以在DOM中看到数据。 您可以抓住它,可以在DOM上向上或向下移动它,并且可以(重要的是)向要显示的每个元素添加交互性,例如工具提示。

But — on the downside — you can’t show a lot of elements. Why? Because the more elements you push into the DOM, the harder the browser has to work to display them all. Let them also move around and the browser needs to re-calculate them constantly. The more knackered the browser gets the lower your frame rate or FPS (Frames Per Second), which measures how many frames the browser can paint each second. A frame rate of 60 is good and enables a fluid experience as long as no frames are missed — a frame rate of anything under 30 can equal a choppy ride. So when you want to show more elements, you can revert to canvas.

但是-不利的一面-您不能展示很多要素。 为什么? 由于您将更多元素放入DOM中,因此浏览器显示所有元素的难度就更大。 让它们也移动,浏览器需要不断地重新计算它们。 浏览器的技巧越多,您的帧速率或FPS(每秒帧数)就越低,该速率衡量浏览器每秒可以绘制多少帧。 只要不丢失任何帧,帧速率为60就是不错的选择,并能带来流畅的体验-帧速率低于30的任何帧都可以使您的行程更加震撼。 因此,当您想显示更多元素时,可以还原到画布。

Why canvas? Canvas is an HTML5 element which comes with its own API to paint on it. All elements drawn on the canvas element won’t manifest in the DOM and save a lot of work for the browser. They are drawn in immediate mode. This means the rendered elements won’t get saved in the DOM but your instructions draw them directly to a particular frame. The DOM only knows the one canvas element; everything on it is only in memory. If you want to change your canvas elements you have to redraw the scene for the next frame.

为什么要帆布? Canvas是HTML5元素,它带有自己的API可以在其上绘制。 canvas元素上绘制的所有元素都不会出现在DOM中,并且会为浏览器节省很多工作。 它们以立即模式绘制。 这意味着渲染的元素不会保存在DOM中,但是您的指令会将它们直接绘制到特定的框架。 DOM只知道一个canvas元素; 它上的所有内容仅在内存中。 如果要更改画布元素,则必须重新绘制下一帧的场景。

The problem with this is of course that you can’t communicate directly with these non-material elements living in memory. You have to find a way to talk to them indirectly. This is where the D3 model comes in as well as custom or ‘virtual’ DOM-elements. What you’ll do in principal is:

当然,这样做的问题是您无法直接与内存中的这些非物质元素进行通信。 您必须找到一种间接与他们交谈的方法。 这就是D3模型以及自定义或“虚拟” DOM元素出现的地方。 您将主要做的是:

  1. Bind your data to custom DOM elements. They don’t live in the DOM but only in memory (in a ‘virtual’ DOM) and describe the life-cycle of these elements in a known D3 way.将您的数据绑定到自定义DOM元素。 它们不存在于DOM中,而仅存在于内存中(在“虚拟” DOM中),并以已知的D3方式描述这些元素的生命周期。
  2. Use canvas to draw these elements.使用画布绘制这些元素。
  3. Add interactivity with a technique called ‘picking’.使用称为“挑选”的技术来增加交互性。

Let’s do it.

我们开始做吧。

数据 (The data)

Before we start to code, let’s produce some data. Let’s say you want 5,000 datapoints. So let’s create an array with 5,000 elements, each of which is an object with just a single property value carrying the element’s index. Here’s how you create it with d3.range(). d3.range() is a D3 utility function, that creates an array based on its argument:

在开始编码之前,让我们产生一些数据。 假设您要5,000个数据点。 因此,让我们创建一个包含5,000个元素的数组,每个元素都是一个对象,只有一个带有元素索引的属性值。 这是使用d3.range()创建它的方法。 d3.range()是D3实用程序函数,它根据其参数创建一个数组:

var data = [];
d3.range(5000).forEach(function(el) {
data.push({ value: el });
});

Here’s how the data looks in the console

这是控制台中数据的外观

Thrills!

激动!

画布容器及其工具 (The canvas container and its tools)

The canvas element is an HTML element. It’s conceptually very much like any SVG-parent-element, which I at least usually add to a simple container div as in:

canvas元素是HTML元素。 从概念上讲,它非常类似于任何SVG-parent-element,我通常至少将其添加到一个简单的容器div中,如下所示:

<div id=“container”></div>

So, let’s add it to your container with D3 as in…

因此,让我们使用D3将其添加到您的容器中,如下所示:

var width = 750, height = 400;
var canvas = d3.select('#container')  .append('canvas')  .attr('width', width)  .attr('height', height);
var context = canvas.node().getContext('2d');

You also need to add the context, which is the canvas toolbox. The context variable is from now on the object carrying all the properties and methods, the brushes and colours you need to draw on the canvas. Without the context, the canvas element would remain empty and white. That’s all you need to setup — a canvas and its tools…

您还需要添加上下文,即画布工具箱。 从现在开始,上下文变量位于对象上,该对象带有您需要在画布上绘制的所有属性和方法,画笔和颜色。 没有上下文,canvas元素将保持空白。 这就是您需要设置的全部—画布及其工具…

HTML (The HTML)

…is simple. The main HTML structure of your site will be:

……很简单。 您网站的主要HTML结构为:

<!-- A title --><h3>Coloured grids</h3>
<!-- An input field with a default value. --> <input type="text" id="text-input" value="5000">
<!-- An explanation... --> <div id="text-explain">...takes numbers between 1 and 10k</div>
<!-- ...and a container for the canvas element. --> <div id="container"></div>

Javascript结构 (The Javascript structure)

On a top level you only need 2 functions:

在顶层,您只需要2个功能:

databind(data) {
// Bind data to custom elements.
}
draw() {
// Draw the elements on the canvas.
}

Pretty straight forward so far.

到目前为止非常简单。

绑定元素 (Bind the elements)

To bind data to the elements you first create a base element for all your custom elements you will produce and draw. If you know D3 well, think of it as a replacement to the SVG element:

要将数据绑定到元素,首先要为将要生成和绘制的所有自定义元素创建一个基础元素。 如果您很了解D3,可以将其视为SVG元素的替代品:

var customBase = document.createElement('custom');
var custom = d3.select(customBase); // This is your SVG replacement and the parent of all other elements

Then you add some settings for your grid. In short, these settings allow you to draw a grid of squares. 100 squares build a ‘parcel’ and there is a line break after 10 parcels (or after 1,000 squares). You can adjust this for different ‘parceling’ of the squares or different line-breaking. Or just not worry about it. I suggest the latter…

然后,为网格添加一些设置。 简而言之,这些设置允许您绘制正方形的网格。 100平方尺构成一个“地块”,而10平方尺后(或1,000平方尺后)有一个换行符。 您可以针对不同的正方形“拼合”或不同的换行符进行调整。 或者只是不用担心。 我建议后者…

// Settings for a grid with 10 cells in a row, // 100 cells in a block and 1000 cells in a row.
var groupSpacing = 4; var cellSpacing = 2; var offsetTop = height / 5; var cellSize = Math.floor((width - 11 * groupSpacing) / 100) - cellSpacing;

Now let’s start the data-binding mission. Let’s get the necessities out of the way first and create a colour scale you will apply to your squares a little later.

现在让我们开始数据绑定任务。 让我们先消除必需品,然后创建一个色标,稍后再将其应用于正方形。

function databind(data) {
// Get a scale for the colours - not essential but nice.
colourScale = d3.scaleSequential(d3.interpolateSpectral)                      .domain(d3.extent(data, function(d) { return d; }));

Now let’s join your data to the ‘replacement-SVG’ you called custom above and add yet non-existing custom elements with the class .rect

现在,让我们将数据连接到您在上面称为custom的“ replacement-SVG”,并使用.rect类添加尚不存在的自定义元素。

var join = custom.selectAll('custom.rect')  .data(data);

You enter the custom elements (remember nothing enters the DOM, this is all in memory).

您输入自定义元素(记住什么都没有输入DOM,这些都在内存中)。

var enterSel = join.enter()  .append('custom')  .attr('class', 'rect')  .attr("x", function(d, i) {    var x0 = Math.floor(i / 100) % 10, x1 = Math.floor(i % 10);         return groupSpacing * x0 + (cellSpacing + cellSize) * (x1 + x0 * 10); })  .attr("y", function(d, i) {  var y0 = Math.floor(i / 1000), y1 = Math.floor(i % 100 / 10);   return groupSpacing * y0 + (cellSpacing + cellSize) * (y1 + y0 * 10); })  .attr('width', 0)  .attr('height', 0);

When an element enters your model, you just give it an x and a y position as well as a width and a height of 0, which you’ll change in the upcoming update selection…

当元素进入模型时,您只需为其指定x和ay位置以及宽度和高度0,即可在接下来的更新选择中对其进行更改…

You merge the enter selection into the update selection and define all attributes for the update and enter selection. This includes a width and a height value as well as a colour from the colour scale you built earlier:

您可以将输入选择合并到更新选择中,并定义更新的所有属性,然后输入选择。 这包括宽度和高度值,以及先前构建的色标中的颜色:

join   .merge(enterSel)  .transition()  .attr('width', cellSize)  .attr('height', cellSize)  .attr('fillStyle', function(d) { return colourScale(d); });

Two things of note about this last line. When you work with SVG this line would be

关于最后一行的两点注意事项。 当您使用SVG时,此行将是

.style('color', function(d) { return colourScale(d); })

But with canvas you use .attr(). Why? Your main interest here is to find a pain-free way to transfer some element-specific information. Here you want to transfer a colour-string from the databind() to the draw() function. You use the element simply as a vessel to transport your data over to where it is being rendered to the canvas.

但是对于画布,您可以使用.attr() 。 为什么? 您的主要兴趣是找到一种轻松传输某些特定于元素的信息的方法。 在这里,您要将颜色字符串从databind()传递到draw()函数。 您仅将元素用作容器即可将数据传输到将其呈现到画布的位置。

That's a very important distinction: when working with SVG or HTML you can bind data to elements and draw or apply styles to the elements in one step. In canvas you need two steps. First you bind the data then you draw the data. You can't style the elements while binding. They only exist in memory and canvas can't be styled via CSS style properties, which is exactly what you access when using .style().

这是一个非常重要的区别:使用SVG或HTML时,您可以将数据绑定到元素,并在一个步骤中为元素绘制或应用样式。 在画布中,您需要执行两个步骤。 首先绑定数据,然后绘制数据。 绑定时无法设置元素的样式。 它们仅存在于内存中,并且无法通过CSS样式属性设置画布的样式,这正是使用.style()时访问的内容。

At first, this might seem limiting as you can do less in one step, but it’s conceptually almost cleaner and also gives you some freedom. .attr() allows us to send any key-value pairs on the journey. You could use other methods like the HTML .dataset property for example, but .attr() will do just fine.

刚开始时,这似乎很局限,因为您可以一步一步减少工作,但从概念上讲它几乎是干净的,而且还给您一些自由。 .attr()允许我们在旅途中发送任何键值对。 您可以使用其他方法,例如HTML .dataset属性,但是.attr()可以很好地工作。

Notice we don't say color but fillStyle. To be honest, you could use color or you could use chooChooTrain here. You would only need to remember this when you fetch the information later during drawing. However, as canvas uses a property called fillStyle to style elements, it seems more appropriate in this case.

注意,我们不是说color而是fillStyle 。 老实说,您可以在这里使用color ,也可以使用chooChooTrain 。 您仅在以后在绘制过程中获取信息时才需要记住这一点。 但是,由于canvas使用名为fillStyle的属性来设置元素的样式,因此在这种情况下似乎更合适。

Finally, you also define the exit selection, deciding what should happen to exiting elements.

最后,您还定义了退出选择,确定退出元素应该发生什么。

var exitSel = join.exit()  .transition()  .attr('width', 0)  .attr('height', 0)  .remove();

That’s it! You can close your databind() function and move on...

而已! 您可以关闭databind()函数并继续...

} // databind()

This is not really scary coming from D3 as it’s pretty much exactly the same. You have now successfully created your data model, the way the application will think about data. Each element will get the properties it needs to be drawn via the .attr() functions and each element will be assigned a life-cycle state depending on the injected data. Our standard D3 model.

从D3传来的消息确实并不可怕,因为它几乎完全一样。 现在,您已经成功创建了数据模型,即应用程序对数据的思考方式。 每个元素将获得需要通过.attr()函数绘制的属性,并且将根据注入的数据为每个元素分配生命周期状态。 我们的标准D3模型。

绘制元素 (Drawing the elements)

Now you need to write the draw function to get the elements on screen. Let’s just note here that nothing has happened yet. You haven’t called databind() yet because you need to find a way to draw it to the canvas first. So here we go... The draw() function doesn't need to take any arguments in this case:

现在,您需要编写draw函数以使元素显示在屏幕上。 让我们在这里仅注意什么都没有发生。 您尚未调用databind() ,因为您需要找到一种首先将其绘制到画布上的方法。 所以我们开始...在这种情况下, draw()函数不需要接受任何参数:

function draw() {

As mentioned fleetingly above, you need to take care of cleaning the canvas every time you draw afresh. The DOM is material, in that when you draw a rect-element on it and you change its x value, it will move in the x-direction and the DOM will take care of this move (or the re-paint) automatically.

如上所述,您需要在每次重新绘制时清洁画布。 DOM是实质性的,因为当您在其上绘制一个rect-元素并更改其x值时,它将在x方向上移动,并且DOM将自动处理此移动(或重新绘制)。

If you move a rect from x = 0 to x = 1 at a certain point in time (after a button press for example) the browser will move the rect from 0 to 1 within one tick or frame-paint (which is roughly 16ms long). If you move it from 0 to 10, it will do so in a time depending on the duration you asked this transition to happen, maybe 1 pixel per tick maybe 8 pixel per tick (for more read this blog post).

如果您在某个时间点(例如在按下按钮后)将rect从x = 0移至x = 1,浏览器将在一个刻度或帧画(大约16毫秒长)内将rect从0移至1。 )。 如果将其从0移到10,它会在一段时间内完成此操作,具体取决于您要求此过渡发生的持续时间,也许每刻度1像素,也许每刻度8像素(有关更多信息,请参阅此博客文章)。

But it will tell the pixel at 0 that the rect has disappeared and the pixel at 1 that there is a rect now. Canvas doesn’t do this. You need to tell canvas what to paint, and if you paint something new, you need to tell it to remove the previous paint.

但是它将告诉0像素处的矩形消失,而1像素处的像素现在存在。 画布不执行此操作。 您需要告诉画布要绘画的内容,如果您绘画新的东西,则需要告诉它删除以前的绘画。

So let’s start with cleaning up anything that might be on the canvas before you draw. Here’s how:

因此,让我们从画前清理画布上可能存在的任何东西开始。 这是如何做:

context.clearRect(0, 0, width, height); // Clear the canvas.

Simple.

简单。

Now you…

现在轮到你…

  1. …get hold of all elements in order to…掌握所有要素,以便
  2. loop through all elements and遍历所有元素并
  3. take the information you have stored in the databind() function to draw the element:

    利用您存储在databind()函数中的信息来绘制元素:

// Draw each individual custom element with their properties.
var elements = custom.selectAll('custom.rect');// Grab all elements you bound data to in the databind() function.
elements.each(function(d,i) { // For each virtual/custom element...
var node = d3.select(this);   // This is each individual element in the loop.     context.fillStyle = node.attr('fillStyle');   // Here you retrieve the colour from the individual in-memory node and set the fillStyle for the canvas paint
context.fillRect(node.attr('x'), node.attr('y'), node.attr('width'), node.attr('height'));  // Here you retrieve the position of the node and apply it to the fillRect context function which will fill and paint the square.
}); // Loop through each element.

And that’s it! You can close the draw() function

就是这样! 您可以关闭draw()函数

} // draw()

When I started with canvas after a while of wanting to dive into it, this simplicity really upped my spirits.

一段时间后,当我想开始使用画布时,这种朴素的精神确实激发了我的精神。

However, nothing has happened in the browser yet. We have the tools in the databind() and the draw() function, but nothing has been drawn yet. How do you do this? If you just wanted to draw a static visual or image, you just call:

但是,浏览器还没有任何React。 我们在databind()draw()函数中具有工具,但尚未绘制任何内容。 你怎么做到这一点? 如果您只想绘制静态视觉或图像,则只需调用:

databind(data);
draw();

This would bind the data to the custom elements, which would live in memory and then draw it — once!

这会将数据绑定到自定义元素,该元素将存在于内存中,然后绘制它-一次!

But you have transitions. Remember above: when you wrote the databind() function you transitioned the cell width and height from 0 to their size as well as the colour from black (the default) to the respective element’s colour. A default D3 transition lasts 250 milliseconds, so you need to redraw the squares many times in these 250 ms in order to get a smooth transition. How do you do this?

但是你有过渡。 记住以上databind() :编写databind()函数时,您将单元格的宽度和高度从0转换为它们的大小,并将颜色从黑色(默认)转换为相应元素的颜色。 默认的D3过渡持续250毫秒,因此您需要在这250毫秒内多次重绘正方形以获取平滑过渡。 你怎么做到这一点?

It’s again simple. You just call databind(data) to create our custom elements before you repeatedly call draw() for as long as it takes the transition to run. So in our case at least 250 ms. You could use setInterval() for this but we really should use requestAnimationFrame() in order to be as performant as possible (for more read this). There are a few ways to use it, but keeping within the D3 spirit, I suggest using d3.timer() which implements requestAnimationFrame() as well as being straight forward to use. So here we go:

再次简单。 您只需调用databind(data)来创建我们的自定义元素,然后再重复调用draw() ,只要它需要过渡即可运行。 因此,在我们的情况下至少为250毫秒。 您可以为此使用setInterval() ,但实际上我们应该使用requestAnimationFrame() ,以使其尽可能地具有性能(有关更多信息,请阅读 )。 有几种使用它的方法,但是为了保持D3的精神,我建议使用d3.timer()来实现requestAnimationFrame()并直接使用。 所以我们开始:

// === First call === //
databind(d3.range(value)); // Build the custom elements in memory.
var t = d3.timer(function(elapsed) {
draw();
if (elapsed > 300) t.stop();
}); // Timer running the draw function repeatedly for 300 ms.

d3.timer() calls the callback repeatedly until elapsed (which is the passed time in milliseconds from instantiation) is past 300 and then the timer is stopped. In these 300 milliseconds, it runs the draw() at each tick (roughly each 16ms). draw() then looks at each element's attributes and draws them accordingly.

d3.timer()反复调用该回调,直到elapsed (从实例化开始经过的时间,以毫秒为单位)超过300,然后停止计时器。 在这300毫秒内,它在每个刻度(大约每16ms)运行一次draw() )。 然后draw()查看每个元素的属性并相应地绘制它们。

This is how a transition works in canvas. You call the drawing function right after the binding function many times. Whatever your D3-model is set up to transition (positions, colours, sizes) will be re-drawn many times with small incremental changes for each draw

这就是过渡在画布中的工作方式。 您多次在绑定函数之后调用绘图函数。 无论您将D3模型设置为过渡到什么(位置,颜色,大小),都会多次重绘,每次绘制都会有较小的增量变化

Note that draw() needs to come right after the databind() function. You couldn't ask the machine to run databind(), then do something else for a second and then call draw(). Because after 1 second the transitioned states calculated by your databind() function have all transitioned already. Done, dusted and forgotten.

请注意, draw()必须databind()databind()函数之后。 您不能要求计算机运行databind() ,然后执行其他操作,然后调用draw() 。 因为1秒钟后,由databind()函数计算出的转换状态已经全部转换。 做过,撒满灰尘,被遗忘。

That’s it! You’ve bound data to custom elements and you’ve drawn it to the canvas.

而已! 您已将数据绑定到自定义元素,并将其绘制到画布上。

让用户更新平方数 (Let the user update the number of squares)

To give the user the chance to repeat this feat with a custom number of elements (ok, semi-custom with a maximum of 10,000) you add the following listener and handler to your text-input box:

为了使用户有机会使用自定义数量的元素(好的,半自定义的,最多为10,000个)重复此壮举,请将以下侦听器和处理程序添加到文本输入框中:

// === Listeners/handlers === //
d3.select('#text-input').on('keydown', function() {
if (d3.event.keyCode === 13) { // Only do something if the user hits return (keycode 13).
if (+this.value < 1 || +this.value > 10000) {   // If the user goes lower than 1 or higher than 10k...         d3.select('#text-explain').classed('alert', true);     // ... highlight the note about the range and return.
return;
} else {   // If the user types in a sensible number...
d3.select('#text-explain').classed('alert', false);     // ...remove potential alert colours from the note...
value = +this.value; // ...set the value...
databind(d3.range(value)); // ...and bind the data.
var t = d3.timer(function(elapsed) {
draw();        if (elapsed > 300) t.stop();
}); // Timer running the draw function repeatedly for 300 ms.     } // If user hits return.
}); // Text input listener/handler

Here it is again, our colourful grid of canvas squares, ready to be updated and redrawn:

这又是我们丰富多彩的画布正方形网格,准备进行更新和重绘:

互动性 (Interactivity)

The biggest ‘pain’ with canvas in comparison to SVG or HTML is that there are no material elements living in the DOM. If there were you could just register listeners to the elements and add handlers to the listeners. For example you can trigger a mouse-over on an SVG rect element and whenever the listener triggers, you could do something to the rect. Like showing data values stored with the rect in a tooltip.

与SVG或HTML相比,画布最大的“痛苦”是DOM中没有任何实质性元素。 如果有的话,您可以将侦听器注册到元素,然后将处理程序添加到侦听器。 例如,您可以在SVG rect元素上触发鼠标悬停,并且每当侦听器触发时,您都可以对rect做一些事情。 就像在工具提示中显示与rect存储的数据值一样。

With canvas you have to find another way to make an event heard on our canvas elements. Luckily there are a number of clever people who thought of an indirect but logical way.

使用画布,您必须找到另一种方法来使事件在我们的画布元素上听到。 幸运的是,有许多聪明的人想到了间接但合乎逻辑的方式。

So what interactivity do we want? As said above let’s go for a tooltip and let’s assume you want to show the index of the square in a tooltip as soon as you hover over the element. Not very thrilling, but the key is that you can access the data bound to the element by hovering over it.

那么我们想要什么交互性? 如上所述,让我们来看看工具提示,并假设您希望将鼠标悬停在元素上后立即在工具提示中显示正方形的索引。 并不是很刺激,但是关键是您可以通过将鼠标悬停在元素上来访问绑定到该元素的数据。

采摘 (Picking)

There are a few steps involved (all logical though). But in short you will build two canvases to achieve this. One main canvas that produces our visual and one hidden canvas (as in we can’t see it) that produces the same visual. The key here is that all elements on the second canvas will be at the exact same position in relation to the canvas origin compared to the first canvas. So square 1 starts on 0,0 on the main canvas as well as on the hidden canvas. Square 2 starts on 8,0 on the main canvas as well as on the hidden canvas and so on.

涉及几个步骤(尽管都是合乎逻辑的)。 但是总之,您将构建两个画布来实现此目的。 一张可产生视觉效果的主画布 ,另一张可产生相同视觉效果的隐藏画布 (如我们所见)。 此处的关键是,与第一个画布相比,第二个画布上的所有元素相对于画布原点将位于完全相同的位置。 因此,平方1从主画布以及隐藏画布上的0,0开始。 Square 2从主画布以及隐藏画布上的8,0开始。

There is only one important difference. Each element on the hidden canvas will get a unique colour. We will create an object (or rather an associative array or map for brevity) that links each unique colour to each element’s data.

只有一个重要区别。 隐藏的画布上的每个元素都将具有唯一的颜色。 我们将创建一个对象(或更简单地说,是一个关联数组或映射 ),将每个唯一的颜色链接到每个元素的数据。

Why? Because next we attach a mouse-move listener to the main-canvas to retrieve a stream of mouse-positions. At each mouse-position we can use a canvas-own method to “pick” the colour at this exact position. Then we just look up the colour in our associative array and we have the data ! And we’re flying…

为什么? 因为接下来我们将鼠标移动侦听器附加到主画布上,以检索鼠标位置流。 在每个鼠标位置,我们都可以使用画布自带的方法在该确切位置“拾取”颜色。 然后,我们只需要在关联数组中查找颜色,就可以得到数据! 而且我们正在飞翔……

You could say “well, my squares have already got a unique colour, I can use those?” And indeed, you could use them. However, your interactivity would go out of the window as soon as you decide to bereft your squares from the colours. So you should make sure to always have one canvas — the hidden canvas — that has a guaranteed set of unique colours for the squares.

您可以说“嗯,我的正方形已经有一种独特的颜色,我可以使用那些颜色吗?” 确实,您可以使用它们。 但是,一旦决定从颜色中删除正方形,您的交互性就会消失在窗口之外。 因此,您应该确保始终有一个画布(隐藏的画布),该画布保证为正方形提供一组唯一的颜色。

Let’s apply this technique step by step. The code you’ve built so far can stay as it is — you just add to it as you go along.

让我们逐步应用此技术。 到目前为止,您已构建的代码可以保持原样-您可以在添加过程中随便添加。

1.准备隐藏的画布 (1. Prepare the hidden canvas)

First let’s create the hidden canvas that will harbour our visual with a unique colour per square.

首先,让我们创建一个隐藏的画布,该画布将以每平方独特的颜色隐藏视觉效果。

1.1 Create hidden canvas element and set its CSS to { display: none; }.

1.1创建隐藏的canvas元素并将其CSS设置为{ display: none; } { display: none; }

// Rename the main canvas and add a 'mainCanvas' class to it.
var mainCanvas = d3.select('#container')  .append('canvas')  .classed('mainCanvas', true)  .attr('width', width) .attr('height', height); // new -----------------------------------
// Add the hidden canvas and give it the 'hiddenCanvas' class.
var hiddenCanvas = d3.select('#container')  .append('canvas')  .classed('hiddenCanvas', true)   .attr('width', width)   .attr('height', height);

In fact, I won’t set the canvas to hidden in this example to show what is going on. But to do so, just add .hiddenCanvas { display: none; } to your CSS and the deed is done.

实际上,在此示例中,我不会将画布设置为隐藏以显示正在发生的情况。 但是,只需添加.hiddenCanvas { display: none; } .hiddenCanvas { display: none; }到您CSS上,事情就完成了。

1.2 Build the context variable in the draw() function and pass two arguments to the function: the canvas as well as a boolean called 'hidden' determining which canvas we build (hidden = true || false) as in:

1.2在draw()函数中构建上下文变量,并将两个参数传递给该函数:画布以及一个名为“ hidden”的布尔值,确定我们要构建哪个画布(hidden = true || false),如下所示:

function draw(canvas, hidden) {

1.3 You now need to adapt all draw functions to include the two new draw() arguments. So from now on, you don't just call draw() you call either draw(mainCanvas, false) or draw(hiddenCanvas, true)

1.3现在,您需要调整所有绘图函数以包括两个新的draw()参数。 因此,从现在开始,您不仅可以调用draw()还可以调用draw(mainCanvas, false)draw(hiddenCanvas, true)

2.将独特的颜色应用于隐藏的元素并进行映射 (2. Apply unique colours to the hidden elements and map them)

Here, dear reader, comes the key part of our operation, the engine of our truck, the spice in our soup.

亲爱的读者,这是我们运营的关键部分,卡车的发动机以及汤中的香料。

2.1 Include a function to generate a new unique colour every time it gets called (via Stack Overflow)

2.1包含一个函数,该函数在每次调用时都会生成新的唯一颜色(通过Stack Overflow )

// Function to create new colours for the picking.
var nextCol = 1;
function genColor(){     var ret = [];
if(nextCol < 16777215){         ret.push(nextCol & 0xff); // R     ret.push((nextCol & 0xff00) >> 8); // G     ret.push((nextCol & 0xff0000) >;> 16); // B
nextCol += 1;     }
var col = "rgb(" + ret.join(',') + ")";
return col;
}

genColour() produces a colour defining string in the form rgb(0,0,0). Every time it's called it increments the R value by one. Once it reaches 255, it increments the G value by 1 and resets the R value to 0. Once it reaches r(255,255,0) it increments the B value by 1 resetting the R and the G to 0 and so on.

genColour()生成颜色定义的字符串,形式为rgb(0,0,0)。 每次调用它时,R值都会增加一。 一旦达到255,它将G值加1,并将R值重置为0。一旦达到r(255,255,0),它将使B值加1,将R和G重置为0,依此类推。

So in total you can have 256*256*256 = 16.777.216 elements to retain a unique colour. However, I can assure you your browser will die beforehand. Even with canvas (webGL tutorial to follow).

因此,总共可以有256 * 256 * 256 = 16.777.216个元素来保留唯一的颜色。 不过,我可以向您保证,您的浏览器将提前失效。 即使使用画布(遵循WebGL教程)。

2.2 Create the map-object that will keep track of which custom element has which unique colour:

2.2创建地图对象,该对象将跟踪哪个自定义元素具有哪种唯一颜色:

var colourToNode = {}; // Map to track the colour of nodes.

You can add the genColour() function wherever you want in your script, as long as it's outside the databind() and draw() function scope. But note that your map variable needs to be created before and beyond the scope of the databind() function.

您可以在脚本中的任意位置添加genColour()函数,只要它在databind()draw()函数范围之外即可。 但是请注意,您的地图变量需要在databind()函数范围之前和之外创建。

2.3 Add a a unique colour to each custom element as for example .attr('fillStyleHidden') and 2.4 build the map-object during element creation

2.3向每个自定义元素添加唯一的颜色,例如.attr('fillStyleHidden')和2.4在元素创建期间构建地图对象

Here you’ll use your ‘colour-canon’ genColour() in our databind() function when assigning the fillStyle to our elements. As you also have access to each datapoint while it's being bound to each element, you can bring colour and data together in your colourToNode map.

在这里,当将fillStyle分配给元素时,将在databind()函数中使用“ color-canon” genColour() 。 由于还可以在将每个数据点绑定到每个元素时对其进行访问,因此可以将颜色和数据合并到colourToNode映射中。

join   .merge(enterSel)   .transition()   .attr('width', cellSize)   .attr('height', cellSize)   .attr('fillStyle', function(d) {     return colorScale(d.value);   });
// new -----------------------------------------------------         .attr('fillStyleHidden', function(d) {
if (!d.hiddenCol) {
d.hiddenCol = genColor();       colourToNode[d.hiddenCol] = d;
}
// Here you (1) add a unique colour as property to each element     // and(2) map the colour to the node in the colourToNode-map.
return d.hiddenCol;
});

2.5 You can now colour the elements according to the canvas the draw() function is rendering. You add a conditional on the fillStyle in the draw() function applying the colours for our visual to the main canvas and the unique colours to the hidden canvas. It's a simple one-liner:

2.5现在,您可以根据draw()函数正在渲染的画布为元素着色。 您可以在draw()函数的fillStyle上添加条件,将视觉效果的颜色应用于主画布,将唯一的颜色应用于隐藏的画布。 这是一个简单的单行代码:

context.fillStyle = hidden ? node.attr('fillStyleHidden') : node.attr('fillStyle');
// The node colour depends on the canvas you draw.

The main canvas still looks the same of course:

当然,主画布看起来还是一样的:

Let’s finally add some interactivity and start with drawing the hidden canvas whenever we move the mouse onto our main canvas.

最后,让我们添加一些交互性,并在每次将鼠标移到主画布上时开始绘制隐藏的画布。

3.用鼠标拾取颜色 (3. Pick up the colours with the mouse)

3.1 First, simply register a listener to the main canvas, listening to mouse-move events.

3.1首先,只需在主画布上注册一个侦听器,侦听鼠标移动事件。

d3.select('.mainCanvas').on('mousemove', function() {
});

Why mousemove? As you can’t register listeners with individual squares but have to use the entire canvas you won’t be able to work with mouseover or -out events as they will only trigger when entering the canvas not the elements. In order to get the mouse position on your canvas you can do mousemove or click/mousedown.

为什么要移动鼠标? 由于您无法使用单个方块注册侦听器,而必须使用整个画布,因此您将无法使用mouseover或-out事件,因为它们只会在进入画布而不是元素时触发。 为了获得鼠标在画布上的位置,您可以进行mousemove或单击/ mousedown。

d3.select('.mainCanvas').on('mousemove', function() {
draw(hiddenCanvas, true); // Draw the hidden canvas.
});

This way, the first thing our user triggers when mousing over the main canvas is to unknowingly create the hidden canvas. As said, in production this canvas would be hidden, but for our educational purposes we want to see it and indeed, trigger the hidden canvas to be drawn when the mouse moves over the main canvas like so:

这样,当用户将鼠标悬停在主画布上时,我们触发的第一件事就是在不知不觉中创建隐藏的画布。 如前所述,在生产中该画布是隐藏的,但是出于我们的教育目的,我们希望看到它,并且实际上是在鼠标移到主画布上时触发隐藏画布的绘制,如下所示:

The colours on the main canvas range from black to red, from rgb(0,0,0) to rgb(255,0,0) and then it looks as if the same range from black to red is repeated. However, now the colour ranges from a slightly greener black, precisely from rgb(0,1,0) to rgb(255,1,0):

主画布上的颜色范围从黑色到红色,从rgb(0,0,0)到rgb(255,0,0),然后看起来好像重复了从黑色到红色的相同范围。 但是,现在颜色的范围从稍微偏绿的黑色,精确到rgb(0,1,0)到rgb(255,1,0):

Zooming into the first couple of hundred squares, here are the colours of the first, the 256th and the 257th square:

放大到前两百个正方形,以下是第一个,第256个和第257个正方形的颜色:

3.3 As our hidden canvas is structurally a carbon copy of our main canvas, all the hidden canvas elements will be at the same position as the elements on our main canvas. So, you can now use the mouse’s x and y positions you are collecting from the listener on the main canvas to establish the same location on the hidden canvas. Back in the listener, you add:

3.3由于隐藏的画布在结构上是主画布的副本,因此所有隐藏的画布元素都将与主画布上的元素位于同一位置。 因此,您现在可以使用从侦听器在主画布上收集的鼠标的x和y位置在隐藏的画布上建立相同的位置。 回到侦听器中,添加:

d3.select('.mainCanvas').on('mousemove', function() {       // Draw the hidden canvas.  draw(hiddenCanvas, true);
// Get mouse positions from the main canvas.  var mouseX = d3.event.layerX || d3.event.offsetX;   var mouseY = d3.event.layerY || d3.event.offsetY; });

Note here we take the event.layerX and event.layerY properties which return the mouse position including scrolling. This can break so use offsetX as a fallback (or just use offsetX).

注意这里我们采用event.layerXevent.layerY属性,它们返回鼠标位置(包括滚动)。 这可能会中断,因此请使用offsetX作为后备(或仅使用offsetX)。

3.4 The picking: Canvas greatly allows access to the pixel-data the mouse is hovering over with the getImageData() function and its .data property. In full bloom this will look like:

3.4选择:Canvas极大地允许使用getImageData()函数及其.data属性访问鼠标悬停的像素数据。 盛开时,它看起来像:

getImageData(posX, posY, 1, 1).data .

getImageData(posX, posY, 1, 1).data

It will return an array with four numbers: the R, the G, the B and the alpha value. As you diligently built the colourToNode map assigning the element data to each of its hidden colours, you can now access this element's data simply by looking up the colour in the map!

它将返回一个包含四个数字的数组:R,G,B和alpha值。 当您努力构建colourToNode映射并将元素数据分配给其每种隐藏的颜色时,现在您只需在映射中查找颜色即可访问该元素的数据!

d3.select('.mainCanvas').on('mousemove', function() {
// Draw the hidden canvas.  draw(hiddenCanvas, true);
// Get mouse positions from the main canvas.  var mouseX = d3.event.layerX || d3.event.offsetX;   var mouseY = d3.event.layerY || d3.event.offsetY;
// new -----------------------------------------------
// Get the toolbox for the hidden canvas.  var hiddenCtx = hiddenCanvas.node().getContext('2d');
// Pick the colour from the mouse position.   var col = hiddenCtx.getImageData(mouseX, mouseY, 1, 1).data;
// Then stringify the values in a way our map-object can read it.  var colKey = 'rgb(' + col[0] + ',' + col[1] + ',' + col[2] + ')';
// Get the data from our map!   var nodeData = colourToNode[colKey];
console.log(nodeData);
});

And indeed, logging the nodeData to the console returns an object every time you hover over a square:

实际上,每次将鼠标悬停在一个正方形上时,将nodeData记录到控制台都会返回一个对象:

The data per node now shows the value which constitutes the original data as well as the key hiddenCol showing this node's colour for the hidden canvas:

现在,每个节点的数据将显示构成原始数据的value以及键hiddenCol ,该键hiddenCol显示隐藏画布的该节点的颜色:

3.5 Finally — and that’s a formality — you add the tooltip

3.5最后,这是一种形式,您添加了工具提示

d3.select('.mainCanvas').on('mousemove', function() {
// Draw the hidden canvas.  draw(hiddenCanvas, true);
// Get mouse positions from the main canvas.  var mouseX = d3.event.layerX || d3.event.offsetX;   var mouseY = d3.event.layerY || d3.event.offsetY;
// Get the toolbox for the hidden canvas.  var hiddenCtx = hiddenCanvas.node().getContext('2d');
// Pick the colour from the mouse position.   var col = hiddenCtx.getImageData(mouseX, mouseY, 1, 1).data;
// Then stringify the values in a way our map-object can read it.  var colKey = 'rgb(' + col[0] + ',' + col[1] + ',' + col[2] + ')';
// Get the data from our map!   var nodeData = colourToNode[colKey];     console.log(nodeData);
// new -----------------------------------------------
if (nodeData) {   // Show the tooltip only when there is nodeData found by the mouse
d3.select('#tooltip')       .style('opacity', 0.8)       .style('top', d3.event.pageY + 5 + 'px')       .style('left', d3.event.pageX + 5 + 'px')         .html(nodeData.value);
} else {   // Hide the tooltip when the mouse doesn't find nodeData.      d3.select('#tooltip').style('opacity', 0);     }
}); // canvas listener/handler

That’s it! You’ve visualised a large number of elements on canvas — more than you would’ve been able to enjoy problem-free with SVG. You still used D3’s lifecycle model and you added some interactivity to access the data attached to each element. These three steps should enable you to do pretty much anything or at least more than what you’re used to when working with D3 and SVG.

而已! 您已经可视化了画布上的大量元素,这比使用SVG可以毫无问题地享受到的更多。 您仍然使用D3的生命周期模型,并添加了一些交互性来访问附加到每个元素的数据。 这三个步骤应该使您可以做任何事情,或者至少比使用D3和SVG时做的更多。

There’s a step-by-step manual from scratch to interactive D3/canvas on my blog which allows internal page links. This way you can see the whole process in one view and click your way through it with ease:

我的博客上有一个循序渐进的手册 ,从零开始到交互式D3 /画布,它允许内部页面链接。 这样,您可以在一个视图中查看整个过程,并轻松单击它:

…and here’s the full code again.

…… 这是完整的代码 。

I hope you enjoyed reading this and please do say hello and/or …

希望您喜欢阅读本文,并打个招呼和/或…

lars verspohl www.datamake.io @lars_vers https://www.linkedin.com/in/larsverspohl

拉尔斯·弗斯波尔www.datamake.io @lars_vers https://www.linkedin.com/in/larsverspohl

…is always grateful for a like? or a follow he can return.

……总是喜欢这样的人吗? 或跟随他可以返回。

翻译自: https://www.freecodecamp.org/news/d3-and-canvas-in-3-steps-8505c8b27444/

d3 canvas

d3 canvas_D3和Canvas分3个步骤相关推荐

  1. ad导出bom表不完整_新手学AD电子设计,分四个步骤学习,不用看视频教程也能上手...

    新手学习Altium Designer软件设计电路图,分为四个步骤--画元器件.原理图.元器件封装和PCB板,学会这四个步骤,就可以不用再看视频教程了,相信会很快上手. 元器件 第一步就是画元器件.其 ...

  2. SVG和canvas

    1.SVG实现的圆环旋转效果 参考:http://www.softwhy.com/article-6472-1.html 2.SVG中的图形可以通过  transform="matrix(0 ...

  3. python woe分箱_python数据处理--WOE分箱

    数据分箱的重要性离散特征的增加和减少都很容易,易于模型的快速迭代: 稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展: 离散化后的特征对异常数据有很强的鲁棒性:比如一个特征是年龄>30是1 ...

  4. python分箱处理_python数据处理--WOE分箱

    数据分箱的重要性离散特征的增加和减少都很容易,易于模型的快速迭代: 稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展: 离散化后的特征对异常数据有很强的鲁棒性:比如一个特征是年龄>30是1 ...

  5. D3.js系列——初步使用、选择元素与绑定数据

    D3 的全称是(Data-Driven Documents),顾名思义可以知道是一个被数据驱动的文档.听名字有点抽象,说简单一点,其实就是一个 JavaScript 的函数库,使用它主要是用来做数据可 ...

  6. 通过三个直观步骤理解ROC曲线

    作者 | Valeria Cortez 来源 | DeepHub IMBA ROC曲线是一个分类模型效果好坏评判的的可视化表示. 在这篇文章中,我将分三个步骤头开始构建ROC曲线. 步骤1:获取分类模 ...

  7. 技术07期:图计算,让数据间的关系无处可藏【社区分切篇】

    社区,即一群拥有相似特征的点,社区内的点连接紧密,社区间稀疏连接. 我们可以把同一公司的同事看作是一个社区内的点,他们从事同一行业,可能有相似的教育背景,由于 工作需要,他们之间要进行频繁的沟通. 而 ...

  8. 线结构光标定详细步骤与实现HALCON

    这部分是HALCON官方的一个例子,下面是对这个比较复杂的例子的一些理解,具体的每一句代码都对应相应的作用解释 具体的例子是这个: 此程序演示如何执行校准光片测量系统: 测量系统由区域扫描摄像机和光线 ...

  9. 下载python的步骤ios_下载及安装Python详细步骤

    安装python分三个步骤: *下载python *安装python *检查是否安装成功 1.下载Python (2)选择下载的版本 (3)点开Download后,找到下载文件 Gzipped sou ...

最新文章

  1. jquery书写左右两个多选下拉列表交换移除功能
  2. MongoDB(课时18 修改器)
  3. Bit-Z CEO长顺入围2018中国经济潮流人物
  4. windows主机加固和评测
  5. mysql备份还原数据库操作系统_mysql 命令行备份还原数据库操作
  6. mysql libc.so.6_解决安装mysql动态库libstdc++.so.6、libc.so.6版本过低问题
  7. Vue使用ajax或者axios获取数据,能获取到数据但是页面没有更新
  8. vue 如何读取编译携带的参数
  9. 幅频特性、相频特性的概念解释
  10. html中最小规格字体,font-size【CSS 字体大小】文字大小尺寸
  11. 6. ROS编程入门--路径跟随(Trajectory following)
  12. 感谢周易算命大师元真先生
  13. 1-10000水仙花数
  14. 关于微信异常烦人自动生成的聊天记录截图视频
  15. FastDfs分布式文件存储系统
  16. 爱签电子合同助力无纸化办公,青岛将推行存量房网签合同电子签名
  17. 新增对象时生成uuid传递到数据库_技术译文 | UUID 很火但性能不佳?今天我们细聊一聊...
  18. gdi win7奔溃_Win7系统细致核心图形架构的操作方法
  19. 教师计算机网络知识培训内容,教师计算机培训总结范文
  20. 计算机网络(西工大复习)更新

热门文章

  1. 05 使用VS程序调试的方法和技巧1214
  2. python-双层嵌套循环-打印小星星
  3. django-数据的插入-利用pymysql
  4. jquery-节点操作
  5. SAP编程中最基本的概念
  6. 区块链开发公司 注重用户的价值才是企业归宿
  7. CAICT:2015年全球云计算市场规模522亿美元
  8. 单多晶技术拉锯战升级
  9. C# 实现连连看功能
  10. 我的react组件化开发道路(二) 分页 组件开发