Sådan arbejder du med D3.js's generelle opdateringsmønster

En rundvisning om implementering af visualiseringsmoduler med dynamiske datasæt

Det er almindeligt at fjerne det eksisterende SVG-element (Scalable Vector Graphics) ved at ringe d3.select('#chart').remove(), før der gengives et nyt diagram.

Der kan dog være scenarier, når du skal producere dynamiske visualiseringer fra kilder såsom eksterne API'er. Denne artikel viser dig, hvordan du gør dette ved hjælp af D3.js.

D3.js håndterer dynamiske data ved at vedtage det generelle opdateringsmønster. Dette beskrives almindeligvis som en dataforbindelse efterfulgt af operationer ved valg af enter, opdatering og afslutning. At beherske disse markeringsmetoder giver dig mulighed for at producere sømløse overgange mellem stater, så du kan fortælle meningsfulde historier med data.

Kom godt i gang

Krav

Vi bygger en graf, der illustrerer bevægelsen af ​​nogle få børshandlede fonde (ETF'er) i anden halvdel af 2018. Grafen består af følgende værktøjer:

  1. Lukning af prislinjediagram
  2. Søjlediagram for handelsvolumen
  3. 50-dages simpelt glidende gennemsnit
  4. Bollinger Bands (20-dages simpelt glidende gennemsnit med standardafvigelse sat til 2,0)
  5. Åbent-høj-lav-luk-diagram (OHLC)
  6. Lysestager

Disse værktøjer bruges almindeligvis i den tekniske analyse af aktier, råvarer og andre værdipapirer. For eksempel kan handlende bruge Bollinger Bands og Candlesticks til at udlede mønstre, der repræsenterer købs- eller salgssignaler.

Sådan ser grafen ud:

Denne artikel har til formål at udstyre dig med de grundlæggende teorier om dataforbindelser og enter-update-exit-mønster for at give dig mulighed for nemt at visualisere dynamiske datasæt. Derudover dækker vi selection.join, som introduceres i D3.js's v5.8.0-udgivelse.

Det generelle opdateringsmønster

Kernen i det generelle opdateringsmønster er valget af DOM-elementer (Document Object Model) efterfulgt af binding af data til disse elementer. Disse elementer oprettes, opdateres eller fjernes derefter for at repræsentere de nødvendige data.

Deltagelse i nye data

Data sammenføjning er kortlægningen af nantallet af elementer i datasættet med nantallet af valgte DOM-noder (Document Object Model), der angiver den nødvendige handling til DOM, når dataene ændres.

Vi bruger data()metoden til at kortlægge hvert datapunkt til et tilsvarende element i DOM-valget. Derudover er det god praksis at opretholde objektkonstans ved at angive en nøgle som den unikke identifikator i hvert datapunkt. Lad os se på følgende eksempel, som er det første skridt mod gengivelse af handelsvolumenbjælker:

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

Ovenstående kodelinje vælger alle elementer med klassen vol, efterfulgt af kortlægning af this.currentDataarrayet med valget af DOM-elementer ved hjælp af data()metoden.

Det andet valgfri argument for data()tager et datapunkt som input og returnerer dateegenskaben som den valgte nøgle for hvert datapunkt.

Indtast / opdater valg

.enter()returnerer et enter-valg, der repræsenterer de elementer, der skal tilføjes, når det sammenføjede array er længere end markeringen. Dette efterfølges af opkald .append(), som opretter eller opdaterer elementer på DOM. Vi kan implementere dette på følgende måde:

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()fletter opdateringen og indtaster valg, inden de efterfølgende metodekæder anvendes til at oprette animationer mellem overgange og for at opdatere deres tilknyttede attributter. Ovenstående kodeblok giver dig mulighed for at udføre følgende handlinger på de valgte DOM-elementer:

  1. Opdateringsvalget, der består af datapunkter, der er repræsenteret af elementerne i grafen, får deres attributter opdateret i overensstemmelse hermed.
  2. Oprettelsen af elementer med klassen volmed de ovennævnte attributter defineret inden for hvert element som indtastningsvalget består af datapunkter, der ikke er repræsenteret på grafen.

Afslut valg

Fjern elementer fra vores datasæt ved at følge nedenstående enkle trin: bars.exit (). Remove ();

.exit()returnerer et exitvalg, der specificerer de datapunkter, der skal fjernes. Den .remove()metode efterfølgende sletter markeringen fra DOM.

Sådan reagerer volumenseriestængerne på ændringer i data:

Vær opmærksom på, hvordan DOM og de respektive attributter for hvert element opdateres, når vi vælger et andet datasæt:

Selection.join (fra v5.8.0)

Introduktionen af selection.joini v5.8.0 af D3.js har forenklet hele datatilslutningsprocessen. Separate funktioner er nu gået til at håndtere indtaste , opdatere , og udgang, som igen returnerer den fusionerede ind og opdatere markeringer.

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

I tilfælde af volumen seriebjælker vil anvendelsen af selection.joinresultere i følgende ændringer på vores kode:

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

Bemærk også, at vi har foretaget nogle ændringer i barernes animation. I stedet for at overføre transition()metoden til de flettede valg af enter og opdatering bruges den nu i opdateringsvalget, således at overgange kun anvendes, når datasættet er ændret.

De returnerede indtastnings- og opdateringsvalg flettes derefter og returneres af selection.join.

Bollinger Bands

På samme måde kan vi ansøge selection.joinom gengivelse af Bollinger Bands. Før vi gengiver båndene, skal vi beregne følgende egenskaber for hvert datapunkt:

  1. 20-dages simpelt glidende gennemsnit.
  2. Det øvre og nedre bånd, der har en standardafvigelse på henholdsvis 2,0 over og under det 20-dages enkle glidende gennemsnit.

Dette er formlen til beregning af standardafvigelse:

Nu oversætter vi ovenstående formel til JavaScript-kode:

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

En hurtig forklaring af beregningen af ​​standardafvigelsen og Bollinger Band-værdier på ovenstående kodeblok er som følger:

For hver iteration,

  1. Beregn gennemsnittet af den lukkede pris.
  2. Find forskellen mellem den gennemsnitlige værdi og den lukkede pris for det datapunkt.
  3. Kvadratere resultatet af hver forskel.
  4. Find summen af ​​kvadratiske forskelle.
  5. Beregn gennemsnittet af de kvadratiske forskelle for at få variansen
  6. Få kvadratroden af ​​variansen for at opnå standardafvigelsen for hvert datapunkt.
  7. Multiplicer standardafvigelsen med 2. Beregn de øvre og nedre båndværdier ved at tilføje eller trække gennemsnittet med den gangede værdi.

Med de definerede datapunkter kan vi derefter bruge til selection.joinat gengive 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) );

