pixel 3 变焦_D3变焦—缺少的手册

pixel 3 变焦

by lars verspohl

由拉斯·韦斯波尔

D3 Zoom:缺少的手册 (D3 Zoom: The Missing Manual)

如何使用SVG和Canvas缩放和平移数据可视化 (How to zoom and pan in your data visualizations using SVG and Canvas)

The best opening paragraph for a D3 zoom article has already been written, and it goes like this:

D3缩放文章的最佳开始段落已经写好,它是这样的:

It’s good. In four sentences, it tells you precisely what zooming is and what it does, and — probably more importantly — it takes away your zooming fears.

很好。 它用四个句子准确地告诉您什么是缩放及其作用,并且-更重要的是-它消除了您对缩放的恐惧。

So, has it all been said then? Well, it never has. It’s always good to have numerous differing perspectives, especially with events that move your precious visual all over the shop and scale it around at your trigger-happy user’s discretion.

那么,一切都这么说了吗? 好吧,它从来没有。 有很多不同的观点总是很好的,尤其是那些可以在商店中移动您宝贵的视觉效果并根据用户的喜好来决定缩放比例的事件。

A while ago, I worked on a fairly complex visualization with many moving elements and a long list of interactions, including zoom and pan, at its initially dark heart. The static visual itself was already relatively complex, but adding zoom and pan to it felt a bit like tying my son’s 4 by 6 foot Lego castle onto a fleeing water buffalo.

前一阵子,我进行了相当复杂的可视化工作,在其最初的黑暗之心中包含了许多移动元素以及一长串的交互,包括缩放和平移。 静态视觉本身已经相对复杂,但是增加缩放和摇摄感觉就像将我儿子的4乘6英尺的乐高城堡绑在逃离的水牛上。

The conceptual problem here is that zoom and pan so fundamentally interfere with our work. They appear to control quite a bit of our handcrafted visualization, which is rarely ever a single whole thing, but a careful concoction of positions, scales and axes. This can be confusing at best and intimidating at worst.

这里的概念性问题是缩放在根本上干扰了我们的工作。 它们似乎控制了我们手工制作的可视化文件中的相当一部分,这很少是一件完整的事情,而是位置,比例和轴的精心组合。 充其量,这可能会造成混乱,而在最坏的情况下则会令人生畏。

So, after my zoom and pan moves grew in confidence, and were tested in a few other projects, the time seemed ripe to write them down. Maybe it’s too late and you all cracked it years ago, but even then, it might be helpful to have another perspective.

因此,当我对缩放和平移的动作逐渐放心,并在其他一些项目中进行了测试后,将它们写下来的时机似乎已经成熟。 也许为时已晚,几年前你们都已将其破解,但是即使那样,换个角度看也可能会有所帮助。

There will be three parts to our journey:

我们的旅程将分为三个部分:

  1. A synchronous recipe for zoom and pan

    同步缩放和平移配方
  2. Building a visual

    建立视觉效果
  3. Implementing zoom and pan in SVG and in Canvas

    在SVG和Canvas中实现缩放和平移

As a bonus, we’ll add programmatic zoom and make our visual pretty.

作为奖励,我们将添加程序化缩放并增加视觉效果。

Now, you might look to that scroll bar over there, thinking you’ll miss supper when reading all this. It’s detailed for a reason, but I’ll make it easy for you to browse and cherry pick as I point out sections you can jump over without missing crucial stuff. So you can make this ride as short or thorough as you like and get something out of it either way.

现在,您可能会看向那边的滚动条,以为在阅读所有这些内容时会错过晚饭。 它的详细说明是有原因的,但是我将使您浏览和选择樱桃变得容易,因为我指出您可以跳过一些部分而不会丢失关键内容。 因此,您可以根据自己的喜好使骑行过程简短或完整,并从中获得任何收益。

一个简单的缩放和平移配方 (A simple zoom and pan recipe)

This first part is this post’s spine. It’s a short manual — nothing more than a series of five simple points you can follow while building up your zoom and pan events. This manual will give you a lifeline-like sequence of how to integrate zoom and pan into your app. The asynchronous and knotted world of programming is often helped by a series of synchronous and simple steps to follow.

第一部分是这篇文章的书脊。 这是一本简短的手册,仅用于构建缩放和平移事件时可以遵循的五个简单要点。 本手册将为您提供类似于生命线的序列,说明如何将缩放和平移集成到您的应用程序中。 遵循一系列同步和简单的步骤通常可以帮助编程的异步和打结世界。

同意某些术语 (Agreeing on some terminology)

Before we pull ourselves along the line, let’s first define some helpful terminology:

在坚持到底之前,让我们先定义一些有用的术语:

  • A zoom transform is an object produced and maintained by D3. It’s your most valuable possession in the zoom and pan context, and it holds three values: the x and y translation as well as the scale factor represented by k. We shall see when and where it gets produced and changed very soon. This is how it looks in its initial state:

    缩放变换是D3生成和维护的对象。 它是缩放和平移上下文中最有价值的资产,它具有三个值: xy平移以及以k表示的比例因子。 我们将很快看到它在何时何地生产和更改。 这是它在初始状态下的样子:

  • It says: “The user has not yet zoomed, or panned the visual. Therefore, the zoom-scale factor is 1 and x and y translation is 0.”

    它说: “用户尚未缩放或平移视觉。 因此,缩放比例因子为1,x和y平移为0。”

  • The zoom behaviour is the event system that keeps track of and passes on the transform values. A listener consumes (takes note of) the user’s actions. Once activated, it will send an event object with information about this event to a handler function. You will write this handler and use the information of the event object. The most important piece of information your zoom handler will receive is the above transform at every zoom activity. Whatever we want to do with the transform values, we will do in the zoom handler. This might sound like a lot, but in its simplest form you set the zoom behaviour up like so:

    缩放行为是事件系统,它跟踪并传递变换值。 侦听器使用(记录)用户的操作。 一旦激活,它将把带有有关该事件的信息的事件对象发送到处理函数。 您将编写此处理程序并使用事件对象的信息。 您的缩放处理程序将获得的最重要的信息是每次缩放活动中的上述变换。 无论我们要对变换值做什么,我们都将在缩放处理程序中进行。 这听起来可能很多,但是以最简单的形式,您可以像这样设置缩放行为:

var zoom = d3.zoom().on(‘zoom’, zoomed);
  • The zoom base is the parent element the zoom is attached to or registered on, as they say. It does two things: 1) It’s the surface that takes in all the user’s moves and gestures, and 2) it holds the transform object (the x, the y, and the scale factor k).

    正如他们所说, zoom base是父元素附加到或注册在 zoom 。 它有两件事:1)它是吸收用户所有移动和手势的表面,2)它包含变换对象( xy和比例因子k )。

  • The zoom targets are all the elements we want to move around. If you want to zoom in and out of a circle, then this circle would be your zoom target.

    缩放目标是我们要移动的所有元素。 如果要放大和缩小一个圆,则该圆将成为您的缩放目标。

Furthermore, we might want to distinguish between two types of zoom. They will become much clearer when we move to our examples, but it will be helpful to define them on a top-level first:

此外,我们可能要区分两种缩放类型。 当我们转到示例时,它们将变得更加清晰,但是首先在顶层定义它们将是有帮助的:

  • Geometric zoom (or graphical zoom) means elements just get scaled up or down without any differentiation. All their properties will get scaled up or down. Think of it as moving or scaling the coordinate system of the respective elements. Everything on it will be scaled and moved indiscriminately. Geometric zoom is closest to our real-life experience. When we walk towards a house, each aspect of the house appears larger at each step. Equally, if we scale an axis all parts of it would become larger or smaller — the lines, the domain path, the labels. For example, a 14px axis label scaled up by a scale factor of 2 would appear 14 × 2 = 28px large.

    几何缩放 (或图形缩放 )意味着元素只是按比例放大或缩小而没有任何区别。 它们的所有属性将按比例放大或缩小。 可以将其视为移动或缩放各个元素的坐标系。 它上的所有内容都会按比例缩放和随意移动。 几何缩放最接近我们的现实生活体验。 当我们朝房屋走去时,房屋的各个方面在每一步都显得更大。 同样,如果缩放轴,则轴的所有部分(线,域路径,标签)都将变大或变小。 例如,按比例因子2放大的14px轴标签将显示14×2 = 28px大。

  • Semantic zoom (or non-geometric zoom) means we control each single element’s property during zoom. If we have an axis, for example, with labels of size 14px and we semantically zoom in to the axis, we could command the labels to retain their original size for every scale factor. The lines might become larger and thinner and the axis would be repositioned according to the zoom, but our label would remain 14px large.

    语义缩放 ( 或非几何缩放 )意味着我们在缩放过程中控制每个元素的属性。 例如,如果有一个轴,其标签大小为14px,并且在语义上放大了该轴,则可以命令标签为每个比例因子保留其原始大小。 线条可能会变大和变细,并且轴会根据缩放比例重新定位,但我们的标签将保持14px大。

We won’t touch on this in the following, but bona fide semantic zoom can go further. It allows us to not only control the element’s properties, but the representation of our element depending on the zoom level. Google maps for example shows countries when zoomed out, states or administrational districts at medium zoom and smaller cities when zoomed in.

在下文中,我们将不再赘述,但是真正意义上的语义缩放可以走得更远。 它使我们不仅可以控制元素的属性,还可以根据缩放级别来控制元素的表示形式。 例如,谷歌地图显示缩小时的国家,放大时的州或行政区和放大时的较小城市。

缩放和平移5个步骤 (Zoom and pan in 5 steps)

We are well-equipped for this now. Here’s our zoom and pan in five simple steps:

我们现在已经为此做好了准备。 这是我们通过五个简单步骤进行的缩放:

1.首先建立静态视觉 (1. Build your static visual first)

In order to zoom into a visual, you will need a visual.

为了放大视觉,您将需要一个视觉。

2.确定您的缩放基准和缩放目标 (2. Identify your zoom base and your zoom targets)

Take a piece of paper, nominate an element that listens (the zoom base), and write down a list of elements that should move (the zoom targets).

