d3.js 数据更新_如何使用D3.js的常规更新模式

颜哲彦
2023-12-01

d3.js 数据更新

关于使用动态数据集实现可视化模块的导览 (A guided tour on implementing visualization modules with dynamic datasets)

It is common to remove the existing Scalable Vector Graphics (SVG) element by calling d3.select('#chart').remove(), before rendering a new chart.

通常在渲染新图表之前,通过调用d3.select('#chart').remove()来删除现有的可缩放矢量图形(SVG)元素。

However, there may be scenarios when you have to produce dynamic visualizations from sources such as external APIs. This article will show you how to do this using D3.js.

但是,在某些情况下,您必须从外部API等来源生成动态可视化效果。 本文将向您展示如何使用D3.js。

D3.js handles dynamic data by adopting the general update pattern. This is commonly described as a data-join, followed by operations on the enter, update and exit selections. Mastering these selection methods will enable you to produce seamless transitions between states, allowing you to tell meaningful stories with data.

D3.js通过采用常规更新模式来处理动态数据。 通常将其描述为数据联接,然后是输入,更新和退出选择的操作。 掌握这些选择方法将使您能够在状态之间进行无缝转换,从而使您可以用数据讲述有意义的故事。

入门 (Getting Started)

要求 (Requirements)

We will be building a graph that illustrates the movement of a few Exchange-Traded Funds (ETFs) over the second half of 2018. The graph consists of the following tools:

我们将建立一个图表,说明一些交易所买卖基金(ETF)在2018年下半年的走势。该图表包含以下工具:

  1. Closing price line chart

    收盘价折线图

  2. Trade volume bar chart

    交易量条形图

  3. 50-day simple moving average

    50天简单移动平均线

  4. Bollinger Bands (20-day simple moving average, with standard deviation set at 2.0)

    布林带 (20天简单移动平均线,标准偏差设置为2.0)

  5. Open-high-low-close (OHLC) chart

    开-高-低-关闭( OHLC )图表

  6. Candlesticks

    烛台

These tools are commonly utilized in the technical analysis of stocks, commodities, and other securities. For example, traders may make use of the Bollinger Bands and Candlesticks to derive patterns which represent buy or sell signals.

这些工具通常用于股票,商品和其他证券的技术分析。 例如,交易者可以利用布林线和烛台来得出表示买入或卖出信号的形态。

This is how the graph will look like:

该图将如下所示:

This article aims to equip you with the fundamental theories of data joins and the enter-update-exit pattern in order to allow you to easily visualize dynamic datasets. In addition, we will be covering selection.join, which is introduced in D3.js’s v5.8.0 release.

本文旨在为您提供数据连接的基本理论和enter-update-exit模式,以便使您轻松可视化动态数据集。 此外,我们将覆盖selection.join ,这是在D3.js的V5.8.0版本中引入的。

常规更新模式 (The general update pattern)

The gist of the general update pattern is the selection of Document Object Model (DOM) elements, followed by binding of data to these elements. These elements are then created, updated or removed, to represent the necessary data.

常规更新模式的要点是选择文档对象模型(DOM)元素,然后将数据绑定到这些元素。 然后创建,更新或删除这些元素,以表示必要的数据。

联接新数据 (Joining new data)

Data join is the mapping of n number of elements in the dataset with nnumber of selected Document Object Model (DOM) nodes, specifying the required action to the DOM as the data changes.

数据联接是数据集中n个元素与n个选定文档对象模型(DOM)节点的映射,从而在数据更改时为DOM指定所需的操作。

We use the data() method to map each data point to a corresponding element in the DOM selection. In addition, it is good practice to maintain object constancy by specifying a key as the unique identifier in each data point. Let’s take a look at the following example, which is the first step towards rendering the trade volume bars:

我们使用data()方法将每个数据点映射到DOM选择中的相应元素。 此外,优良作法是通过在每个数据点中将键指定为唯一标识符来保持对象的一致性 。 让我们看下面的示例,这是渲染交易量条的第一步:

const bars = d3
  .select('#volume-series')
  .selectAll(.'vol')
  .data(this.currentData, d => d['date']);

The above line of code selects all elements with the class vol , followed by mapping the this.currentData array with the selection of DOM elements using the data() method.

上面的代码行选择了class类为vol所有元素,然后使用data()方法将this.currentData数组与DOM元素的选择进行映射。

The second optional argument of data() takes a data point as input and returns the date property as the selected key for each data point.

data()的第二个可选参数以一个数据点作为输入,并返回date属性作为每个数据点的选定键。

输入/更新选择 (Enter/Update selection)

.enter() returns an enter selection which represents the elements that need to be added when the joined array is longer than the selection. This is followed by calling .append(), which creates or updates elements on the DOM. We can implement this in the following manner:

