Cómo trabajar con el patrón de actualización general de D3.js

Una visita guiada sobre la implementación de módulos de visualización con conjuntos de datos dinámicos

Es común eliminar el elemento Scalable Vector Graphics (SVG) existente llamando d3.select('#chart').remove()antes de generar un nuevo gráfico.

Sin embargo, puede haber escenarios en los que tenga que producir visualizaciones dinámicas a partir de fuentes como API externas. Este artículo le mostrará cómo hacer esto usando D3.js.

D3.js maneja datos dinámicos adoptando el patrón de actualización general. Esto se describe comúnmente como una unión de datos, seguida de operaciones en las selecciones de entrada, actualización y salida. Dominar estos métodos de selección le permitirá producir transiciones fluidas entre estados, lo que le permitirá contar historias significativas con datos.

Empezando

Requisitos

Crearemos un gráfico que ilustra el movimiento de algunos fondos cotizados en bolsa (ETF) durante la segunda mitad de 2018. El gráfico consta de las siguientes herramientas:

  1. Gráfico de líneas de precios de cierre
  2. Gráfico de barras de volumen comercial
  3. Media móvil simple de 50 días
  4. Bandas de Bollinger (promedio móvil simple de 20 días, con desviación estándar establecida en 2.0)
  5. Gráfico abierto-alto-bajo-cerrado (OHLC)
  6. Candelabros

Estas herramientas se utilizan comúnmente en el análisis técnico de acciones, materias primas y otros valores. Por ejemplo, los operadores pueden hacer uso de las Bandas de Bollinger y las Candelabros para derivar patrones que representan señales de compra o venta.

Así es como se verá el gráfico:

Este artículo tiene como objetivo equiparlo con las teorías fundamentales de las uniones de datos y el patrón de entrada-actualización-salida para permitirle visualizar fácilmente conjuntos de datos dinámicos. Además, cubriremos selection.join, que se presenta en la versión v5.8.0 de D3.js.

El patrón de actualización general

La esencia del patrón de actualización general es la selección de elementos del Modelo de objetos de documento (DOM), seguida de la vinculación de datos a estos elementos. Estos elementos luego se crean, actualizan o eliminan para representar los datos necesarios.

Uniendo nuevos datos

La unión de datos es el mapeo del nnúmero de elementos en el conjunto de datos con el nnúmero de nodos del Modelo de objeto de documento (DOM) seleccionados, especificando la acción requerida para el DOM a medida que cambian los datos.

Usamos el data()método para mapear cada punto de datos a un elemento correspondiente en la selección DOM. Además, es una buena práctica mantener la constancia del objeto especificando una clave como identificador único en cada punto de datos. Echemos un vistazo al siguiente ejemplo, que es el primer paso hacia la representación de las barras de volumen de operaciones:

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

La línea de código anterior selecciona todos los elementos con la clase vol, seguida de mapeo de la this.currentDatamatriz con la selección de elementos DOM usando el data()método.

El segundo argumento opcional de data()toma un punto de datos como entrada y devuelve la datepropiedad como la clave seleccionada para cada punto de datos.

Entrar / Actualizar selección

.enter()devuelve una selección de entrada que representa los elementos que deben agregarse cuando la matriz unida es más larga que la selección. A esto le sigue una llamada .append(), que crea o actualiza elementos en el DOM. Podemos implementar esto de la siguiente manera:

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()fusiona las selecciones de actualización e introducción, antes de aplicar las cadenas de métodos subsiguientes para crear animaciones entre transiciones y actualizar sus atributos asociados. El bloque de código anterior le permite realizar las siguientes acciones en los elementos DOM seleccionados:

  1. La selección de actualización, que consta de puntos de datos representados por los elementos en el gráfico, tendrá sus atributos actualizados en consecuencia.
  2. La creación de elementos con la clase vol, con los atributos anteriores definidos dentro de cada elemento, ya que la selección de entrada consta de puntos de datos que no están representados en el gráfico.

Salir de la selección