拿一张纸,指定一个要侦听的元素(以zoom为基数 ),并写下一个应该移动的元素的列表( zoom目标 )。

  • Choose your zoom base element first. Determine which DOM element you want to use for your zoom base. You can attach the zoom to an svg, g, rect or any other element that your mouse has access to. Note here, that g elements can only register events where they have children with a fill. So, if you have a large g element with a circle of radius 1, your zoom gestures will only work on that tiny circle. It is often best to set-up a dedicated SVG rectangle (rect) with fill but 0 opacity and pointer-events set to all to register the zoom listener on. You might have to unset pointer events of ascendant elements.

    首先选择您的缩放基础元素。 确定要用于缩放基础的DOM元素。 您可以将缩放svg附加到svggrect或鼠标可以访问的任何其他元素上。 请注意, g元素只能在有子元素填充的事件中注册事件。 因此,如果您有一个半径为1的大g元素,则缩放手势将仅在该小圆圈上起作用。 通常最好设置一个专用的SVG矩形( rect ),将其填充,但将0的不透明度和pointer-events设置为all以注册缩放侦听器。 您可能必须取消设置上升元素的指针事件。

  • Identify your zoom target elements and write them down. Remember, the zoom targets are the elements you want to move. Make a list of all zoom target elements.

    确定缩放目标元素并将其写下来。 请记住,缩放目标是您要移动的元素。 列出所有缩放目标元素。

  • For each target, identify if you want to use geometric or semantic zoom.

    对于每个目标,确定要使用几何 缩放还是语义缩放

  • Note it down. Here’s an example table you might end up with:

    记下来。 这是您可能最终得到的示例表:
3.设置缩放行为 (3. Set up the zoom behaviour)

Now you want to set up the behaviour that will make the listener listen.

现在,您要设置使侦听器侦听的行为。

  • Create the zoom behaviour with at least:

    创建缩放行为时至少要:
var zoom = d3.zoom().on(‘zoom’, zoomed);
zoomBaseElement.call(zoom)

Note, you don’t have to call your zoom base zoomBaseElement of course.

请注意,您当然不必调用缩放基础 zoomBaseElement

4.编写处理程序 (4. Write the handler)

This is where the zoom and pan will happen. The handler’s single most precious possession will be the transform object updating x, y, and k continuously when the user wheels or drags. You will apply these to your zoom targets.

这就是发生缩放和平移的地方。 处理程序唯一最珍贵的财产是当用户旋转或拖动时连续更新xyktransform对象。 您会将它们应用于缩放目标。

  • The first thing you want to do is capture the transform object passed into the handler by the listener at every user interaction (wheel or mouse):

    您要做的第一件事是在每次用户交互(滚轮或鼠标)时捕获由侦听器传递到处理程序中的transform对象:

var transform = d3.event.transform;
  • Now that you have your zoom and pan parameters (tx, ty, k), you can do whatever you want with it...

    现在您有了zoom和pan参数( txtyk ),就可以使用它进行任何操作...

  • If you only want to administer geometric zoom, you just call:

    如果只想管理几何缩放 ,只需调用:

zoomTargetElement  .attr(‘transform’,         ‘translate(‘ + transform.x + ‘, ‘ + transform.y + ‘)          scale(‘ + transform.k + ‘)’);

or simpler:

或更简单:

zoomTargetElement.attr(‘transform’, transform.toString());

…which is exactly the same. This assumes you want to apply all transform values. You can also focus on only tx, ty or the scale k of course.

…完全一样。 假定您要应用所有变换值。 当然,您也可以只关注txty或比例k

  • If you want semantic zoom you need to rescale.

    如果要进行语义缩放 ,则需要重新缩放。

  • Assuming all your data values went through a scale to be translated from data to screen space, this translation changes on zoom. If your data point x = 10 was translated to pixel space 50 before zoom, the zoom will move it to a different point.

    假设您所有的数据值都经过缩放以从数据转换为屏幕空间,则此转换会随着缩放而改变。 如果在缩放之前将数据点x = 10转换为像素空间50,则缩放会将其移动到另一个点。
  • If you translate the x by 5 and scale by 2, the new position will be:

    如果将x平移5并按2平移,则新位置将为:

x2 = x1 × k + tx

x2 = x1×k + tx

  • Luckily, you don’t have to (nor should you) produce these calculations yourself, but you can rescale your scale on each zoom and apply it to the target properties you want to change. These include axes or circles or rects or whatever target shapes and components you have.

    幸运的是,您不必(也不应该)自己生成这些计算,但是您可以在每次缩放时重新缩放比例并将其应用于您要更改的目标属性。 这些包括轴,圆或rects或您拥有的任何目标形状和组件。

  • With a scale called xScale, you can use the sugar function .rescaleX() and apply it like so:

    使用称为xScale的比例,您可以使用.rescaleX()函数.rescaleX()并按如下方式应用它:

var updatedScale = transform.rescaleX(xScale);
  • Now you can use updatedScale in your zoomed function for all the elements you want to update. For example an axis:

    现在,您可以在缩放功能中对要更新的所有元素使用updatedScale 。 例如一个轴:

xAxis.scale(updatedScale); gAxis.call(xAxis);
  • or a set of circle x positions:

    或一组圆x位置:
circles.attr(‘cx’. function(d) { return updatedScale(d.value); })
5.您是否需要以编程方式将目标移动到某个位置? (5. Do you need to programmatically move your target into a position?)
  • Calculate/determine the position and the scale

    计算/确定位置和比例
  • Determine the new positions tx and ty and the new scale k in D3’s own transform producing function by saying:

    确定D3自己的transform产生函数中的新位置txty以及新标度k ,方法是说:

var t = d3.zoomIdentity.translateBy(tx, ty).scale(k);
  • Store the object in the zoom base AND propagate the changes by calling your first zoom handler, which will move the targets with:

    将对象存储在缩放库中,并通过调用第一个缩放处理程序传播更改,该处理程序将通过以下方式移动目标:
zoomBaseElement.call(zoom.transform, t);
  • Now enable user triggered zoom with:

    现在通过以下方式启用用户触发的缩放:
zoomBaseElement.call(zoom)

Here you are. I think they call this an executive summary. However, we have only superficially touched on the key concepts and haven’t even mentioned different renderers. Let’s add some flesh to the bones with a real life example.

这个给你。 我认为他们将此称为执行摘要。 但是,我们只是在表面上涉及了关键概念,甚至没有提到其他渲染器。 让我们以一个真实的例子为骨骼添加一些肉。

我们做什么 (What we make)

Here’s the Guinea pig we shall build in passing:

这是我们将要建造的豚鼠:

It’s a visualisation of our solar system’s planets showing their distance from the Sun. Zooming will come in handy to allow an overview, and panning conveys some feel for distance. In addition, all orbs are pink!

这是对太阳系行星的可视化,显示了它们与太阳的距离。 缩放将非常方便,以便进行概览,而平移则传达了一定的距离感。 另外,所有的球都是粉红色的!

Oh, and you don’t really have to, but if you want, you can follow along. Go here for all commented code. Alternatively, you can just play around with the app step by step. I’ll drop a link whenever we progress.

哦,您不一定要这样做,但是如果您愿意,可以继续。 去这里查看所有注释的代码 。 另外,您也可以逐步使用该应用程序。 每当我们前进时,我都会删除一个链接。

建立我们的静态视觉 (Building our static visual)

As with nearly all visualisations, data is our starting point. So, here it is in its entirety:

与几乎所有可视化一样,数据是我们的起点。 因此,这里是全部内容:

We have 8 planets, 1 star called Sun, and Pluto, which is not actually a planet anymore but still in here for romantic reasons. We also have each planet’s distance from the sun and their radii. That’s all we need. But in order to turn it into this:

我们有8颗行星,一颗叫做太阳的恒星,还有冥王星,实际上这颗行星已经不再是行星了,但出于浪漫的原因,它仍然在这里。 我们还拥有每个行星与太阳及其半径的距离。 这就是我们所需要的。 但是为了把它变成这样:

…we need to write some code.

…我们需要编写一些代码。

Please note: this post is about zooming rather than about building a static visualization of our solar system. Nevertheless, I will run through the code to give you a round-trip of this app. However, if you’re only here for the zoom, please feel free to browse through this section and quickly move on to the first zoom-building step called Identifying our zoom base and zoom targets (mind you, it might be worth reading the Calculating the Dimensions section in a moment).

请注意:这篇文章是关于缩放,而不是关于我们太阳系的静态可视化。 不过,我将遍历代码,为您提供该应用程序的往返行程。 不过,如果你只是这里变焦,请随时通过本节来浏览和快速移动到第一变焦建设步叫确定我们的变焦基地和缩小目标 (请注意,这可能是值得一读的计算尺寸部分)。

Let’s start with the sparse HTML:

让我们从稀疏HTML开始:

<h1 id="headline">Measuring our planets'   <span id="pink">     <a href="http://bit.do/solar-system">distances</a>   </span> </h1>
<div id="vis"></div>

That’s it. We have a headline with a link and a span to give it an appropriately pink bottom border and a container div for our vis. Now we’ll move swiftly on to the JavaScript and bypass the CSS, which is not invited until the end of this post…

而已。 我们有一个带有链接和span的标题,为它提供了适当的粉红色底边框和一个用于vis的容器div 。 现在,我们将快速转到JavaScript并绕过CSS,直到本文结束时才邀请CSS…

The first thing we do is to load in the data:

我们要做的第一件事是加载数据:

d3.csv('planets.csv', row, function(error, data) {   if (error) throw error;    make(data);
});
function row(d) {   return {     planet: d.planet,     distance: +d.distance,     radius: +d.radius   }; }

We’re loading in our planets.csv piping it through the row() function which makes sure our numbers are indeed numbers. Then we call the make() function, which will be the home of all further code.

我们正在通过row()函数将其通过pipes.csv管道加载,以确保我们的数字确实是数字。 然后我们调用make()函数,它将成为所有其他代码的宿主。

The make() function does the following:

make()函数执行以下操作:

  1. It sets the dimensions of our visual

    它设定了我们视觉的尺寸
  2. It builds an svg as well as a zoom surface

    它建立一个svg以及一个缩放表面

  3. It calculates our scales

    它计算我们的秤
  4. It builds our axis

    它建立了我们的轴心
  5. It builds the planets

    它建造行星