.enter()返回一个enter选择,表示连接的数组比选择长时需要添加的元素。 接下来是调用.append() ,它会在DOM上创建或更新元素。 我们可以通过以下方式实现此目的:

bars
  .enter()
  .append('rect')
  .attr('class', 'vol')
  .merge(bars)
  .transition()
  .duration(750)
  .attr('x', d => this.xScale(d['date']))
  .attr('y', d => yVolumeScale(d['volume']))
  .attr('fill', (d, i) => {
    if (i === 0) {
      return '#03a678';
    } else {
      // green bar if price is rising during that period, and red when price is falling
      return this.currentData[i - 1].close > d.close
        ? '#c0392b'
        : '#03a678';
    }
  })
  .attr('width', 1)
  .attr('height', d => this.height - yVolumeScale(d['volume']));

.merge() merges the update and enter selections, before applying the subsequent method chains to create animations between transitions, and to update their associated attributes. The above block of code enables you to perform the following actions on the selected DOM elements:

.merge()合并更新并输入选择,然后应用后续方法链在过渡之间创建动画并更新其相关属性。 上面的代码块使您可以对选定的DOM元素执行以下操作:

  1. The update selection, which consists of data points represented by the <rect> elements on the graph, will have their attributes updated accordingly.

    更新选择(由图形上<rect>元素表示的数据点组成)将相应地更新其属性。

  2. The creation of <rect> elements with the class vol, with the above attributes defined within each element as the enter selection consists of data points that are not represented on the graph.

    创建具有类vol<rect>元素,并在每个元素内定义上述属性作为enter选择,其中包含图上未表示的数据点。

退出选择 (Exit selection)

Remove items from our dataset by following the simple steps below:bars.exit().remove();

请按照以下简单步骤从我们的数据集中删除项目:bars.exit()。remove();

.exit() returns an exit selection, which specifies the data points that need to be removed. The .remove() method subsequently deletes the selection from the DOM.

.exit()返回退出选择,该选择指定需要删除的数据点。 .remove()方法随后从DOM中删除选择。

This is how the volume series bars will respond to changes in data:

这是音量序列条将如何响应数据更改的方式:

Take note of how the DOM and the respective attributes of each <rect>element are updated as we select a different dataset:

请注意在选择不同的数据集时,如何更新DOM和每个<rect>元素的相应属性:

Selection.join(从v5.8.0开始) (Selection.join (as of v5.8.0))

The introduction of selection.join in v5.8.0 of D3.js has simplified the entire data join process. Separate functions are now passed to handle enter, update, and exit which in turn returns the merged enter and update selections.

D3.js v5.8.0中的selection.join引入简化了整个数据联接过程。 独立的功能现在通过处理输入更新并退出这反过来又返回合并后的输入和更新的选择。

selection.join(
    enter => // enter.. ,
    update => // update.. ,
    exit => // exit.. 
  )
  // allows chained operations on the returned selections

In the case of the volume series bars, the application of selection.join will result in the following changes on our code:

对于音量系列条形图, selection.join的应用将导致我们的代码发生以下更改:

//select, followed by updating data join
const bars = d3
  .select('#volume-series')
  .selectAll('.vol')
  .data(this.currentData, d => d['date']);
bars.join(
  enter =>
    enter
      .append('rect')
      .attr('class', 'vol')
      .attr('x', d => this.xScale(d['date']))
      .attr('y', d => yVolumeScale(d['volume']))
      .attr('fill', (d, i) => {
        if (i === 0) {
          return '#03a678';
        } else {
          return this.currentData[i - 1].close > d.close
            ? '#c0392b'
            : '#03a678';
        }
      })
      .attr('width', 1)
      .attr('height', d => this.height - yVolumeScale(d['volume'])),
  update =>
    update
      .transition()
      .duration(750)
      .attr('x', d => this.xScale(d['date']))
      .attr('y', d => yVolumeScale(d['volume']))
      .attr('fill', (d, i) => {
        if (i === 0) {
          return '#03a678';
        } else {
          return this.currentData[i - 1].close > d.close
            ? '#c0392b'
            : '#03a678';
        }
      })
      .attr('width', 1)
      .attr('height', d => this.height - yVolumeScale(d['volume']))
);

Also, note that we have made some changes to the animation of the bars. Instead of passing the transition() method to the merged enter and update selections, it is now used in the update selection such that transitions will only be applied when the dataset has changed.

另外,请注意,我们对条形的动画进行了一些更改。 现在,它不再将transition()方法传递给合并的enter和update选择,而是在update选择中使用它,以便仅在数据集发生更改时才应用transition。

The returned enter and update selections are then merged and returned by selection.join.

然后将返回的enter和update选择合并并通过selection.join返回。