Elimine elementos de nuestro conjunto de datos siguiendo los sencillos pasos a continuación: bars.exit (). Remove ();

.exit()devuelve una selección de salida, que especifica los puntos de datos que deben eliminarse. El .remove()método elimina posteriormente la selección de la DOM.

Así es como las barras de la serie de volumen responderán a los cambios en los datos:

Tome nota de cómo se actualizan el DOM y los atributos respectivos de cada elemento cuando seleccionamos un conjunto de datos diferente:

Selection.join (a partir de v5.8.0)

La introducción selection.joinen v5.8.0 de D3.js ha simplificado todo el proceso de unión de datos. Funciones separadas se pasan ahora a manejar entrar , actualización , y la salida que a su vez devuelve el fusionaron entrar y selecciones de actualización.

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

En el caso de las barras de series de volumen, la aplicación de selection.joinresultará en los siguientes cambios en nuestro código:

//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'])) );

Además, tenga en cuenta que hemos realizado algunos cambios en la animación de las barras. En lugar de pasar el transition()método a las selecciones fusionadas de ingreso y actualización, ahora se usa en la selección de actualización de manera que las transiciones solo se aplicarán cuando el conjunto de datos haya cambiado.

Las selecciones de entrada y actualización devueltas se fusionan y devuelven por selection.join.

Bandas de Bollinger

Del mismo modo, podemos aplicar selection.joinen la representación de Bandas de Bollinger. Antes de renderizar las Bandas, debemos calcular las siguientes propiedades de cada punto de datos:

  1. Media móvil simple de 20 días.
  2. Las bandas superior e inferior, que tienen una desviación estándar de 2.0 por encima y por debajo del promedio móvil simple de 20 días, respectivamente.

Esta es la fórmula para calcular la desviación estándar:

Ahora, traduciremos la fórmula anterior a código 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);

Una explicación rápida del cálculo de la desviación estándar y los valores de la banda de Bollinger en el bloque de código anterior es la siguiente:

Para cada iteración,

  1. Calcula el promedio del precio de cierre.
  2. Encuentre la diferencia entre el valor promedio y el precio de cierre para ese punto de datos.
  3. Cuadre el resultado de cada diferencia.
  4. Calcula la suma de las diferencias al cuadrado.
  5. Calcule la media de las diferencias al cuadrado para obtener la varianza
  6. Obtenga la raíz cuadrada de la varianza para obtener la desviación estándar para cada punto de datos.
  7. Multiplique la desviación estándar por 2. Calcule los valores de la banda superior e inferior sumando o restando el promedio con el valor multiplicado.

Con los puntos de datos definidos, podemos utilizarlos selection.joinpara renderizar las Bandas de Bollinger:

// 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) );

Esto representa el gráfico de áreas que denota el área ocupada por las Bandas de Bollinger. En la función de actualización, podemos usar el selection.transition()método para proporcionar transiciones animadas en la selección de actualización.

Candelabros

El gráfico de velas muestra los precios máximos, mínimos, de apertura y de cierre de una acción durante un período específico. Cada vela representa un punto de datos. El verde representa cuando la acción cierra al alza, mientras que el rojo representa cuando la acción cierra a un valor más bajo.

A diferencia de las Bandas de Bollinger, no es necesario realizar cálculos adicionales, ya que los precios están disponibles en el conjunto de datos existente.

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']); 

En la función de entrada, cada vela se representa en función de sus propiedades individuales.

En primer lugar, a cada elemento del grupo de velas se le asigna una clase de up-daysi el precio de cierre es más alto que el precio de apertura, y down-daysi el precio de cierre es menor o igual que el precio de apertura.

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']) } ]); });

A continuación, agregamos el pathelemento, que representa el precio más alto y más bajo de ese día, a la selección anterior.

 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.

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

.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.

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.

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.

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.

Special thanks to Debbie Leong for reviewing this article.

Additional references:

  1. D3.js API documentation
  2. Interactive demonstration of selection.join