Let’s start with setting our visual’s dimensions.

让我们从设置视觉尺寸开始。

计算尺寸^ (Calculating the dimensions ^)

The margin and the height calculations are straightforward:

边距和高度的计算很简单:

var margin = {   top: window.innerHeight * 0.3,   left: 50,   bottom: window.innerHeight * 0.4,   right: 50 };
var height = window.innerHeight - margin.top - margin.bottom;

We want the svg element to cover our entire screen. So our height will be the window.innerHeight subtracting some margins. We define the top and bottom margin in respect to the window.innerHeight to keep them relative to each other.

我们希望svg元素覆盖整个屏幕。 因此,我们的高度将是window.innerHeight减去一些边距。 我们定义关于window.innerHeight的顶部和底部边距,以使它们彼此相对。

On to the width, which needs just a little more thought:

到宽度,这需要多一点思考:

var maxDist = d3.max(data, function(d) { return d.distance; });
var mapScale = 1/10e4;
// The full width of all planets var chartWidth = maxDist * mapScale;
// svg width will only be as large as screen var screenWidth = window.innerWidth - margin.left - margin.right;

The gist of our width calculation is that we want two widths. One for the chart and one for the svg. What’s the difference? Well, the chart will be very wide, because it needs to fit all our planets on it. The svg, however, doesn’t need to be very wide. The svg’s task is to show us the planets that fit on our browser window. An svg of the window’s dimensions is henceforth enough. It’ll look like this:

宽度计算的要点是我们需要两个宽度。 一种用于图表,另一种用于svg 。 有什么不同? 好吧,这张图会很宽,因为它需要适合我们所有的行星。 但是, svg不必很宽。 svg的任务是向我们展示适合我们浏览器窗口的行星。 一个svg窗口的尺寸是足够的今后。 它看起来像这样:

Note that this is only possible using the zoom behaviour. If we wanted to allow the user to see all planets without the zoom and pan magic, we would need to have an svg as wide as the chart. As a result, the browser would give us scroll bars our users could use to move to the right or left — like in the marvelous If the Moon were only 1 Pixel visual.

请注意,这只能使用缩放行为来实现。 如果我们想让用户在没有缩放和平移魔术的情况下看到所有行星,则需要与图表一样宽的svg 。 结果,浏览器将为我们提供滚动条,用户可以使用滚动条向右或向左移动-就像在奇妙的“月球只有1像素”视觉效果中一样。

However, using D3 zoom, the zoom transform object we will initialize will keep track of our gestures: how far we “scrolled” to the right, left, and along the z-axis virtually piercing through the screen following our line of sight.

但是,使用D3缩放,我们将初始化的缩放变换对象将跟踪我们的手势:我们向右,向左和沿z轴“滚动”了多远,实际上沿着视线穿透了屏幕。

Based on the transform, we can re-position our elements. And if they happen to be within screen coordinates, they get displayed on our base svg. No harm done if not, they just won’t get shown.

基于转换,我们可以重新定位元素。 如果它们恰好在屏幕坐标内,它们会显示在我们的基本svg 。 如果没有的话,不会造成任何伤害,它们不会被显示出来。

As such, our svg’s width will get the screenWidth which is just the window.innerWidth minus the margins. How wide will our chartWidth, the base for all planets, be? We will scale down the distance between the two furthest apart orbs (the Sun and Pluto, that is) with our mapScale by 10e4, or 1:10,000. When Pluto is 5,913,000,000 km away from the Sun in real space, it will be 59,130 pixels away from the centre of the Sun in our visual.

这样,我们的svg的宽度将得到screenWidth ,它就是window.innerWidth减去边距。 我们的chartWidth (所有行星的底数)将有多宽? 我们将使用mapScale将两个距离最远的球体(即太阳和冥王星)之间的距离mapScale 10e4或1:10,000。 当冥王星在现实空间中距太阳59.13亿公里时,在我们的视线中它将距太阳中心59,130​​像素。

That wasn’t too bad. Onwards!

那还不错。 向前!

建立基地 (Building out the base)

First, we build our svg base: a margin-transformed g element dangling off an svg element:

首先,我们构建svg基础:悬挂在svg元素上的已转换边距的g元素:

var svg = d3.select('#vis')   .append('svg')     .attr('width', screenWidth + margin.left + margin.right)         .attr('height', height + margin.top + margin.bottom)      .append('g')     .attr('class', 'chart')     .attr('transform', 'translate(' + margin.left + ', '            + margin.top + ')');

Then we overlay it with a rect element that we’ll use as our zoom base. This rect will listen to all mouse events and gestures, and as such we’ll boldly call it listenerRect:

然后,将其与一个rect元素叠加,将其用作缩放基础 。 该rect将侦听所有鼠标事件和手势,因此,我们将其大胆命名为listenerRect

var listenerRect = svg   .append('rect')     .attr('class', 'listener-rect')     .attr('x', 0)     .attr('y', -margin.top)    .attr('width', screenWidth)     .attr('height', height)    .style('opacity', 0);

Important to note here that our zoom base is at the same spot as the zoom targets — the elements we want to zoom. We will attach our zoom base listenerRect to the svg (which in fact is the margin translated g.chart element as you can see just one code block above), which also be the home of our planet circles we’ll draw later.

在此需要注意的重要一点是,我们的缩放基础与缩放目标位于同一位置,即我们要缩放的元素。 我们会将缩放基础的listenerRect附加到svg (实际上是由margin转换的g.chart元素,您可以在上面看到一个代码块),这也是我们稍后绘制的行星圈子的所在地。

Scales next.

下秤。

设置秤 (Setting up our scales)

We are mapping two measures to screen coordinates: distance and radius. As such we need two scales. Here’s the first, mapping our planet’s radii in km to screen radii:

我们正在将两个度量映射到屏幕坐标:距离和半径。 因此,我们需要两个尺度。 这是第一个,将以公里为单位的地球半径映射到屏幕半径:

var rExtent = d3.extent(data, function(d) { return d.radius; });
var rScale = d3.scaleLinear()   .domain([0, rExtent[1]])   .range([3, height/2 * 0.9]);

First, we get the radius scale. We calculate the domain and map these values to a range of 3px to a little less than half our window’s height, keeping the measures relative to the window.

首先,我们获得半径比例。 我们计算域,并将这些值映射到3px的范围内,以小于窗口高度的一半,并保持相对于窗口的尺寸。

Our second scale is the distance scale:

我们的第二个尺度是距离尺度:

var xScale = d3.scaleLinear()   .domain([0, maxDist])   .range([0, chartWidth]);

We map the data extent to the full chartWidth. If you mapped it to the screenWidth, all the planets would stand on their feet:

我们将数据范围映射到完整的chartWidth 。 如果将其映射到screenWidth ,则所有行星都将站起来:

We could correct this by using a tighter radius scale, but we would like them to stretch out initially and then allow the user to zoom in or out.

我们可以使用更严格的半径比例来更正此问题,但是我们希望它们首先伸展,然后允许用户放大或缩小。

绘制轴 (Drawing the axis)

We’ll be using a normal D3 axis component to build the axis. However, as you can see in the image above, we will stagger the labels so they don’t overlap.

我们将使用普通的D3轴组件来构建轴。 但是,如您在上图中所看到的,我们将错开标签,以免标签重叠。

First, we build out the axis component:

首先,我们构建轴组件:

var xAxis = d3.axisBottom(xScale)   .tickSizeOuter(0)   .tickPadding(10)   .tickValues(data.map(function(el) { return el.distance; }))   .tickFormat(function(d, i) {     return data[i].planet + ' ' + d3.format(',')(d) + ' km';   });

We determine the exact number of tick mark labels by passing an array of the planets’ distance values to .tickValues():

我们通过将行星距离值数组传递给.tickValues()来确定刻度标记的确切数量:

[0, 58000000, 108000000, 150000000, 228000000, 778000000,   1429000000, 2871000000, 4504000000, 5913000000]

The axis will now only draw tick labels for these values. We use .tickFormat() to specify what the label will say. In our case, it’ll be <planet name> <distance from sun> <km>.

轴现在将仅绘制这些值的刻度标签。 我们使用.tickFormat()来指定标签将要说的内容。 在我们的例子中,它将是<行星名称> <与太阳的距离 > <km>。

Now we produce the axis’ g base and unleash the component on it:

现在我们生成轴的g基础并释放其上的组件:

var xAxisDraw = svg.insert('g', ':first-child')  .attr('class', 'x axis')   .call(xAxis);

Like our listenerRect, the axis becomes a child of our g.chart element we labelled svg. Why insert it? We want our zoom base to be on top of all other elements dangling off the svg so it can consume all the events. Looking at the DOM it should be the last child of svg. To achieve this, we’ll insert the axis — and soon the planets — before listenerRect.

就像我们的listenerRect一样,该轴成为我们标记为svg g.chart元素的g.chart 。 为什么要插入? 我们希望缩放基础位于悬空于svg的所有其他元素之上,以便它可以吸收所有事件。 从DOM来看,它应该是svg的最后一个子代。 为了实现这一点,我们将在轴listenerRect 之前插入轴(然后是行星)。

Moving on to our axis labels. By default, all labels will be drawn on the same y level. But we want them staggered, so we need to write some code to achieve the steps. This is the stagger voodoo we apply:

转到我们的轴标签。 默认情况下,所有标签将在相同的y级别上绘制。 但是我们希望它们交错,因此我们需要编写一些代码来实现这些步骤。 这是我们应用的交错伏都教:

// Move the axis-labels and -lines down
var labelHeight = xAxisDraw.select('text').node().getBBox().height;
xAxisDraw.attr('transform', 'translate(0, ' +                 (height + labelHeight * data.length) + ')');
// Position the axis text
xAxisDraw.selectAll('text')   .attr('y', function(d, i) {     return -(i * labelHeight + labelHeight);   })   .attr('dx', '-0.15em')   .attr('dy', '1.15em')   .style('text-anchor', 'start');

