Τρόπος εργασίας με το γενικό μοτίβο ενημέρωσης του D3.js

Μια ξενάγηση για την υλοποίηση ενοτήτων οπτικοποίησης με δυναμικά σύνολα δεδομένων

Είναι σύνηθες να αφαιρείτε το υπάρχον στοιχείο Scalable Vector Graphics (SVG) κάνοντας κλήση d3.select('#chart').remove(), πριν από την απόδοση ενός νέου γραφήματος.

Ωστόσο, ενδέχεται να υπάρχουν σενάρια όταν πρέπει να παράγετε δυναμικές απεικονίσεις από πηγές όπως εξωτερικά API. Αυτό το άρθρο θα σας δείξει πώς να το κάνετε αυτό χρησιμοποιώντας το D3.js.

Το D3.js χειρίζεται δυναμικά δεδομένα υιοθετώντας το γενικό μοτίβο ενημέρωσης. Αυτό συνήθως περιγράφεται ως ένωση δεδομένων, ακολουθούμενο από λειτουργίες στις επιλογές εισαγωγής, ενημέρωσης και εξόδου. Η εξάσκηση αυτών των μεθόδων επιλογής θα σας επιτρέψει να πραγματοποιήσετε απρόσκοπτες μεταβάσεις μεταξύ των καταστάσεων, επιτρέποντάς σας να πείτε σημαντικές ιστορίες με δεδομένα.

Ξεκινώντας

Απαιτήσεις

Θα δημιουργήσουμε ένα γράφημα που θα απεικονίζει την κίνηση μερικών χρηματιστηριακών συναλλαγών (ETFs) κατά το δεύτερο εξάμηνο του 2018. Το γράφημα αποτελείται από τα ακόλουθα εργαλεία:

  1. Γράφημα γραμμής κλεισίματος τιμών
  2. Διάγραμμα όγκου συναλλαγών
  3. Απλός μέσος όρος 50 ημερών
  4. Bollinger Bands (απλός μέσος όρος 20 ημερών, με τυπική απόκλιση σε 2,0)
  5. Γράφημα Open-high-low-close (OHLC)
  6. Κηροπήγια

Αυτά τα εργαλεία χρησιμοποιούνται συνήθως στην τεχνική ανάλυση αποθεμάτων, εμπορευμάτων και άλλων τίτλων. Για παράδειγμα, οι έμποροι μπορούν να κάνουν χρήση των Bollinger Bands και Candlesticks για να αντλήσουν μοτίβα που αντιπροσωπεύουν σήματα αγοράς ή πώλησης.

Έτσι θα μοιάζει το γράφημα:

Αυτό το άρθρο στοχεύει να σας εξοπλίσει με τις θεμελιώδεις θεωρίες των συνδέσεων δεδομένων και το μοτίβο εισόδου-ενημέρωσης-εξόδου για να σας επιτρέψει να απεικονίσετε εύκολα δυναμικά σύνολα δεδομένων. Επιπλέον, θα καλύψουμε το select.join, το οποίο παρουσιάζεται στην έκδοση v3.8.0 του D3.js.

Το γενικό μοτίβο ενημέρωσης

Η ουσία του γενικού μοτίβου ενημέρωσης είναι η επιλογή των στοιχείων του αντικειμένου μοντέλου εγγράφου (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()επιστρέφει μια επιλογή 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, με τα παραπάνω χαρακτηριστικά να ορίζονται σε κάθε στοιχείο καθώς η επιλογή enter αποτελείται από σημεία δεδομένων που δεν αντιπροσωπεύονται στο γράφημα.

Έξοδος επιλογής

Καταργήστε στοιχεία από το σύνολο δεδομένων μας ακολουθώντας τα απλά βήματα παρακάτω: bars.exit (). Remove ();

.exit()επιστρέφει μια επιλογή εξόδου, η οποία καθορίζει τα σημεία δεδομένων που πρέπει να αφαιρεθούν. Στη .remove()συνέχεια, η μέθοδος διαγράφει την επιλογή από το DOM.

Αυτός είναι ο τρόπος με τον οποίο οι γραμμές σειράς όγκου θα ανταποκριθούν σε αλλαγές στα δεδομένα:

Σημειώστε πώς ενημερώνεται το DOM και τα αντίστοιχα χαρακτηριστικά κάθε στοιχείου καθώς επιλέγουμε ένα διαφορετικό σύνολο δεδομένων:

Selection.join (από v5.8.0)

Η εισαγωγή selection.joinστο v5.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.

Bollinger Bands

Ομοίως, μπορούμε να κάνουμε αίτηση selection.joinγια την απόδοση των Bollinger Bands. Πριν από την απόδοση των ζωνών, πρέπει να υπολογίσουμε τις ακόλουθες ιδιότητες κάθε σημείου δεδομένων:

  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

Το δυναμικό του D3.js είναι απεριόριστο και οι παραπάνω εικόνες είναι απλώς η κορυφή του παγόβουνου. Πολλοί ικανοποιημένοι χρήστες δημιούργησαν απεικονίσεις που είναι πολύ πιο περίπλοκες και εξελιγμένες από αυτές που εμφανίζονται παραπάνω. Αυτή η λίστα δωρεάν API μπορεί να σας ενδιαφέρει εάν θέλετε να ξεκινήσετε τα δικά σας έργα οπτικοποίησης δεδομένων.

Μη διστάσετε να δείτε τον πηγαίο κώδικα και την πλήρη επίδειξη αυτού του έργου.

Σας ευχαριστώ πολύ που διαβάσατε αυτό το άρθρο. Εάν έχετε οποιεσδήποτε ερωτήσεις ή προτάσεις, μη διστάσετε να τα αφήσετε στα παρακάτω σχόλια!

Είστε νέοι στο D3.js; Μπορείτε να ανατρέξετε σε αυτό το άρθρο σχετικά με τα βασικά στοιχεία της εφαρμογής κοινών στοιχείων γραφήματος.

Ιδιαίτερες ευχαριστίες στην Debbie Leong για την αναθεώρηση αυτού του άρθρου.

Πρόσθετες αναφορές:

  1. Τεκμηρίωση API D3.js
  2. Διαδραστική επίδειξη του select.join