布林乐队 (Bollinger Bands)

Similarly, we can apply selection.join on the rendering of Bollinger Bands. Before rendering the Bands, we are required to calculate the following properties of each data point:

同样,我们可以在布林带的渲染中应用selection.join 。 在渲染波段之前,我们需要计算每个数据点的以下属性:

  1. 20-day simple moving average.

    20天的简单移动平均线。
  2. The upper and lower bands, which have a standard deviation of 2.0 above and below the 20-day simple moving average, respectively.

    上限和下限分别在20天简单移动平均线之上和之下的标准偏差为2.0。

This is the formula for calculating standard deviation:

这是计算标准偏差的公式:

Now, we shall translate the above formula into JavaScript code:

现在,我们将上面的公式转换为JavaScript代码:

calculateBollingerBands(data, numberOfPricePoints) {
  let sumSquaredDifference = 0;
  return data.map((row, index, total) => {
    const start = Math.max(0, index - numberOfPricePoints);
    const end = index; 
    
    // divide the sum with subset.length to obtain moving average
    const subset = total.slice(start, end + 1);
    const sum = subset.reduce((a, b) => {
      return a + b['close'];
    }, 0);
    const sumSquaredDifference = subset.reduce((a, b) => {
      const average = sum / subset.length;
      const dfferenceFromMean = b['close'] - average;
      const squaredDifferenceFromMean = Math.pow(dfferenceFromMean, 2);
      return a + squaredDifferenceFromMean;
    }, 0);
    const variance = sumSquaredDifference / subset.length;
  return {
      date: row['date'],
      average: sum / subset.length,
      standardDeviation: Math.sqrt(variance),
      upperBand: sum / subset.length + Math.sqrt(variance) * 2,
      lowerBand: sum / subset.length - Math.sqrt(variance) * 2
    };
  });
}
.
.
// calculates simple moving average, and standard deviation over 20 days
this.bollingerBandsData = this.calculateBollingerBands(validData, 19);

A quick explanation of the calculation of the standard deviation, and Bollinger Band values on the above block of code is as follows:

在上述代码块中,对标准偏差和布林带值的计算的简要说明如下:

For each iteration,

对于每次迭代,

  1. Calculate the average of the close price.

    计算收盘价的平均值。
  2. Find the difference between the average value and close price for that data point.

    找到该数据点的平均值与收盘价之间的差。
  3. Square the result of each difference.

    平方每个差异的结果。
  4. Find the sum of squared differences.

    找到平方差的总和。
  5. Calculate the mean of the squared differences to get the variance

    计算平方差的均值以获得方差
  6. Get the square root of the variance to obtain the standard deviation for each data point.

    获取方差的平方根,以获取每个数据点的标准偏差。
  7. Multiply the standard deviation by 2. Calculate the upper and lower band values by adding or subtracting the average with the multiplied value.

    将标准偏差乘以2。通过将平均值乘以或减去乘积来计算上限和下限。

With the data points defined, we can then make use of selection.join to render Bollinger Bands:

定义好数据点后,我们便可以使用selection.join渲染布林带:

// code not shown: rendering of upper and lower bands 
.
.
// bollinger bands area chart
const area = d3
  .area()
  .x(d => this.xScale(d['date']))
  .y0(d => this.yScale(d['upperBand']))
  .y1(d => this.yScale(d['lowerBand']));
const areaSelect = d3
  .select('#chart')
  .select('svg')
  .select('g')
  .selectAll('.band-area')
  .data([this.bollingerBandsData]);
areaSelect.join(
  enter =>
    enter
      .append('path')
      .style('fill', 'darkgrey')
      .style('opacity', 0.2)
      .style('pointer-events', 'none')
      .attr('class', 'band-area')
      .attr('clip-path', 'url(#clip)')
      .attr('d', area),
  update =>
    update
      .transition()
      .duration(750)
      .attr('d', area)
);

This renders the area chart which denotes the area filled by the Bollinger Bands. On the update function, we can use the selection.transition()method to provide animated transitions on the update selection.

这将显示面积图,该面积图表示由布林带所填充的区域。 在更新功能上,我们可以使用selection.transition()方法在更新选择上提供动画过渡。

烛台 (Candlesticks)

The candlesticks chart displays the high, low, open and close prices of a stock for a specific period. Each candlestick represents a data point. Green represents when the stock closes higher while red represents when the stock closes at a lower value.

烛台图显示特定时期内股票的高,低,开盘和收盘价。 每个烛台代表一个数据点。 绿色表示股票收盘价较高时,红色表示股票收盘价较低时。

Unlike the Bollinger Bands, there is no need for additional calculations, as the prices are available in the existing dataset.

与布林带不同,由于价格可在现有数据集中获得,因此无需进行其他计算。