Don’t feel obliged to follow me down this rabbit hole — in short, we move them all down by # of labels × their label height. Then we move each label up by their height × their index. As a result, the Sun, for example, won’t move up as it’ll be lifted by 0 × labelHeight = 0, but Mercury (the next planet to the Sun) will move up by 1 × labelHeight and so on.

不要觉得有义务跟着我走到这个兔子洞里—简而言之,我们将它们全部向下移动#个标签×它们的标签高度 。 然后,我们将每个标签按其高度×索引向上移动。 结果,例如,太阳将不会向上移动,因为它将被提升0×labelHeight = 0 ,但是水星(太阳的下一个行星)将向上移动1×labelHeight ,依此类推。

The tick line needs a little more attention, as we have to cater for its y1 and y2 value:

刻度线需要更多注意,因为我们必须迎合其y1y2值:

// Draw the axis lines
xAxisDraw.selectAll('line')   .attr('y1', function(d, i) {     return -(i * labelHeight + labelHeight);   })   .attr('y2', function(d, i) {       return -(i * labelHeight + labelHeight + from axis-y 0            // ^ this label’s start position              (data.length-1-i) * labelHeight +             // ^ the distance from the start position             //   to the bottom of the chart area               height);             // ^ the height    });

Good news. We can now draw our planets in (nearly) a single D3 chain:

好消息。 现在,我们可以(几乎)在一条D3链中绘制我们的行星:

var gPlanets = svg  .insert('g', '.listener-rect')  .attr('class', 'planet-group');
var planets = gPlanets.selectAll('.planet')     .data(data)  .enter().append('circle')     .attr('class', 'planet')     .attr('id', function(d) { return d.planet; })     .attr('cx', function(d) { return xScale(d.distance); })     .attr('cy', 0)     .attr('r', function(d) {       d.scaledRadius = rScale(d.radius);       return d.scaledRadius;     });

First, we create a group for all our planets and make sure the listenerRect also covers these planets by inserting our g.planet-group before the rect.listener-rect. Then we join and enter() the data to our as yet virtual .planet's, which will manifest as circles with the respectively scaled distances as x positions and rScaled radii. So there:

首先,我们为所有行星创建一个组,并通过在rect.listener-rect之前插入g.planet-group来确保listenerRect也覆盖这些行星。 然后,我们将数据加入并enter()到我们仍然是虚拟的.planet ,这将显示为圆,其缩放比例分别为x位置和rScale d半径。 所以那里:

Great! We have our visual. Now let’s get to the zoom…

大! 我们有视觉。 现在,让我们开始放大…

确定我们的缩放基准和缩放目标^ (Identifying our zoom base and zoom targets ^)

It’s often a wise idea to start by thinking about what you want to do before a head first code plunge. Before setting up our zoom, let’s identify what and how we want to zoom and pan. We ask 3 questions:

首先考虑在开始编写首批代码之前要做什么,这通常是一个明智的主意。 在设置缩放之前,让我们确定要缩放和平移的内容方式 。 我们提出3个问题:

  1. What will be our zoom base — the “sensor element” that we’ll use for the zoom?

    我们的缩放基础是什么—我们将用于缩放的“传感器元素”?
  2. What will be our zoom targets — the elements that we will move?

    我们的缩放目标是什么-我们将移动的元素?
  3. What type of zoom do we want for each element — geometric or semantic zoom?

    我们希望每个元素使用哪种类型的缩放-几何缩放或语义缩放?
确定我们的缩放基准 (Identifying our zoom base)

Let’s choose our zoom base element first. You can attach the zoom to an svg, g, rect or any other element that your mouse has access to. Note here that g elements can only register events where they have children with a set fill property. So, if you have a large g element with a circle of radius 1, your zoom gestures will only work on that tiny circle.

首先选择缩放基础元素。 您可以将缩放svg附加到svggrect或鼠标可以访问的任何其他元素上。 在此请注意, g元素只能在具有设置了fill属性的子元素的情况下注册事件。 因此,如果您有一个半径为1的大g元素,则缩放手势将仅在该小圆圈上起作用。

As such, it’s often wise to set up a dedicated rect with fill, but 0 opacity. You have to make sure that the zoom base can consume all events. So, it should either be on top of all other elements, or its pointer-events should be set to all while all other elements’ pointer-events are set to none.

因此,明智的做法是设置一个具有填充但不透明度为0的专用rect 。 您必须确保缩放基准可以占用所有事件。 因此,它应该位于所有其他元素的顶部,或者应将其pointer-events设置为all而将所有其他元素的pointer-events设置为none

In fact, we already totally decided to set up an extra rect element to listen to events. We wisely cached it in the listenerRect variable, which we can refer to upon set-up. Done.

实际上,我们已经完全决定设置一个额外的rect元素来监听事件。 我们明智地将其缓存在listenerRect变量中,我们可以在设置时引用它。 做完了

确定我们的变焦目标 (Identifying our zoom targets)

Now let’s identify our target elements and write them down. Which elements do we want to move when we zoom and pan? Let’s make a list:

现在,让我们确定目标元素并将其写下来。 缩放和平移时要移动哪些元素? 让我们列出一个清单:

  • The planets

    行星
  • The axis and all their elements (tick lines and tick text only; we’re not showing the axis path).

    轴及其所有元素(仅刻度线和刻度文本;我们未显示轴路径)。

Now we know our zoom base and our targets, we want to make sure they share the same coordinate system at the initial zoom state — when no zoom or pan has happened yet. That’s why we attached the zoom base and targets (planets, axis) to the same g above.

现在我们知道了缩放基础和目标,我们想确保它们在初始缩放状态下共享相同的坐标系-当尚未发生缩放或平移时。 这就是为什么我们将缩放基础和目标(行星,轴)附加到同一g上的原因。

This is going really well!

这真的很好!

识别变焦类型 (Identifying the type of zoom)

Lastly, let’s decide how we want to zoom them — geometrically or semantically? First of all, this distinction only makes sense for zooming, not panning. We’ve defined it above, but for the purpose of redundant completeness, let’s repeat that Geometric zoom is simple: all elements are just being scaled up or down uniformly. Semantic zoom is a little more elaborate, as you can decide what you want to scale up or down.

最后,让我们决定如何缩放它们-几何还是语义? 首先,这种区别仅对缩放有意义,而不对平移有意义。 我们已经在上面定义了它,但是出于冗余完整性的目的,让我们重复一下“几何缩放”很简单:所有元素都只是均匀地按比例缩放。 语义缩放稍微复杂一些,因为您可以决定要放大还是缩小。

In our case, we might want to scale up the size of the planets, but keep the line width at 4px. For that we would need semantic zoom. For our educational purposes, let’s implement both types! Why not?

在我们的情况下,我们可能想扩大行星的大小,但将线宽保持在4px。 为此,我们需要语义缩放。 为了我们的教育目的,让我们同时实现这两种类型! 为什么不?

设置变焦 (Setting up the zoom)

For any zoom we decide to implement, we will need to set it up first. You’ll probably agree that it couldn’t be less complex:

对于我们决定实施的任何缩放,我们需要先进行设置。 您可能会同意,它不会那么简单:

var zoom = d3.zoom() .on('zoom', zoomed);

Calling d3.zoom() will return an object and a function. As with many parts of the D3 API, the object allows us to configure the variables we use in the function. So what we do up there is configure the use of the d3.zoom() function with a single method: .on() attaches a handler function called zoomed. zoomed will be called every time we zoom. This is where we’ll make the elements move.

调用d3.zoom()将返回一个对象和一个函数。 与D3 API的许多部分一样,该对象使我们能够配置函数中使用的变量。 因此,我们在那里做的是使用单个方法配置d3.zoom()函数的使用: .on()附加一个名为zoomed的处理函数。 zoomed都会被调用我们的时间zoom 。 这是我们将元素移动的地方。

We have two other zoom cycle events to trigger a function, start and end. It should be relatively easy to guess when they would trigger the callback.

我们还有两个其他的缩放周期事件来触发功能,即startend 。 猜测何时触发回调应该相对容易。

We store the returned function in the creatively named variable zoom. Next, we can use this function as zoom(<listener-element>) or, as it’s more commonly done in D3 <listener-element>;.call(zoom) like so:

我们将返回的函数存储在创造性命名的变量zoom 。 接下来,我们可以将此函数用作zoom(<listener-elemen t>),或者,它通常in D3 <listener-element> ; .call(zoom)中完成,如下所示:

listenerRect.call(zoom);

That’s great, but what does that mean? It means that the listenerRect is now the official home of our zoom. Our zoom base! At this very moment, it has two things dangling off it: the .on() event and the zoom transform. If we console.dir(d3.select(‘#listener-rect’).node()) and check our attributes, we’ll find these two D3 properties at the very bottom of the list:

很好,但这是什么意思呢? 这意味着listenerRect现在是我们zoom的官方主页。 我们的变焦座 ! 目前,它有两件事.on().on()事件和zoom变换。 如果我们console.dir(d3.select('#listener-rect').node())并检查我们的属性,我们将在列表的最底部找到这两个D3属性:

The __on object holds our listener information, and the __zoom object is a transform object holding the 3 values we discussed in the beginning of this pot: the x and y translation when we zoom and pan, and the scale factor k changing upon zoom.

__on对象保存我们的侦听器信息,而__zoom对象是一个转换对象,其中包含我们在锅中开始讨论的3个值:缩放和平移时的xy平移,缩放时缩放的比例因子k改变。

You can always come to your zoom base — the listenerRect for us — to query the current transform values. However, you don’t need to do so very often, as the transform will be handily accessible in the event object from within our zoomed handler function. Right. For the love of our lives — let’s finally zoom.

您总是可以使用缩放基础(我们的listenerRect来查询当前的变换值。 但是,您不需要经常执行此操作,因为可以从zoomed处理函数中方便地在事件对象中访问转换。 对。 为了热爱我们的生活,让我们最终放大。

SVG的几何缩放 (Geometric zoom with SVG)

We have our static visual. We’ve set up the zoom. We’ve attached it to the zoom base. Let’s finally decide which type of zoom we’re going for. Here’s the thing: axes should be zoomed semantically, you decide for the other elements. Going back to our zoom targets, let’s decree this here in a table on a piece of parchment:

我们有静态的视觉效果。 我们已经设置好缩放比例。 我们已将其附加到缩放基础。 最后,让我们决定要使用的变焦类型。 事情就是这样:轴应该在语义上进行缩放,您可以确定其他元素。 回到缩放目标,让我们在一张羊皮纸上的一个表中声明以下内容:

Now, let’s write the zoom handler:

现在,让我们编写缩放处理程序:

function zoomed() {
var transform = d3.event.transform;
gPlanets.attr('transform', transform.toString());
}

We’re not quite done yet, but this is the simplest zoom possible and will already move our planets. We cache the transform object that dangles off the d3.event object which gets passed in on every zoom and pan move in the variable transform. Then we move our planets by just updating the transform attribute of our circles.

我们还没有完成任务,但这是最简单的缩放方法,已经可以移动我们的行星了。 我们缓存了d3.event对象d3.event的转换对象,该对象在变量transform每次缩放和平移中都会传递。 然后,仅通过更新圆的transform属性来移动行星。

transform.toString() is just a convenience method the transform object gives us. It saves us from having to type out the transform attribute’s value. For the identity transform { k: 1, x: 0, y: 0 } it returns the string "translate(0, 0) scale(1)"

transform.toString()只是transform对象提供给我们的一种便捷方法。 它使我们不必键入转换属性的值。 对于身份转换{ k: 1, x: 0, y: 0 }它将返回字符串"translate(0, 0) scale(1)"

How will this look?

看起来如何?

Very good! The planets are moving — the rest is not. We need to do 3 things to improve this:

很好! 行星在运动-其余部分则没有。 我们需要做三件事来改善这一点:

  1. Let’s prohibit the planets from moving to the right (there’s no planet left of the sun, so it would be futile).

    让我们禁止行星向右移动(太阳没有左边的行星,所以这将是徒劳的)。
  2. Let’s also prohibit the planets from moving up and down.

    我们也禁止行星上下移动。
  3. Move the scales.

    移动秤。

1 and 2 are simple; we just manipulate the transform object before we use it like so:

1和2很简单; 我们只是在像这样使用转换对象之前对其进行操作:

function zoomed() {
var transform = d3.event.transform;     transform.x = Math.min(0, transform.x);   transform.y = 0;
gPlanets.attr('transform', transform.toString());
}

As a result, x is never higher than 0, and therefore we can’t move the thing to the right. Also, y will always be 0. The result does what we expect:

结果, x永远不会大于0,因此我们不能将其右移。 此外, y始终为0。结果符合我们的预期:

Next, let’s make the axis move semantically. Our axis consists of labels and lines. We choose semantic over geometric zoom, as we only want to change their position on zoom — not the label size or the line width.

接下来,让我们使轴在语义上移动。 我们的轴由标签和线条组成。 我们选择语义而不是几何缩放,因为我们只想更改它们在缩放上的位置 -而不是标签大小或线宽。

The main positioning engine behind the axis’ elements — the thing that makes the labels and lines move — is the scale. And what does the scale do? The scale maps our data values to the width of our svg element. If we want to change a scale with D3, we usually update the scale’s domain and/or range. But as rescaling axes is such a common activity for D3 zoom, we have the rescaleX() and rescaleY() methods dangling off the transform object. It updates the mapping for us according to the zoom. Perfect syntactic sugar we can use to create an updated scale:

轴元素后面的主要定位引擎(使标签和线条移动的东西)是比例尺。 规模是做什么的? 比例尺将我们的数据值映射到svg元素的宽度。 如果要使用D3更改比例,通常会更新比例的域和/或范围。 但是由于缩放轴是D3缩放的一种常见活动,因此我们有rescaleX()rescaleY()方法悬挂在transform对象之外。 它根据缩放为我们更新映射。 我们可以使用完美的语法糖来创建更新的量表:

var xScaleNew = transform.rescaleX(xScale);

The next section is called Semantic Zoom with SVG and will carelessly open the hood of this rescaleX() method in much more detail. But for now, let's just use xScaleNew trustingly like so:

下一部分称为带SVG的语义缩放,将不经意地更详细地打开此rescaleX()方法的内容。 但是现在,让我们像下面这样信任地使用xScaleNew

xAxis.scale(xScaleNew); xAxisDraw.call(xAxis);

We update the scale of our xAxis and redraw the axis with our new axis component. The last thing we need to do to the axis is stagger our labels and lines again, as we’ve done above.

我们更新xAxis的比例,并使用新的axis组件重新绘制轴。 我们需要做的最后一件事是再次交错标签和线条,就像上面所做的那样。

// Stagger the axis-labels xAxisDraw.selectAll('text')   .attr('y', function(d, i) {     return -(i * labelHeight + labelHeight);   })
// Stagger the axis-lines xAxisDraw.selectAll('line')   .attr('y1', function(d, i) {     return -(i * labelHeight + labelHeight);   })   .attr('y2', function(d, i) {     return -(i * labelHeight + labelHeight            + (data.length-1-i) * labelHeight + height);   });

Remember, all of this happens in our zoomed handler.

请记住,所有这些都发生在我们的zoomed处理程序中。

It works:

有用:

SVG的语义缩放 (Semantic zoom with SVG)

This headline comes a bit late. We have semantically zoomed our axis already. But now let’s also apply it to our planets and dive into the rescaling process. Here’s our updated prep table:

这个标题来得有点晚。 我们已经在语义上缩放了轴。 但是,现在我们也将其应用于我们的星球,并深入研究重新缩放过程。 这是我们更新的准备表:

圆环的语义缩放 (Semantic zoom of circles)

First of all, why would we want to use semantic zoom on the planets? I guess the above gif demonstrates the semantic need pretty well. As the planets get smaller, their outline is nearly impossible to see. With semantic zoom, we will have control over which element properties change or remain. In our case, zoom should change the position as well as the size of our planets, but the width of the outline stroke should stay constant at 4px.

首先,为什么我们要对行星使用语义缩放? 我想上面的gif很好地说明了语义需求。 随着行星变小,几乎看不到它们的轮廓。 使用语义缩放,我们将可以控制更改或保留哪些元素属性。 在我们的情况下,缩放应会更改位置以及行星的大小,但轮廓描边的宽度应保持恒定为4px。

What we do is simple:

我们的工作很简单:

function zoomed() {
var transform = d3.event.transform;
transform.x = Math.min(0, transform.x);
var xScaleNew = transform.rescaleX(xScale);
planets     .attr('cx', function(d) {       return xScaleNew(d.distance);     })    .attr('r', function(d) {       return d.scaledRadius * transform.k;     });
// Zoom and pan the axis here (…)
}

First, we remove our geometric planet zoom. Then we grab our planets and, instead of transforming them, we specifically only access their cx and the r attributes. The x position will be re-calculated with the updated xScaleNew and the radius just needs to be multiplied by the scale factor. No translation necessary here.

首先,我们删除几何行星缩放。 然后,我们抓住我们的行星,而不是对其进行变换,而仅访问它们的cxr属性。 x位置将使用更新的xScaleNew重新计算,而半径仅需乘以比例因子即可。 此处无需翻译。

And that’s it:

就是这样:

However far we zoom in or out, our stroke remains at 4px allowing us to actually see our planets even if they’re fully zoomed out.

无论我们放大还是缩小,我们的笔触都保持在4px,即使它们完全缩小,我们也可以实际看到它们。

了解缩放重新缩放 (Understanding zoom rescale)

Semantic zoom requires us to zoom and pan properties selectively. Our semantic planet zoom above only changed the cx and the r attribute, while keeping the stroke width at 4px. To specifically change cx, we needed to update our scale — the main positioning engine of our visualisation — so that it positions our elements according to the new transform.

语义缩放要求我们选择性地缩放和平移属性。 我们上方的语义星球缩放仅更改了cxr属性,同时将笔划宽度保持为4px。 要专门更改cx ,我们需要更新比例尺(可视化的主要定位引擎),以便它根据新的变换来定位元素。

As said, D3 offers the convenience methods rescaleX() and rescaleY() to update scales according to the transform. Of course it’s perfectly fine to use these methods without knowing the inner workings, so please feel free to jump straight to the next section. But if you’re curious about how exactly the rescale happens, stay with me. There'll be images in color, too.

如前所述,D3提供了方便的方法rescaleX()rescaleY()来根据变换更新尺度。 当然,在不了解内部工作原理的情况下使用这些方法是完全可以的,因此请随时直接跳至下一部分 。 但是,如果您对重新调整的精确程度感到好奇,请与我在一起。 也将有彩色图像。

We’ll use a real simple example. Let’s assume we only look at the x-dimension, and we want to map a data space that covers a domain from 0 to 100 to a 1000 pixel wide screen. As such we have a data domain of [0, 100] we want to map to a width range of [0, 1000]. Our scale would look like this:

我们将使用一个简单的示例。 假设我们只看x维度,并且想要将覆盖从0到100的域的数据空间映射到1000像素宽的屏幕。 因此,我们有一个[0,100]的数据域,我们希望映射到[0,1000]的宽度范围。 我们的规模如下所示:

var xScale = d3.scaleLinear()   .domain([0, 100])   .range([0, 1000]);

Let’s also assume that we have a single circle with the data value 20, which would be mapped to the pixel value 200:

我们还假设我们有一个圆,其数据值为20,它将映射到像素值200:

Easy. Now we zoom in so that our scale factor k will be 2. No translation, just zoom. As a result, our circle would move according to our zoom transform formula we started this post with: tx + x × k, which would result in 0 + 200 × 2 = 400:

简单。 现在我们放大,使比例因子k为2。无需平移,只需缩放即可。 结果,我们的圆将根据我们以以下内容开始的缩放变换公式移动: tx + x×k ,这将导致0 + 200×2 = 400

Note, we’ve also scaled up its radius by 2. All good so far? Great.

请注意,我们还将其半径扩大了2。到目前为止,一切都很好吗? 大。

In this case, we could just do our transform calculation for the circle. But it’s much simpler, more convenient, and more consistent to continue to use our scale. However, we need to update it, as our data value 10 shouldn’t scale to 100px anymore but to 200px!

在这种情况下,我们可以对圆进行变换计算。 但是,继续使用我们的体重秤更加简单,方便和一致。 但是,我们需要对其进行更新,因为我们的数据值10不应再缩放到100px,而应该缩放到200px!

How do we do this? As we’ve done above, we just pass our xScale to the transform.rescaleX() function. This returns the respectivley updated newXScale, which we use on the circle’s data value to determine the cx position:

我们如何做到这一点? 如上所述,我们只是将xScale传递给transform.rescaleX()函数。 这将返回更新后的尊重的newXScale ,我们将其用于圆的数据值以确定cx位置:

var newXScale = transform.rescaleX(xScale);
circle.attr(‘cx’, function(d) { return d.dataValue; }); // note: d.dataValue is from a fictitious dataset

But what exactly does this rescale do? Let’s look at the source code first before considering its logic. A rescale under the hood looks like so:

但是,这种重新缩放的确切作用是什么? 在考虑其逻辑之前,让我们先看一下 代码 。 引擎盖下的重新缩放看起来像这样:

function rescaleX(x) {
var range = x.range().map(transform.invertX, transform),
domain = range.map(x.invert, x);
return x.copy().domain(domain);
}

As you can see in the last line, it will return the original scale BUT with an updated domain. The range will remain as is. If you asked me before I looked at this code, I would’ve guessed D3 would update the range and keep the domain as is. Much more direct. But it’s the other way around. This makes sense, as the pixel range is a more static concept. In our case, 1000 is the width of the screen — that won’t change upon zoom.

如您在最后一行所看到的,它将返回具有更新域的原始比例BUT 。 范围将保持不变。 如果您在查看这段代码之前问我,我猜想D3会更新范围并保持域名不变。 更直接。 但这是另一回事。 这是有道理的,因为像素范围是一个更静态的概念。 在我们的例子中,屏幕的宽度为1000,缩放后不会改变。

The (small) downside is that the new domain calculation is slightly more involved than a new range calculation would be. There are 4 steps involved in calculating the new domain at each zoom and pan move:

不利的一面是,新的域计算比新的范围计算要复杂得多。 每次缩放和平移时,计算新域涉及四个步骤:

  1. We first take the range of our original scale. In our example that would be [0, 1000].

    我们首先采用原始比例的范围。 在我们的示例中,该值为[0,1000]。
  2. We then apply the inverse transform to it, which will return [0, 500].

    然后,我们对其应用逆变换,该逆变换将返回[0,500]。
  3. Next we will use the scale’s .invert() method to find the data value associated with the range values 0 and 500, which will be [0 and 50] in our case.

    接下来,我们将使用scale的.invert()方法来查找与范围值0和500相关联的数据值,在本例中为0和50。

  4. Finally, we override the current x-scale domain with this new domain and return it.

    最后,我们用这个新域覆盖当前的x缩放域并返回它。

But why? Let’s consider this conceptually…

但为什么? 让我们从概念上考虑一下……

First, we calculate a new range by taking the inverse of our transform function for the x value. By now we know the zoom transform function for x is tx + x × k. Its inverse is (x — tx) / k.

首先,我们通过对x值采用变换函数的逆来计算新范围。 至此,我们知道x的缩放变换函数为tx + x×k 。 它的逆是(x_tx)/ k

If you never came across inverse functions, they are just the opposite — the reverse of their main function. If you had f(x) = 3 × x then the inverse is g(y) = y/3. Plugging 2 into the main function f(x) returns 6 — plugging this 6 into the inverse function g(y) returns 2 again. It reverses the process of the main function.

如果您从未遇到过逆函数,那么它们就是相反的-主函数的反面。 如果您有f(x)= 3×x,则逆为g(y)= y / 3 。 将2插入主函数f(x)返回6-将6插入反函数g(y)再次返回2。 它颠倒了主要功能的过程。

Why do we take the inverse on our range? We want to adjust the domain, but keep the range at [0, 1000]. The easiest way to get the updated domain is to first calculate updated range extent values (min and max) in order to derive the new domain extent values from them.

为什么我们对范围取反? 我们要调整域,但将范围保持在[0,1000]。 获取更新域的最简单方法是首先计算更新的范围范围值(最小值和最大值),以便从它们中导出新的范围范围值。

Let’s play this through with a single value. Let’s take our maximum range value of 1000. Our current scale maps the maximum data value of 100 to the maximum range value of 1000 pixels.

让我们通过一个单一的值来解决这个问题。 让我们取最大范围值1000。我们当前的比例将最大数据值100映射到最大范围值1000像素。

What’s the max range value when we scale by 2? Scaling by 2 means we’re zooming in. So, our current max range value of 1000 will move to 2000 (0 + 1000 × 2). However, we would like to know the new pixel point that moves to the edge of our screen when we zoom. The previous point that was at 1000 and is now at 2000 is no help to us as it’s beyond the screen area now. So, which point is at the edge of our window after we zoomed? Which point is our new maximum range value?

当我们缩放2时,最大范围值是多少? 按2缩放表示我们正在放大。因此,当前的最大范围值1000将移至2000 (0 + 1000×2) 。 但是,我们想知道在缩放时移动到屏幕边缘的新像素点。 以前的1000点到现在的2000点对我们没有帮助,因为它现在超出了屏幕区域。 那么,缩放后哪个点位于窗口的边缘? 我们新的最大范围值是哪一点?

In order to get that point, we don’t ask: where does our current max range value of 1000 zoom to? We ask, where does the new max range value come from! Logically, this is the opposite or the INVERSE question. Accordingly, we apply the inverse zoom transformation: (x — tx) / k. We plug in our previous max range value of 1000px, our tx of 0 and scale k of 2 to get: (1000–0) / 2 = 500.

为了达到这一点,我们不问:我们当前的最大最大范围值1000会缩放到哪里? 我们问,新的最大范围值从何而来! 逻辑上,这是相反的或问题。 因此,我们应用逆缩放变换: (x — tx)/ k 。 我们插入之前的最大范围值1000px, tx为0,比例k为2,以获得: (1000-0)/ 2 = 500

We can now say that our new maximum range value would come from the pixel position 500.

现在我们可以说我们新的最大范围值将来自像素位置500。

Why did we do this again? Isn’t this all a bit silly as we want to keep the range at [0, 1000] anyway? Yes. And no. It’s not silly, because we don’t use this new maximum range value in a new range input for our scale. We just use it to find our new maximum data domain value.

为什么我们再次这样做? 反正我们想要将范围保持在[0,1000]也不是那么愚蠢吗? 是。 和不。 这不是很愚蠢,因为我们没有在新的量程输入中使用这个新的最大量程值作为刻度。 我们只是使用它来查找新的最大数据域值。

We take our original scale that mapped a data value of 0 to 0 pixel, a data value of 100 to 1000 pixel and all in-between values accordingly. Now we ask which data value maps to the pixel value of 500? For this simple case we can use our brain, or — much better — we use the .invert() method of our original x-scale. xScale.invert(500) will return 50 as probably expected.

我们采用原始比例,将数据值映射为0到0像素,100到1000像素的数据值以及所有中间值。 现在我们问哪个数据值映射到像素值500? 对于这种简单的情况,我们可以使用我们的大脑,或者-更好-我们使用原始x缩放的.invert()方法。 xScale.invert(500)将返回50,这可能与预期的一样。

Let’s remember here that we still have our original range of [0, 1000]. All the range calculations we have done were only done in order to get to the new domain. Our new x-scale still maps the data value 0 to pixel 0, but now maps the new maximum data domain value of 50 to the loyally standing maximum range value of 1000.

让我们记住这里,我们仍然有原始范围[0,1000]。 All the range calculations we have done were only done in order to get to the new domain. Our new x-scale still maps the data value 0 to pixel 0, but now maps the new maximum data domain value of 50 to the loyally standing maximum range value of 1000.

Likewise, our circle center x value still has the data value of 10, which now doesn’t map to 100 but to 200. We successfully zoomed in, we did.

Likewise, our circle center x value still has the data value of 10, which now doesn't map to 100 but to 200. We successfully zoomed in, we did.

Well done! Now, onwards to Canvas. Same game — different board…

做得好! Now, onwards to Canvas. Same game — different board…

Geometric zoom with Canvas ^ (Geometric zoom with Canvas ^)

We only have 10 circles on our site. However, there are of course a great many more orbs out there to visualize. Visualizing more than 1000 of them might get you into render performance troubles, which you can attempt to cure with Canvas.

We only have 10 circles on our site. However, there are of course a great many more orbs out there to visualize. Visualizing more than 1000 of them might get you into render performance troubles, which you can attempt to cure with Canvas.

Unlike SVG, Canvas produces a single bitmap of your drawing. 1000 planets on your screen will be drawn to a single DOM element, the canvas. In SVG 1000 planets will produce 1000 circle elements that the browser has to maintain, which affects performance. There’s a list of Canvas resources in the sources section below if you want to know more, but don’t worry, you don’t need a Canvas degree to follow along.

Unlike SVG, Canvas produces a single bitmap of your drawing. 1000 planets on your screen will be drawn to a single DOM element, the canvas . In SVG 1000 planets will produce 1000 circle elements that the browser has to maintain, which affects performance. There's a list of Canvas resources in the sources section below if you want to know more, but don't worry, you don't need a Canvas degree to follow along.

We will change very little in our app. As a quick reminder, here are the main steps we’ve folowed to get here:

We will change very little in our app. As a quick reminder, here are the main steps we've folowed to get here:

  1. Load data

    Load data
  2. Calculate the dimensions of our visual

    Calculate the dimensions of our visual
  3. Build the SVG base and the listener rectangle

    Build the SVG base and the listener rectangle

  4. Calculate the scales

    Calculate the scales
  5. Define and draw the axis

    Define and draw the axis
  6. Build the SVG visual

    Build the SVG visual

  7. Zoom

    Zoom

We’ll change points 3, 6 and 7 above and leave the rest unchanged. In fact, we won’t produce a pure Canvas drawing, but rather will draw the planets in Canvas and keep the axes in SVG. This is called Mixed-mode rendering, and is really clever if you have axes to draw. Drawing axes is wonderfully solved by D3 in SVG but can be a pain in Canvas. (Elijah Meeks dedicates a good section to Mixed-mode rendering in chapter 11 of his book D3js in Action)

We'll change points 3, 6 and 7 above and leave the rest unchanged. In fact, we won't produce a pure Canvas drawing, but rather will draw the planets in Canvas and keep the axes in SVG. This is called Mixed-mode rendering, and is really clever if you have axes to draw. Drawing axes is wonderfully solved by D3 in SVG but can be a pain in Canvas. ( Elijah Meeks dedicates a good section to Mixed-mode rendering in chapter 11 of his book D3js in Action )

Adding a canvas base (Adding a canvas base)

As with SVG, we need a base to draw on. For Canvas we need two things, the canvas element and its drawing context — the tools we can use to draw on the canvas. Below our svg base we add the following Canvas base snippet:

As with SVG, we need a base to draw on. For Canvas we need two things, the canvas element and its drawing context — the tools we can use to draw on the canvas. Below our svg base we add the following Canvas base snippet:

var canvas = d3.select('#vis').append('canvas')   .attr('width', screenWidth + margin.left + margin.right)     .attr('height', height + margin.top + margin.bottom);
var context = canvas.node().getContext('2d');

It’s often wise to skip the margin convention for Canvas (we don’t have a g we can move around). But, especially when drawing SVG axes, we want to cling on to our margins.

It's often wise to skip the margin convention for Canvas (we don't have a g we can move around). But, especially when drawing SVG axes, we want to cling on to our margins.

We also want to overlay our canvas element perfectly over our svg element and its children, the planet g and the listenerRect. To achieve this, we need to give it the same size as the svg element and position the canvas absolute on top of the svg. Here’s our CSS:

We also want to overlay our canvas element perfectly over our svg element and its children, the planet g and the listenerRect . To achieve this, we need to give it the same size as the svg element and position the canvas absolute on top of the svg . Here's our CSS:

canvas {   position: absolute;   top: 0;   left: 0;   pointer-events: none; }

Notice that we also remove all pointer-events from our canvas so that the listenerRect receives all gestures. As a result, we have quite a few layers:

Notice that we also remove all pointer-events from our canvas so that the listenerRect receives all gestures. As a result, we have quite a few layers:

The g now only holds our axis, which we can view through our svg. The canvas will display our planets, but only the section in green above (the other planets are drawn here for completeness but will initially be invisible). The top level is the listenerRect consuming all pointer events and informing our zoom and pan.

The g now only holds our axis, which we can view through our svg . The canvas will display our planets, but only the section in green above (the other planets are drawn here for completeness but will initially be invisible). The top level is the listenerRect consuming all pointer events and informing our zoom and pan.

Drawing the planet circles in Canvas (Drawing the planet circles in Canvas)

We remove the logic that built out the SVG planets, and instead draw our Canvas circles. We will draw it in a single function. Let me first show you the code of this Canvas drawing function before running you through it. Here we go:

We remove the logic that built out the SVG planets, and instead draw our Canvas circles. We will draw it in a single function. Let me first show you the code of this Canvas drawing function before running you through it. 开始了:

function drawGeometricCircles(data, transform) {

We pass our data and the transform. If we only wanted to build a static visual we wouldn’t need to worry about the transform, but zooming is very much our mission!

We pass our data and the transform. If we only wanted to build a static visual we wouldn't need to worry about the transform, but zooming is very much our mission!

context.clearRect(0, 0, screenWidth + margin.left + margin.right,                       height + margin.top + margin.bottom);

Next, we access our Canvas context (we cached in the context variable) and run a method called .clearRect. You can surely guess what it does — it clears the canvas. We pass it the canvas dimensions, which will clear the canvas every time we call this function.

Next, we access our Canvas context (we cached in the context variable) and run a method called .clearRect . You can surely guess what it does — it clears the canvas . We pass it the canvas dimensions, which will clear the canvas every time we call this function.

This is what we do with Canvas. Unlike in SVG where we have manifest nodes in the DOM for our circles, we only have a pixel image on our canvas. Instead of moving around a DOM node, we just remove the image we drew earlier and draw a new image with elements in slightly different positions. That’s Canvas for you.

This is what we do with Canvas. Unlike in SVG where we have manifest nodes in the DOM for our circles, we only have a pixel image on our canvas . Instead of moving around a DOM node, we just remove the image we drew earlier and draw a new image with elements in slightly different positions. That's Canvas for you.

context.save();

Then we .save() the default and unchanged context, and we .restore() it in a moment after all drawing is done. This way we secure not only a blank canvas slate, but also a blank context slate whenever we draw a new planet.

Then we .save() the default and unchanged context, and we .restore() it in a moment after all drawing is done. This way we secure not only a blank canvas slate, but also a blank context slate whenever we draw a new planet.

context.lineWidth = 4;   context.strokeStyle = 'deeppink';   context.fillStyle = 'white';

Next, we define our painting brushes. We want a line width of 4px, we want a stroke color of deeppink and a fill of white. These aesthetic properties will apply to everything we draw after we set them. Until we change them.

Next, we define our painting brushes. We want a line width of 4px, we want a stroke color of deeppink and a fill of white. These aesthetic properties will apply to everything we draw after we set them. Until we change them.

context.translate(transform.x + margin.left, margin.top);       context.scale(transform.k, transform.k);

These next two lines are the geometric zoom. We translate and scale the entire image we draw by the respective transform values.

These next two lines are the geometric zoom. We translate and scale the entire image we draw by the respective transform values.

for (var i = 0; i < data.length; i++) {
context.beginPath();    context.arc(xScale(data[i].distance), 0,                 rScale(data[i].radius), 0, 2 * Math.PI, false);     context.stroke();     context.fill();
context.fill();
}
context.restore();
}

Finally, we draw the circles. If you haven’t seen much of Canvas yet, this might look a little raw. And indeed, D3 internalizes this loop through the elements for us by joining the data to selections that we can subsequently access, position, and style.

Finally, we draw the circles. If you haven't seen much of Canvas yet, this might look a little raw. And indeed, D3 internalizes this loop through the elements for us by joining the data to selections that we can subsequently access, position, and style.

With Canvas, we do this ourselves. We loop through the data, start a path, draw the path as a circle with the context.arc() method, and finally stroke and fill the path.

With Canvas, we do this ourselves. We loop through the data, start a path, draw the path as a circle with the context.arc() method, and finally stroke and fill the path.

The rest is a piece of code. We just need to call it right here, and then with our data and the identity, transform, which is simply { k: 1, x: 0, y: 0 }:

The rest is a piece of code. We just need to call it right here, and then with our data and the identity, transform, which is simply { k: 1, x: 0, y: 0 } :

drawGeometricCircles(data, d3.zoomIdentity);

Whenever we zoom, we replace the code that moved our SVG planets with this:

Whenever we zoom, we replace the code that moved our SVG planets with this:

drawGeometricCircles(data, transform);

I’ll spare you the gif as it looks exactly like what we’ve seen above with geometric SVG zoom. But the working implementation with code is just a click away!

I'll spare you the gif as it looks exactly like what we've seen above with geometric SVG zoom. But the working implementation with code is just a click away !

Semantic zoom with Canvas (Semantic zoom with Canvas)

Let’s celebrate our geometric zoom feat by getting rid of it. In fact, to achieve semantic instead of geometric zoom, we will just rename and change our draw function. We will call it appropriately drawSemanticCircles().

Let's celebrate our geometric zoom feat by getting rid of it. In fact, to achieve semantic instead of geometric zoom, we will just rename and change our draw function. We will call it appropriately drawSemanticCircles() .

Changing from geometric to semantic zoom in Canvas requires the same high-level actions. Instead of translating and scaling the planet’s coordinate system, we will change the planet’s positions and radius according to the transforms.

Changing from geometric to semantic zoom in Canvas requires the same high-level actions. Instead of translating and scaling the planet's coordinate system, we will change the planet's positions and radius according to the transforms.

drawSemanticCircles() will clear our canvas and then draw all circles with drawCircle():

drawSemanticCircles() will clear our canvas and then draw all circles with drawCircle() :

function drawSemanticCircles(data, transform) {
context.clearRect(0, 0, screenWidth + margin.left + margin.right,                     height + margin.top + margin.bottom);
for (var i = 0; i < data.length; i++) {     drawCircle(data[i], transform);   }
}

drawCircle() will be run for each data element, taking the data element and the current transform:

drawCircle() will be run for each data element, taking the data element and the current transform:

function drawCircle(elem, transform) {
var x = (transform.x + transform.k * xScale(elem.distance))           + margin.left;  var y = margin.top;   var r = transform.k * rScale(elem.radius);
context.lineWidth = 4; context.strokeStyle = 'deeppink';     context.fillStyle = 'white';
context.beginPath();   context.arc(x, y, r, 0, 2 * Math.PI);   context.stroke();   context.fill();
}

We first determine the x and the y positions as well as the radius r. Then we define the styles for our circles. Lastly, we draw our galactic spheres as arcs. And that’s it…

We first determine the x and the y positions as well as the radius r . Then we define the styles for our circles. Lastly, we draw our galactic spheres as arcs. And that's it…

Great! We’ve covered the two types of zoom in two renderers. On to the bonus tracks: programmatic zoom and making our galaxy pretty…

大! We've covered the two types of zoom in two renderers. On to the bonus tracks: programmatic zoom and making our galaxy pretty…

Programmatic zoom (Programmatic zoom)

It’s often helpful to move our visuals into a certain position. You can let a user center a map, move a long bar chart to the beginning, or zoom in and out of the solar system.

It's often helpful to move our visuals into a certain position. You can let a user center a map, move a long bar chart to the beginning, or zoom in and out of the solar system.

We have neither a map nor a bar chart, so let’s programmatically zoom out and back into our planets upon load. We go back to SVG for this, as we don’t really need Canvas here. Because of its lower level, I’d recommend using Canvas only if you need it or speak it like your mother tongue. As we only have 10 circles to move around here, we don’t need it.

We have neither a map nor a bar chart, so let's programmatically zoom out and back into our planets upon load. We go back to SVG for this, as we don't really need Canvas here. Because of its lower level, I'd recommend using Canvas only if you need it or speak it like your mother tongue. As we only have 10 circles to move around here, we don't need it.

Here’s what we want to achieve:

Here's what we want to achieve:

We start with a heavily zoomed-in visual at a zoom scale of 20. We then zoom out to our minimum zoom, so all planets fit comfortably on the page. Lastly, we zoom back in to our default zoom scale of 1.

We start with a heavily zoomed-in visual at a zoom scale of 20. We then zoom out to our minimum zoom, so all planets fit comfortably on the page. Lastly, we zoom back in to our default zoom scale of 1.

To achieve this, we bolt on the programmatic logic to the bottom of our make() function where all our app code lives. We start by zooming in to a scale factor of 20 without panning:

To achieve this, we bolt on the programmatic logic to the bottom of our make() function where all our app code lives. We start by zooming in to a scale factor of 20 without panning:

var initialTransform = d3.zoomIdentity.scale(20); listenerRect.call(zoom.transform, initialTransform);

d3.zoomIdentity returns the identity transform we have already encountered a few times. We change the transform scale to 20 and cache it in initialTransform. Then we use the zoom.transform() function. This function is obviously different from our transform object, but it directly manipulates it. We use it here with D3's own <selection>.call() method we encountered above. The selection we call zoom.transform() on will be its first argument. It will be our zoom base listenerRect, home to our current transform object. The second argument has to be a new transform object. It will replace the current transform on that node.

d3.zoomIdentity returns the identity transform we have already encountered a few times. We change the transform scale to 20 and cache it in initialTransform . Then we use the zoom.transform() function. This function is obviously different from our transform object, but it directly manipulates it. We use it here with D3's own <selection>. call() method we encountered above. The selection we call zoom.trans form() on will be its first argument. It will be our zoom base listen erRect, home to our current transform object. The second argument has to be a new transform object. It will replace the current transform on that node.

The cherry on top is that instead of passing our zoom base as a simple selection, we can pass it as a transition. Remember (or note) that transitions are just derived selections, so passing in listenerRect.transition() will in fact transition our visual from one transform to the other.

The cherry on top is that instead of passing our zoom base as a simple selection, we can pass it as a transition. Remember (or note) that transitions are just derived selections, so passing in listenerRect.transition() will in fact transition our visual from one transform to the other.

But so far, we’ve just snapped our visual to a scale of 20. Let’s kick off the transition. First to a scale of minZoom we have defined earlier, then to a scale of 1. Here’s what we do:

But so far, we've just snapped our visual to a scale of 20. Let's kick off the transition. First to a scale of minZoom we have defined earlier, then to a scale of 1. Here's what we do:

// Trigger programmatic zoom progZoom()

Let’s write it. It won’t take any arguments:

写吧 It won't take any arguments:

function progZoom() {

We first define the transform for the minZoom we want to zoom to first:

We first define the transform for the minZoom we want to zoom to first:

var zoomOutTransform = d3.zoomIdentity.scale(minZoom);

In the following lines, we turn our listenerRect into a transition and call zoomTransform() again. Using .call() we pass in the transition we just built as a first argument and zoomOutTransform — the minZoom transform we just saved:

In the following lines, we turn our listenerRect into a transition and call zoomTransform() again. Using .call() we pass in the transition we just built as a first argument and zoomOutTransform — the minZoom transform we just saved:

listenerRect   .transition()   .duration(5000)   .call(zoom.transform, zoomOutTransform)     .on('end', zoomToNormal)

At the end of the zoom we call a function called zoomToNormal. It does exactly what we just have done, apart from transition-zooming to an identity transform:

At the end of the zoom we call a function called zoomToNormal . It does exactly what we just have done, apart from transition-zooming to an identity transform:

function zoomToNormal() {   listenerRect     .transition()     .duration(3000)     .ease(d3.easeQuadInOut)     .call(zoom.transform, d3.zoomIdentity) }

Apart from zooming to a different transform, we’re also setting a different duration as well as a different easing function.

Apart from zooming to a different transform, we're also setting a different duration as well as a different easing function.

}

And that was our first bonus track. On to track two…

And that was our first bonus track. On to track two…

Making our visual pretty (Making our visual pretty)

It’s wise to get your visuals right in black and white first (pink and white in our case). But in the end, a lick of paint can’t hurt. In order to get here…

It's wise to get your visuals right in black and white first (pink and white in our case). But in the end, a lick of paint can't hurt. In order to get here…

…we only need to change a few things, of which the planet’s glow is probably the most elaborate. Let’s look at the rest first:

…we only need to change a few things, of which the planet's glow is probably the most elaborate. Let's look at the rest first:

We’ll add a dark blue background with a radial gradient moving into the dark blue from a slightly lighter one. It’s one line in our body CSS:

We'll add a dark blue background with a radial gradient moving into the dark blue from a slightly lighter one. It's one line in our body CSS:

body {   font-family: Avenir, sans-serif;  sans-serif;   font-size: 0.75rem;   margin: 0;   background: radial-gradient(#091C33, #091426); }

We change the text and line colour to a grey off-white (#ddd), and instead of the solid lines, we render dashed lines with wide gaps:

We change the text and line colour to a grey off-white ( #ddd ), and instead of the solid lines, we render dashed lines with wide gaps:

.tick line, .lines {   stroke: #ddd;   stroke-width: 0.5;   shape-rendering: crispEdges;   stroke-dasharray: 1,5; }

Lastly, we fill the planets with our favourite deeppink and add the glow. The glow is an SVG filter we apply to each planet. I won’t go into detail here, but you can find the code commented right here. In short, we thicken the planets a little bit before feathering them with some Gaussian blur. We fill the blur deeppink and marvel at the resulting glow. The filter gets an id of #soft-glow, which our planets can reference with the filter attribute:

Lastly, we fill the planets with our favourite deeppink and add the glow. The glow is an SVG filter we apply to each planet. I won't go into detail here, but you can find the code commented right here . In short, we thicken the planets a little bit before feathering them with some Gaussian blur. We fill the blur deeppink and marvel at the resulting glow. The filter gets an id of #soft-glow , which our planets can reference with the filter attribute:

var planets = gPlanets.selectAll('.planet')     .data(data)  .enter().append('circle')     .attr('class', 'planet')     // (…)     .attr('filter', 'url(#soft-glow)');

And that’s it!

就是这样!

We’ve come a long way, and hopefully you now understand D3 zoom a little better. We’ve looked into a short recipe you can follow before and during wiring your visual up with any zooming and panning. We then applied this blueprint to a real project with pink orbs, playing through geometric and semantic zoom rendering in SVG as well as Canvas. As a bonus, we looked at programmatic zoom and finally made its subtly pink face even more pink. What fun!

We've come a long way, and hopefully you now understand D3 zoom a little better. We've looked into a short recipe you can follow before and during wiring your visual up with any zooming and panning. We then applied this blueprint to a real project with pink orbs, playing through geometric and semantic zoom rendering in SVG as well as Canvas. As a bonus, we looked at programmatic zoom and finally made its subtly pink face even more pink. What fun!

Two more things that might help: a quick note on updating your zoom from D3 v3 to v4, and a list of sources.

Two more things that might help: a quick note on updating your zoom from D3 v3 to v4, and a list of sources.

Updating zoom from v3 to v4 (Updating zoom from v3 to v4)

In 2016 (as in many generations ago) D3 v4 superseded v3 with some great but breaking changes. Some conceptual changes including the zoom behaviour kept devs up at night (including myself). The changes are consistent and sensible, but are worth a few extra notes that might help you find sleep:

In 2016 (as in many generations ago) D3 v4 superseded v3 with some great but breaking changes. Some conceptual changes including the zoom behaviour kept devs up at night (including myself). The changes are consistent and sensible, but are worth a few extra notes that might help you find sleep:

  • As with v3, zoom in v4 is just about the x and y translation and the scale — the transform parameters. That is, of course, brutally simplifying complexity, but it’s a mantra you should try out when gridlocked.

    As with v3, zoom in v4 is just about the x and y translation and the scale — the transform parameters. That is, of course, brutally simplifying complexity, but it's a mantra you should try out when gridlocked.
  • The transform parameters are stored with the zoom base in v4, while they were stored with the behavior in v3. The behavior now just passes the transform on to the targets. This is good to know when we want to retrieve the transform outside of the zoom handler.

    The transform parameters are stored with the zoom base in v4, while they were stored with the behavior in v3. The behavior now just passes the transform on to the targets. This is good to know when we want to retrieve the transform outside of the zoom handler.
  • The v3 behaviour rescaled your scale automatically. In v4 you need to rescale your scale in the zoom function manually, and update all scale-based shapes and components. This is a little more work, but significantly less magic and a clearer separation of concerns.

    The v3 behaviour rescaled your scale automatically. In v4 you need to rescale your scale in the zoom function manually, and update all scale-based shapes and components. This is a little more work, but significantly less magic and a clearer separation of concerns.

Sources ^ ^ (Sources ^ ^)

There is no abundance of D3 (v4) zoom related posts and tutorials out there. The lack thereof was one reason to write this tutorial. However, there are a few zoom gems as well as some helpful further Canvas related material you can have a look at:

There is no abundance of D3 (v4) zoom related posts and tutorials out there. The lack thereof was one reason to write this tutorial. However, there are a few zoom gems as well as some helpful further Canvas related material you can have a look at:

Article additions:

Article additions:

Zoom tutorials:

Zoom tutorials:

Zoom tech:

Zoom tech:

Canvas:

Canvas:

I truly hope you enjoyed reading this. Please clap if you want to spread the word, follow me on Twitter and do say hello to either just say hello or tell me about other ways to zoom.

I truly hope you enjoyed reading this. Please clap if you want to spread the word, follow me on Twitter and do say hello to either just say hello or tell me about other ways to zoom.

Knowledge is partial and we’re all here to learn…

Knowledge is partial and we're all here to learn…

Originally published at www.datamake.io.

Originally published at www.datamake.io .

翻译自: https://www.freecodecamp.org/news/get-ready-to-zoom-and-pan-like-a-pro-after-reading-this-in-depth-tutorial-5d963b0a153e/

pixel 3 变焦

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值