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.

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:

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

The returned enter and update selections are then merged and returned by 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:

20-day simple moving average. The upper and lower bands, which have a standard deviation of 2.0 above and below the 20-day simple moving average, respectively.

This is the formula for calculating standard deviation:

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

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,

Calculate the average of the close price. Find the difference between the average value and close price for that data point. Square the result of each difference. Find the sum of squared differences. Calculate the mean of the squared differences to get the variance Get the square root of the variance to obtain the standard deviation for each data point. Multiply the standard deviation by 2. Calculate the upper and lower band values by adding or subtracting the average with the multiplied value.

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

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

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.

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.

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: