วิธีทำงานกับรูปแบบการอัปเดตทั่วไปของ D3.js

ทัวร์แนะนำเกี่ยวกับการใช้โมดูลการแสดงภาพด้วยชุดข้อมูลแบบไดนามิก

เป็นเรื่องปกติที่จะลบองค์ประกอบกราฟิกแบบเวกเตอร์ที่ปรับขนาดได้ (SVG) ที่มีอยู่ออกโดยการเรียกd3.select('#chart').remove()ก่อนที่จะแสดงแผนภูมิใหม่

อย่างไรก็ตามอาจมีสถานการณ์สมมติเมื่อคุณต้องสร้างภาพแบบไดนามิกจากแหล่งที่มาเช่น API ภายนอก บทความนี้จะแสดงวิธีการใช้ D3.js

D3.js จัดการข้อมูลแบบไดนามิกโดยใช้รูปแบบการอัปเดตทั่วไป โดยทั่วไปจะอธิบายว่าเป็นการรวมข้อมูลตามด้วยการดำเนินการเกี่ยวกับการป้อนอัปเดตและออก การเรียนรู้วิธีการเลือกเหล่านี้จะช่วยให้คุณสร้างการเปลี่ยนระหว่างสถานะได้อย่างราบรื่นช่วยให้คุณสามารถบอกเล่าเรื่องราวที่มีความหมายด้วยข้อมูล

เริ่มต้นใช้งาน

ข้อกำหนด

เราจะสร้างกราฟที่แสดงการเคลื่อนไหวของ Exchange-Traded Funds (ETF) สองสามตัวในช่วงครึ่งหลังของปี 2018 กราฟประกอบด้วยเครื่องมือต่อไปนี้:

  1. กำลังปิดกราฟเส้นราคา
  2. กราฟแท่งปริมาณการซื้อขาย
  3. ค่าเฉลี่ยเคลื่อนที่อย่างง่าย 50 วัน
  4. Bollinger Bands (ค่าเฉลี่ยเคลื่อนที่อย่างง่าย 20 วันโดยมีค่าเบี่ยงเบนมาตรฐานตั้งไว้ที่ 2.0)
  5. แผนภูมิเปิดสูง - ต่ำ - ปิด (OHLC)
  6. เชิงเทียน

เครื่องมือเหล่านี้มักใช้ในการวิเคราะห์ทางเทคนิคของหุ้นสินค้าโภคภัณฑ์และหลักทรัพย์อื่น ๆ ตัวอย่างเช่นผู้ค้าอาจใช้ประโยชน์จาก Bollinger Bands และ Candlesticks เพื่อหารูปแบบที่แสดงถึงสัญญาณซื้อหรือขาย

นี่คือลักษณะของกราฟ:

บทความนี้มีวัตถุประสงค์เพื่อให้คุณมีทฤษฎีพื้นฐานของการรวมข้อมูลและรูปแบบการเข้า - อัปเดต - ออกเพื่อให้คุณเห็นภาพชุดข้อมูลแบบไดนามิกได้อย่างง่ายดาย นอกจากนี้เราจะครอบคลุมถึง selection.join ซึ่งเปิดตัวในรุ่น v5.8.0 ของ D3.js

รูปแบบการอัปเดตทั่วไป

ส่วนสำคัญของรูปแบบการอัปเดตทั่วไปคือการเลือกองค์ประกอบ Document Object Model (DOM) ตามด้วยการผูกข้อมูลกับองค์ประกอบเหล่านี้ จากนั้นองค์ประกอบเหล่านี้จะถูกสร้างปรับปรุงหรือลบออกเพื่อแสดงข้อมูลที่จำเป็น

การเข้าร่วมข้อมูลใหม่

การรวมข้อมูลคือการจับคู่nจำนวนองค์ประกอบในชุดข้อมูลด้วยnจำนวนโหนด Document Object Model (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

ในทำนองเดียวกันเราสามารถใช้selection.joinกับการเรนเดอร์ของ Bollinger Bands ก่อนที่จะแสดง 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. คำนวณค่าเฉลี่ยของผลต่างกำลังสองเพื่อรับความแปรปรวน
  6. หารากที่สองของความแปรปรวนเพื่อหาค่าเบี่ยงเบนมาตรฐานสำหรับแต่ละจุดข้อมูล
  7. คูณค่าเบี่ยงเบนมาตรฐานด้วย 2 คำนวณค่าแถบด้านบนและด้านล่างโดยการบวกหรือลบค่าเฉลี่ยด้วยค่าคูณ

เมื่อกำหนดจุดข้อมูลแล้วเราสามารถใช้selection.joinเพื่อแสดงผล 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) );

สิ่งนี้แสดงผลแผนภูมิพื้นที่ซึ่งแสดงถึงพื้นที่ที่เต็มไปด้วย Bollinger Bands ในฟังก์ชันการอัปเดตเราสามารถใช้selection.transition()วิธีการเพื่อให้มีการเปลี่ยนภาพเคลื่อนไหวในการเลือกอัปเดต

เชิงเทียน

กราฟแท่งเทียนแสดงราคาสูงต่ำราคาเปิดและปิดของหุ้นในช่วงเวลาหนึ่ง แท่งเทียนแต่ละแท่งแทนจุดข้อมูล สีเขียวหมายถึงเมื่อหุ้นปิดสูงขึ้นในขณะที่สีแดงหมายถึงเมื่อหุ้นปิดที่มูลค่าต่ำกว่า

ซึ่งแตกต่างจาก Bollinger Bands ไม่จำเป็นต้องคำนวณเพิ่มเติมเนื่องจากราคามีอยู่ในชุดข้อมูลที่มีอยู่

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

ในฟังก์ชัน enter แท่งเทียนแต่ละแท่งจะแสดงผลตามคุณสมบัติของมัน

ก่อนอื่นองค์ประกอบของกลุ่มแท่งเทียนแต่ละตัวจะได้รับการกำหนดระดับup-dayว่าราคาปิดสูงกว่าราคาเปิดหรือไม่และdown-dayหากราคาปิดต่ำกว่าหรือเท่ากับราคาเปิด

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

ต่อไปเราจะผนวกpathองค์ประกอบซึ่งแสดงถึงราคาสูงสุดและต่ำสุดของวันนั้นเข้ากับตัวเลือกด้านบน

 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