Dette gengiver arealdiagrammet, der angiver det område, der er udfyldt af Bollinger Bands. På opdateringsfunktionen kan vi bruge selection.transition()metoden til at levere animerede overgange til opdateringsvalget.

Lysestager

Lysestakens diagram viser de høje, lave, åbne og lukkede priser på en aktie i en bestemt periode. Hver lysestage repræsenterer et datapunkt. Grøn repræsenterer når bestanden lukker højere, mens rød repræsenterer når bestanden lukker til en lavere værdi.

I modsætning til Bollinger Bands er der ikke behov for yderligere beregninger, da priserne er tilgængelige i det eksisterende datasæt.

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

På enter-funktionen gengives hvert lysestage baseret på dets individuelle egenskaber.

Først og fremmest tildeles hvert lysestake-gruppeelement en klasse, up-dayhvis lukkeprisen er højere end den åbne pris, og down-dayhvis den lukkede pris er lavere end eller lig med åbent prisen.

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

Derefter føjer vi pathelementet, som repræsenterer den højeste og laveste pris den dag, til ovenstående valg.

 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

D3.js potentiale er ubegrænset, og ovenstående illustrationer er kun toppen af ​​isbjerget. Mange tilfredse brugere har skabt visualiseringer, der er langt mere komplekse og sofistikerede end den ovenstående. Denne liste over gratis API'er kan interessere dig, hvis du er ivrig efter at starte dine egne datavisualiseringsprojekter.

Du er velkommen til at tjekke kildekoden og den fulde demonstration af dette projekt.

Mange tak for at læse denne artikel. Hvis du har spørgsmål eller forslag, er du velkommen til at efterlade dem i kommentarerne nedenfor!

Ny på D3.js? Du kan henvise til denne artikel om det grundlæggende ved implementering af almindelige diagramkomponenter.

Særlig tak til Debbie Leong for at have gennemgået denne artikel.

Yderligere referencer:

  1. D3.js API-dokumentation
  2. Interaktiv demonstration af valg. Slutte sig til