Hogyan kell működni a D3.js általános frissítési mintájával

Vezetett túra a vizualizációs modulok dinamikus adatkészletekkel történő megvalósításáról

Gyakran eltávolítják a meglévő Scalable Vector Graphics (SVG) elemet hívással d3.select('#chart').remove(), mielőtt új diagramot hoznának létre.

Előfordulhatnak azonban olyan esetek, amikor dinamikus vizualizációkat kell készítenie olyan forrásokból, mint a külső API-k. Ez a cikk megmutatja, hogyan lehet ezt megtenni a D3.js használatával.

A D3.js az általános frissítési minta elfogadásával kezeli a dinamikus adatokat. Ezt általában adatcsatlakozásnak írják le, amelyet az Enter, a Frissítés és a Kilépés kiválasztás művelete követ. Ezeknek a kiválasztási módszereknek az elsajátítása lehetővé teszi az állapotok közötti zökkenőmentes átmeneteket, lehetővé téve az adatokkal való értelmes történetek elmondását.

Elkezdeni

Követelmények

Olyan grafikont készítünk, amely néhány tőzsdén kereskedett alap (ETF) mozgását szemlélteti 2018 második felében. A grafikon a következő eszközökből áll:

  1. Záró árdiagram
  2. Kereskedelmi volumen oszlopdiagram
  3. 50 napos egyszerű mozgóátlag
  4. Bollinger szalagok (20 napos egyszerű mozgóátlag, szórással 2,0-re állítva)
  5. Nyílt-magas-alacsony-záró (OHLC) diagram
  6. Gyertyatartók

Ezeket az eszközöket általában a részvények, áruk és más értékpapírok technikai elemzésében használják. Például a kereskedők a Bollinger szalagokat és a gyertyatartókat felhasználhatják a vételi vagy eladási jeleket ábrázoló minták levezetésére.

A grafikon így fog kinézni:

Ennek a cikknek a célja az adatok összekapcsolásának alapvető elméleteivel és az enter-update-exit mintával annak érdekében, hogy könnyedén megjeleníthesse a dinamikus adathalmazokat. Ezenkívül kitérünk a selection.join elemre is, amelyet a D3.js v5.8.0 kiadása mutat be.

Az általános frissítési minta

Az általános frissítési minta lényege a Document Object Model (DOM) elemek kiválasztása, majd az adatok ezekhez az elemekhez való kötése. Ezeket az elemeket ezután létrehozzák, frissítik vagy eltávolítják, hogy képviseljék a szükséges adatokat.

Új adatok összekapcsolása

Az adatcsatlakozás naz adatkészletben lévő elemek számának leképezése a nkiválasztott dokumentumobjektum-modell (DOM) csomópontok számával, megadva a szükséges műveletet a DOM számára az adatok változásakor.

A data()módszert arra használjuk, hogy az egyes adatpontokat a DOM kiválasztás megfelelő eleméhez hozzárendeljük. Ezenkívül jó gyakorlat fenntartani az objektum állandóságát úgy, hogy az egyes adatpontokban kulcsot határoz meg egyedi azonosítóként. Vessünk egy pillantást a következő példára, amely az első lépés a kereskedelmi volumen sávok renderelése felé:

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

A fenti kódsor kiválasztja az osztály összes elemét vol, majd a this.currentDatatömb leképezése a DOM elemek kiválasztásával a data()módszer segítségével.

A második opcionális argumentum: data()egy adatpontot vesz bemenetként, és a datetulajdonságot adja vissza az egyes adatpontok kiválasztott kulcsaként.

Enter / Update selection

.enter()egy enter választást ad vissza, amely azokat az elemeket képviseli, amelyeket hozzá kell adni, ha az összekapcsolt tömb hosszabb, mint a kijelölés. Ezt követi a hívás .append(), amely elemeket hoz létre vagy frissít a DOM-on. Ezt a következő módon tudjuk megvalósítani:

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()egyesíti a frissítést és az enter kiválasztásokat, mielőtt a következő metódusláncokat alkalmazná, hogy animációkat hozzon létre az átmenetek között, és frissítse a hozzájuk tartozó attribútumokat. A fenti kódblokk lehetővé teszi a következő műveletek végrehajtását a kiválasztott DOM elemeken:

  1. A grafikon elemei által ábrázolt adatpontokból álló frissítési választás attribútumait ennek megfelelően frissítik.
  2. Elemek létrehozása az osztállyal úgy vol, hogy a fenti attribútumok az egyes elemeken belül definiálódnak, mivel az enter kiválasztás olyan adatpontokból áll, amelyeket a grafikon nem képvisel.

Kilépés a kijelölésből

Távolítsa el az elemeket az adatkészletből az alábbi egyszerű lépések végrehajtásával: bars.exit (). Remove ();

.exit()egy kilépési választást ad vissza, amely meghatározza az eltávolítandó adatpontokat. A .remove()módszer ezt követően törli a kiválasztást a DOM-ból.

A kötetsorok oszlopai így reagálnak az adatok változásaira:

Vegye figyelembe, hogy a DOM és az egyes elemek megfelelő attribútumai hogyan frissülnek, amikor másik adatkészletet választunk:

Selection.join (v5.8.0-tól)

A selection.joinD3.js v5.8.0 verziójának bevezetése egyszerűsítette a teljes adatcsatlakozási folyamatot. Külön funkciók kerülnek át kezelni be , frissítés , és kilép, ami viszont visszatér a összeolvadt bevitelét és frissítését választás.

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

A kötetsorok esetében a (z) alkalmazása selection.joina következő változásokat eredményezi kódunkban:

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

Ne feledje, hogy a bárok animációján is változtattunk. Ahelyett, hogy a transition()metódust átadná az egyesített enter és update kiválasztásoknak, most már azt használja a frissítés kiválasztásában, hogy az átmeneteket csak akkor alkalmazzák, amikor az adatkészlet megváltozott.

A visszaküldött enter és update kijelöléseket ezután egyesíti és visszaadja selection.join.

Bollinger zenekarok

Hasonlóképpen alkalmazhatjuk selection.joina Bollinger zenekarok renderelésére is. A Sávok renderelése előtt ki kell számolnunk az egyes adatpontok következő tulajdonságait:

  1. 20 napos egyszerű mozgóátlag.
  2. A felső és az alsó sáv, amelynek szórása 2,0, a 20 napos egyszerű mozgóátlag felett és alatt.

Ez a képlet a szórás kiszámításához:

Most a fenti képletet lefordítjuk JavaScript kódba:

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 standard eltérés és a Bollinger Band értékek kiszámításának gyors magyarázata a fenti kódblokkon a következő:

Minden iterációnál

  1. Számítsa ki a közeli ár átlagát.
  2. Keresse meg a különbséget az adott adat átlagos értéke és a közeli ár között.
  3. Szögezd be az egyes különbségek eredményét.
  4. Keresse meg a négyzetbeli különbségek összegét.
  5. Számítsa ki a négyzetbeli különbségek átlagát a variancia megszerzéséhez
  6. Szerezze meg a variancia négyzetgyökét, hogy megkapja az egyes adatpontok szórását.
  7. Szorozza meg a szórást 2-vel. Számítsa ki a felső és az alsó sáv értékeit úgy, hogy összeadja vagy kivonja az átlagot a szorzott értékkel.

A megadott adatpontokkal ezután felhasználhatjuk a selection.joinBollinger szalagok renderelését:

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

Ez megjeleníti a területdiagramot, amely a Bollinger szalagok által kitöltött területet jelöli. A frissítés funkción selection.transition()animált átmenetek biztosítására használhatjuk a frissítés kiválasztását.

Gyertyatartók

A gyertyatartók táblázata egy adott időszak magas, alacsony, nyitott és záró árfolyamát mutatja. Minden gyertyatartó adatpontot képvisel. A zöld azt jelöli, amikor a részvény magasabbra zár, míg a piros azt jelenti, ha a részvény alacsonyabb értéken zár.

A Bollinger szalagokkal ellentétben nincs szükség további számításokra, mivel az árak a meglévő adatkészletben érhetők el.

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

Az enter függvényben minden gyertyatartó egyedi tulajdonságai alapján jelenik meg.

Először is, minden gyertyatartó csoport elemhez hozzárendelnek egy osztályt, up-dayha a záróár magasabb, mint a nyitott ár, és down-dayha a záróár alacsonyabb vagy egyenlő, mint a nyitott ár.

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

Ezután patha fenti választáshoz hozzáfűzzük az adott nap legmagasabb és legalacsonyabb árát képviselő elemet.

 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