Как да работите с общия модел на актуализация на D3.js.

Обиколка с екскурзовод за внедряване на модули за визуализация с динамични набори от данни

Обичайно е да се премахне съществуващия елемент Scalable Vector Graphics (SVG) чрез извикване d3.select('#chart').remove(), преди да се изобрази нова диаграма.

Възможно е обаче да има сценарии, когато трябва да създадете динамични визуализации от източници като външни API. Тази статия ще ви покаже как да направите това с помощта на D3.js.

D3.js обработва динамични данни, като приема общия модел на актуализация. Това обикновено се описва като присъединяване на данни, последвано от операции за избор на въвеждане, актуализиране и излизане. Овладяването на тези методи за подбор ще ви позволи да създадете безпроблемни преходи между състоянията, което ще ви позволи да разказвате смислени истории с данни.

Приготвяме се да започнем

Изисквания

Ще изградим графика, която илюстрира движението на няколко борсово търгувани фонда (ETF) през втората половина на 2018 г. Графиката се състои от следните инструменти:

  1. Затваряща графика на ценовата линия
  2. Стълбовидна диаграма на обема на търговията
  3. 50-дневна проста плъзгаща се средна
  4. Ленти на Болинджър (20-дневна проста пълзяща средна, със стандартно отклонение, зададено на 2.0)
  5. Диаграма отворено-високо-ниско-затворено (OHLC)
  6. Свещници

Тези инструменти обикновено се използват при техническия анализ на акции, стоки и други ценни книжа. Например, търговците могат да използват лентите и свещниците на Болинджър, за да извлекат модели, които представляват сигнали за покупка или продажба.

Ето как ще изглежда графиката:

Тази статия има за цел да ви предостави основните теории за обединяването на данни и модела за въвеждане-актуализиране-излизане, за да ви позволи лесно да визуализирате динамични набори от данни. Освен това ще разгледаме selection.join, който е представен в версията на D3.js v5.8.0.

Общият модел на актуализация

Същността на общия модел на актуализация е изборът на елементи на обект на модел на документ (DOM), последван от обвързване на данни към тези елементи. След това тези елементи се създават, актуализират или премахват, за да представят необходимите данни.

Присъединяване към нови данни

Присъединяването на данни е картографиране на nброя на елементите в набора от данни с nброя на избраните възли на обект на модел на документ (DOM), указващо необходимото действие към DOM при промяна на данните.

Използваме data()метода за картографиране на всяка точка от данни към съответния елемент в DOM селекцията. Освен това е добра практика да се поддържа постоянство на обекта, като се посочва ключ като уникален идентификатор във всяка точка от данни. Нека да разгледаме следния пример, който е първата стъпка към изобразяването на лентите за обема на търговията:

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

Горният ред на кода избира всички елементи с класа vol, последван от картографиране на this.currentDataмасива с избора на DOM елементи с помощта на data()метода.

Вторият незадължителен аргумент на data()приема точка от данни като вход и връща dateсвойството като избрания ключ за всяка точка от данни.

Въведете / актуализирайте избора

.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()обединява актуализацията и въвежда селекции, преди да приложи следващите вериги от методи за създаване на анимации между преходи и за актуализиране на свързаните с тях атрибути. Горният блок код ви позволява да извършвате следните действия върху избраните DOM елементи:

  1. Изборът на актуализация, който се състои от точки от данни, представени от елементите на графиката, ще има съответно актуализирани атрибути.
  2. Създаването на елементи с класа vol, с горните атрибути, дефинирани във всеки елемент, тъй като изборът за въвеждане се състои от точки от данни, които не са представени на графиката.

Излезте от избора

Премахнете елементи от набора ни от данни, като изпълните следните прости стъпки: bars.exit (). Remove ();

.exit()връща селекция за изход, която определя точките с данни, които трябва да бъдат премахнати. Впоследствие .remove()методът изтрива селекцията от DOM.

Ето как лентите на томовите серии ще реагират на промените в данните:

Обърнете внимание как се актуализират DOM и съответните атрибути на всеки елемент, докато избираме различен набор от данни:

Selection.join (от v5.8.0)

Въвеждането на selection.joinv5.8.0 на D3.js опрости целия процес на присъединяване на данни. Отделни функции вече са преминали за да се справят влиза , актуализация , и изход, който на свой ред се връща в резултат на сливането влиза и актуализиране на селекции.

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

В случай на лентите за томовите серии, прилагането на 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'])) );

Също така имайте предвид, че направихме някои промени в анимацията на лентите. Вместо да предава transition()метода на обединените селекции за въвеждане и актуализиране, той вече се използва в селекцията за актуализация, така че преходите ще се прилагат само когато набора от данни се промени.

След това върнатите селекции за въвеждане и актуализиране се обединяват и връщат от selection.join.

Ленти Болинджър

По същия начин можем да кандидатстваме selection.joinза изобразяване на ленти на Болинджър. Преди да изобразим лентите, ние трябва да изчислим следните свойства на всяка точка от данни:

  1. 20-дневна проста плъзгаща се средна.
  2. Горната и долната ленти, които имат стандартно отклонение съответно 2,0 над и под 20-дневната проста плъзгаща се средна.

Това е формулата за изчисляване на стандартното отклонение:

Сега ще преведем горната формула в 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);

Бързо обяснение на изчислението на стандартното отклонение и стойностите на Bollinger Band в горния блок код е както следва:

За всяка итерация,

  1. Изчислете средната стойност на цената на затваряне.
  2. Намерете разликата между средната стойност и цената на затваряне за тази точка от данни.
  3. Квадратирайте резултата от всяка разлика.
  4. Намерете сумата на квадратите разлики.
  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.

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:

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