const bodyWidth = 5;
const candlesticksLine = d3
  .line()
  .x(d => d['x'])
  .y(d => d['y']);
const candlesticksSelection = d3
  .select('#chart')
  .select('g')
  .selectAll('.candlesticks')
  .data(this.currentData, d => d['volume']);
candlesticksSelection.join(enter => {
  const candlesticksEnter = enter
    .append('g')
    .attr('class', 'candlesticks')
    .append('g')
    .attr('class', 'bars')
    .classed('up-day', d => d['close'] > d['open'])
    .classed('down-day', d => d['close'] <= d['open']);

On the enter function, each candlestick is rendered based on its individual properties.

在输入功能上,每个烛台根据其各自的属性进行渲染。

First and foremost, each candlestick group element is assigned a class of up-day if the close price is higher than the open price, and down-day if the close price is lower than or equal to the open-price.

首先,如果收盘价高于开盘价,则为每个烛台组元素分配一个up-day类别,如果收盘价低于或等于开盘价,则为down-day分配一个类别。

candlesticksEnter
    .append('path')
    .classed('high-low', true)
    .attr('d', d => {
      return candlesticksLine([
        { x: this.xScale(d['date']), y: this.yScale(d['high']) },
        { x: this.xScale(d['date']), y: this.yScale(d['low']) }
      ]);
    });

Next, we append the path element, which represents the highest and lowest price of that day, to the above selection.

接下来,我们将path元素(代表当天的最高和最低价格)附加到上述选择中。

candlesticksEnter
    .append('rect')
    .attr('x', d => this.xScale(d.date) - bodyWidth / 2)
    .attr('y', d => {
      return d['close'] > d['open']
        ? this.yScale(d.close)
        : this.yScale(d.open);
    })
    .attr('width', bodyWidth)
    .attr('height', d => {
      return d['close'] > d['open']
        ? this.yScale(d.open) - this.yScale(d.close)
        : this.yScale(d.close) - this.yScale(d.open);
    });
});

This is followed by appending the rect element to the selection. The height of each rect element is directly proportionate to its day range, derived by subtracting the open price with the close price.

接下来,将rect元素附加到所选内容。 每个rect元素的高度均与其日范围成正比,该日范围是通过用开盘价减去开盘价得出的。

On our stylesheets, we will define the following CSS properties to our classes making the candlesticks red or green:

在样式表上,我们将为类定义以下CSS属性,使烛台变为红色或绿色:

.bars.up-day path {
 stroke: #03a678;
}
.bars.down-day path {
 stroke: #c0392b;
}
.bars.up-day rect {
 fill: #03a678;
}
.bars.down-day rect {
 fill: #c0392b;
}

This results in the rendering of the Bollinger Bands and candlesticks:

这导致布林带和烛台的渲染:

The new syntax has proven to be simpler and more intuitive than explicitly calling selection.enter, selection.append, selection.merge, and selection.remove.

与显式调用selection.enterselection.appendselection.mergeselection.remove相比,新语法已被证明更简单,更直观。

Note that for those who are developing with D3.js’s v5.8.0 and beyond, it has been recommended by Mike Bostock that these users start using selection.join due to the above advantages.

请注意,对于那些使用D3.js v5.8.0及更高版本进行开发的用户,Mike Bostock 建议这些用户由于上述优点而开始使用selection.join

结论 (Conclusion)

The potential of D3.js is limitless and the above illustrations are merely the tip of the iceberg. Many satisfied users have created visualizations which are vastly more complex and sophisticated than the one show above. This list of free APIs may interest you if you are keen to embark on your own data visualization projects.

D3.js的潜力是无限的,上面的插图只是冰山一角。 许多满意的用户创建了可视化文件,这些可视化文件比上面显示的可视化文件复杂得多。 如果您热衷于自己的数据可视化项目,则此免费API列表可能会让您感兴趣。

Feel free to check out the source code and the full demonstration of this project.

随时检查源代码和该项目的完整演示

Thank you very much for reading this article. If you have any questions or suggestions, feel free to leave them on the comments below!

非常感谢您阅读本文。 如果您有任何疑问或建议,请随时将它们留在下面的评论中!

New to D3.js? You may refer to this article on the basics of implementing common chart components.

D3.js的新手? 您可以参考 本文 以了解实现常见图表组件的基础。

Special thanks to Debbie Leong for reviewing this article.

特别感谢Debbie Leong审阅了本文。

Additional references:

其他参考:

  1. D3.js API documentation

    D3.js API文档

  2. Interactive demonstration of selection.join

    selection.join的交互式演示

翻译自: https://www.freecodecamp.org/news/how-to-work-with-d3-jss-general-update-pattern-8adce8d55418/

d3.js 数据更新

 类似资料: