O abordare pas cu pas către vizualizarea seturilor de date financiare

Este o provocare comunicarea datelor și afișarea acestor vizualizări pe mai multe dispozitive și platforme.

„Datele sunt la fel ca brute. Este valoros, dar dacă nu este rafinat, nu poate fi folosit cu adevărat. ” – Michael Palmer

D3 (Documentele bazate pe date) rezolvă această dilemă veche. Oferă dezvoltatorilor și analiștilor capacitatea de a construi vizualizări personalizate pentru web cu libertate completă. D3.js ne permite să legăm date la DOM (Document Object Model). Apoi aplicați transformări bazate pe date pentru a crea vizualizări rafinate ale datelor.

În acest tutorial, vom înțelege cum putem face ca biblioteca D3.js să funcționeze pentru noi.

Noțiuni de bază

Vom construi un grafic care ilustrează mișcarea unui instrument financiar pe o perioadă de timp. Această vizualizare seamănă cu graficele de preț furnizate de Yahoo Finance. Vom descompune diferitele componente necesare pentru a reda o diagramă interactivă a prețurilor care urmărește un anumit stoc.

Componente necesare:

  1. Încărcarea și analizarea datelor
  2. Element SVG
  3. Axele X și Y
  4. Închideți graficul cu linii de preț
  5. Diagramă simplă a curbei medii mobile cu câteva calcule
  6. Diagrama cu bare în serie a volumului
  7. Crucea mouse-ului și legenda

Încărcarea și analizarea datelor

const loadData = d3.json('sample-data.json').then(data => {
  const chartResultsData = data['chart']['result'][0];
  const quoteData = chartResultsData['indicators']['quote'][0];
  return chartResultsData['timestamp'].map((time, index) => ({
    date: new Date(time * 1000),
    high: quoteData['high'][index],
    low: quoteData['low'][index],
    open: quoteData['open'][index],
    close: quoteData['close'][index],
    volume: quoteData['volume'][index]
  }));
});

În primul rând, vom folosi aduc modul pentru a încărca datele noastre eșantion. D3-fetch acceptă și alte formate, cum ar fi fișierele TSV și CSV. Datele vor fi apoi procesate în continuare pentru a returna o serie de obiecte. Fiecare obiect conține marca temporală de tranzacționare, preț ridicat, preț scăzut, preț deschis, preț închis și volumul tranzacțiilor.

body {
  background: #00151c;
}
#chart {
  background: #0e3040;
  color: #67809f;
}

Adăugați proprietățile CSS de bază de mai sus pentru a personaliza stilul diagramei dvs. pentru un apel vizual maxim.

Adăugarea elementului SVG

const initialiseChart = data => {
  const margin = { top: 50, right: 50, bottom: 50, left: 50 };
  const width = window.innerWidth - margin.left - margin.right;
  const height = window.innerHeight - margin.top - margin.bottom; 
  // add SVG to the page
  const svg = d3
    .select('#chart')
    .append('svg')
    .attr('width', width + margin['left'] + margin['right'])
    .attr('height', height + margin['top'] + margin['bottom'])
    .call(responsivefy)
    .append('g')
    .attr('transform', `translate(${margin['left']},  ${margin['top']})`);

Ulterior, putem folosi append() metoda de a adăuga elementul SVG la <div> element cu the id, diagramă. Apoi, noi, noie the metoda attr () pentru a atribui lățimea și înălțimea elementului SVG. Apoi am call the responsimetoda vefy () (inițial writtro de Brendan Sudol). Aceasta permite elementului SVG să aibă capacități de reacție ascultând evenimente de redimensionare a ferestrei.

Nu uitați să adăugați elementul grup SVG la elementul SVG de mai sus înainte de a-l traduce folosind valorile din margin constant.

Redarea axelor X și Y

Înainte de a reda componenta axelor, va trebui să ne definim domeniul și domeniul, care vor fi apoi utilizate pentru a crea scale pentru axele noastre

// find data range
const xMin = d3.min(data, d => {
  return d['date'];
});
const xMax = d3.max(data, d => {
  return d['date'];
});
const yMin = d3.min(data, d => {
  return d['close'];
});
const yMax = d3.max(data, d => {
  return d['close'];
});
// scales for the charts
const xScale = d3
  .scaleTime()
  .domain([xMin, xMax])
  .range([0, width]);
const yScale = d3
  .scaleLinear()
  .domain([yMin - 5, yMax])
  .range([height, 0]);

Axele x și y pentru graficul de linie de preț închis constau în data tranzacționării și, respectiv, în prețul de închidere. Prin urmare, trebuie să definim valorile minime și maxime x și y, folosind d3.max() și d3.min(). Putem folosi apoi Scara D3‘s scaleTime() și scaleLinear() pentru a crea scara de timp pe axa x și respectiv scala liniară pe axa y. Gama scalei este definită de lățimea și înălțimea elementului nostru SVG.

// create the axes component
svg
  .append('g')
  .attr('id', 'xAxis')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(xScale));
svg
  .append('g')
  .attr('id', 'yAxis')
  .attr('transform', `translate(${width}, 0)`)
  .call(d3.axisRight(yScale));

După acest pas, trebuie să adăugăm primul g element la elementul SVG, care numește d3.axisBottom() metoda, luând în considerare xScale ca parametru pentru a genera axa x. Axa x este apoi tradusă în partea de jos a zonei graficului. În mod similar, axa y este generată prin adăugarea g element, apelând d3.axisRight () cu yScale ca parametru, înainte de a traduce axa y în dreapta zonei graficului.

Redarea graficului de linie de preț închis

// generates close price line chart when called
const line = d3
  .line()
  .x(d => {
    return xScale(d['date']);
  })
  .y(d => {
    return yScale(d['close']);
  });
// Append the path and bind data
svg
 .append('path')
 .data([data])
 .style('fill', 'none')
 .attr('id', 'priceChart')
 .attr('stroke', 'steelblue')
 .attr('stroke-width', '1.5')
 .attr('d', line);

Acum, putem adăuga fișierul path element din elementul nostru principal SVG, urmat de trecerea setului de date analizat,data. Am setat atributul d cu funcția noastră de ajutor, line. care numește d3.line() metodă. x și y atributele liniei acceptă funcțiile anonime și returnează data și respectiv prețul de închidere.

Până acum, așa ar trebui să arate graficul:

Cum sa construiti diagrame istorice ale preturilor cu D3js
Punct de control # 1: Închideți graficul de linii de preț, cu axele X și Y.

Redarea curbei medii mobile simple

În loc să ne bazăm doar pe prețul apropiat ca singura noastră formă de indicator tehnic, folosim Medie mobilă simplă. Această medie identifică tendințele ascendente și descendente pentru o anumită securitate.

const movingAverage = (data, numberOfPricePoints) => {
  return data.map((row, index, total) => {
    const start = Math.max(0, index - numberOfPricePoints);
    const end = index;
    const subset = total.slice(start, end + 1);
    const sum = subset.reduce((a, b) => {
      return a + b['close'];
    }, 0);
    return {
      date: row['date'],
      average: sum / subset.length
    };
  });
};

Ne definim funcția de ajutor, movingAverage pentru a calcula media mobilă simplă. Această funcție acceptă doi parametri, și anume setul de date și numărul de puncte de preț sau perioade. Apoi returnează o serie de obiecte, fiecare obiect conținând data și media pentru fiecare punct de date.

// calculates simple moving average over 50 days
const movingAverageData = movingAverage(data, 49);
// generates moving average curve when called
const movingAverageLine = d3
 .line()
 .x(d => {
  return xScale(d['date']);
 })
 .y(d => {
  return yScale(d['average']);
 })
  .curve(d3.curveBasis);
svg
  .append('path')
  .data([movingAverageData])
  .style('fill', 'none')
  .attr('id', 'movingAverageLine')
  .attr('stroke', '#FF8900')
  .attr('d', movingAverageLine);

Pentru contextul nostru actual, movingAverage() calculează media mobilă simplă pe o perioadă de 50 de zile. Similar cu graficul de linie de preț închis, adăugăm path element din elementul nostru principal SVG, urmat de trecerea setului de date mediu mobil și setarea atributului d cu funcția noastră de ajutor, movingAverageLine. Singura diferență față de cele de mai sus este că am trecut d3.curveBasis la d3.line().curve() în vederea realizării unei curbe.

Acest lucru are ca rezultat curba medie mobilă simplă suprapusă peste graficul nostru actual:

1612173549 690 Cum sa construiti diagrame istorice ale preturilor cu D3js
Punctul de control # 2: curbă portocalie, care descrie media mobilă simplă. Acest lucru ne oferă o idee mai bună despre mișcarea prețurilor.

Redarea diagramei de bare a volumului

Pentru această componentă, vom reda tranzacția volum sub forma unei diagrame cu bare colorate care ocupă același element SVG. Barele sunt verzi atunci când stocul se închide mai mare decât prețul de închidere din ziua precedentă. Sunt roșii atunci când stocul se închide mai mic decât prețul de închidere din ziua precedentă. Aceasta ilustrează volumul tranzacționat pentru fiecare dată de tranzacționare. Acest lucru poate fi apoi utilizat alături de graficul de mai sus pentru a analiza mișcările de preț.

/* Volume series bars */
const volData = data.filter(d => d['volume'] !== null && d['volume']   !== 0);
const yMinVolume = d3.min(volData, d => {
  return Math.min(d['volume']);
});
const yMaxVolume = d3.max(volData, d => {
  return Math.max(d['volume']);
});
const yVolumeScale = d3
  .scaleLinear()
  .domain([yMinVolume, yMaxVolume])
  .range([height, 0]);

Axele x și y pentru graficul de bare din seria volumelor constau în data tranzacției și, respectiv, în volum. Astfel, va trebui să redefinim valorile y minime și maxime și să folosim scaleLinear()pe axa y. Gama acestor scale este definită de lățimea și înălțimea elementului nostru SVG. Vom refolosi xScale deoarece axa x a graficului cu bare corespunde în mod similar cu data tranzacției.

svg
  .selectAll()
  .data(volData)
  .enter()
  .append('rect')
  .attr('x', d => {
    return xScale(d['date']);
  })
  .attr('y', d => {
    return yVolumeScale(d['volume']);
  })
  .attr('fill', (d, i) => {
    if (i === 0) {
      return '#03a678';
    } else {  
      return volData[i - 1].close > d.close ? '#c0392b' : '#03a678'; 
    }
  })
  .attr('width', 1)
  .attr('height', d => {
    return height - yVolumeScale(d['volume']);
  });

Această secțiune se bazează pe înțelegerea dvs. despre modul în careselectAll() metoda funcționează cu enter() și append() metode. Poate doriți să citiți acest (scris de Mike Bostock el însuși) dacă nu sunteți familiarizați cu aceste metode. Acest lucru poate fi important deoarece aceste metode sunt utilizate ca parte a introduceți-actualizați-ieșiți model, pe care îl pot acoperi într-un tutorial ulterior.

Pentru a reda barele, vom folosi mai întâi .selectAll() pentru a returna o selecție goală sau o matrice goală. Apoi, trecem volData pentru a defini înălțimea fiecărei bare. enter() metoda compară volData set de date cu selecția din selectAll(), care este în prezent gol. În prezent, DOM nu conține niciunul <rect> element. Prin urmare, the apmetoda pend () acceptă un argument „rect”, care creează a new element din DOM pentru fiecare singlee object în volData.

Iată o defalcare a atributelor barelor. Vom folosi următoarele atribute: x, y, fill, width, și height.

.attr('x', d => {
  return xScale(d['date']);
})
.attr('y', d => {
  return yVolumeScale(d['volume']);
})

Primul attr() metoda definește coordonata x. Acceptă o funcție anonimă care returnează data. În mod similar, al doilea attr() metoda definește coordonata y. Acceptă o funcție anonimă care returnează volumul. Acestea vor defini poziția fiecărei bare.

.attr('width', 1)
.attr('height', d => {
  return height - yVolumeScale(d['volume']);
});

Atribuim o lățime de 1 pixel fiecărei bare. Pentru a face bara să se întindă de sus (definită de y) pe axa x, deduceți pur și simplu înălțimea cu y valoare.

.attr('fill', (d, i) => {
  if (i === 0) {
    return '#03a678';
  } else {  
    return volData[i - 1].close > d.close ? '#c0392b' : '#03a678'; 
  }
})

Vă amintiți modul în care barele vor fi codate în culori? Vom folosi fill atribut pentru a defini culorile fiecărei bare. Pentru acțiunile care au închis mai mult decât prețul de închidere din ziua precedentă, bara va avea culoarea verde. În caz contrar, bara va fi roșie.

Așa ar trebui să arate graficul curent:

1612173549 646 Cum sa construiti diagrame istorice ale preturilor cu D3js
Punctul de control # 3: Graficul seriei volumului, reprezentat de bare roșii și verzi.

Redarea Crosshair și Legend pentru interactivitate

Am ajuns la pasul final al acestui tutorial, prin care vom genera un punct de reper care să afișeze liniile de drop. Trecerea peste diferite puncte din grafic va face ca legendele să fie actualizate. Aceasta ne oferă informații complete (preț deschis, preț închis, preț ridicat, preț scăzut și volum) pentru fiecare dată de tranzacționare.

Următoarea secțiune este menționată de la Exemplul excelent al lui Micah Stubb.

// renders x and y crosshair
const focus = svg
  .append('g')
  .attr('class', 'focus')
  .style('display', 'none');
focus.append('circle').attr('r', 4.5);
focus.append('line').classed('x', true);
focus.append('line').classed('y', true);
svg
  .append('rect')
  .attr('class', 'overlay')
  .attr('width', width)
  .attr('height', height)
  .on('mouseover', () => focus.style('display', null))
  .on('mouseout', () => focus.style('display', 'none'))
  .on('mousemove', generateCrosshair);
d3.select('.overlay').style('fill', 'none');
d3.select('.overlay').style('pointer-events', 'all');
d3.selectAll('.focus line').style('fill', 'none');
d3.selectAll('.focus line').style('stroke', '#67809f');
d3.selectAll('.focus line').style('stroke-width', '1.5px');
d3.selectAll('.focus line').style('stroke-dasharray', '3 3');

Crosshair-ul este format dintr-un cerc translucid cu linii de cădere formate din liniuțe. Blocul de cod de mai sus oferă stilul elementelor individuale. La trecerea cu mouse-ul, acesta va genera mireasa pe baza funcției de mai jos.

const bisectDate = d3.bisector(d => d.date).left;
function generateCrosshair() {
  //returns corresponding value from the domain
  const correspondingDate = xScale.invert(d3.mouse(this)[0]);
  //gets insertion point
  const i = bisectDate(data, correspondingDate, 1);
  const d0 = data[i - 1];
  const d1 = data[i];
  const currentPoint = correspondingDate - d0['date'] > d1['date'] - correspondingDate ? d1 : d0;
  
  focus.attr('transform',`translate(${xScale(currentPoint['date'])},     ${yScale(currentPoint['close'])})`);
focus
  .select('line.x')
  .attr('x1', 0)
  .attr('x2', width - xScale(currentPoint['date']))
  .attr('y1', 0)
  .attr('y2', 0);
focus
  .select('line.y')
  .attr('x1', 0)
  .attr('x2', 0)
  .attr('y1', 0)
  .attr('y2', height - yScale(currentPoint['close']));
 updateLegends(currentPoint);
}

Putem folosi apoi d3.bisector () metoda de localizare a punctului de inserare, care va evidenția cel mai apropiat punct de date din graficul de linie de preț închis. După determinarea currentPoint, liniile de drop vor fi actualizate. updateLegends() metoda folosește currentPoint ca parametru.

const updateLegends = currentData => {  d3.selectAll('.lineLegend').remove();
const updateLegends = currentData => {
  d3.selectAll('.lineLegend').remove();
  const legendKeys = Object.keys(data[0]);
  const lineLegend = svg
    .selectAll('.lineLegend')
    .data(legendKeys)
    .enter()
    .append('g')
    .attr('class', 'lineLegend')
    .attr('transform', (d, i) => {
      return `translate(0, ${i * 20})`;
    });
  lineLegend
    .append('text')
    .text(d => {
      if (d === 'date') {
        return `${d}: ${currentData[d].toLocaleDateString()}`;
      } else if ( d === 'high' || d === 'low' || d === 'open' || d === 'close') {
        return `${d}: ${currentData[d].toFixed(2)}`;
      } else {
        return `${d}: ${currentData[d]}`;
      }
    })
    .style('fill', 'white')
    .attr('transform', 'translate(15,9)');
  };

updateLegends() metoda actualizează legenda afișând data, prețul deschis, prețul închis, prețul ridicat, prețul scăzut și volumul punctului de trecere al mouse-ului selectat pe graficul liniar de închidere. Similar cu graficele cu bare de volum, vom folosi selectAll() metoda cu enter() și append() metode.

Pentru a reda legendele, vom folosi.selectAll('.lineLegend') pentru a selecta legendele, urmat de apelarea remove() metoda de eliminare a acestora. Apoi, trecem cheile legendelor, legendKeys, care va fi folosit pentru a defini înălțimea fiecărei bare. enter() se numește metoda, care compară volData set de date și la selectarea din selectAll(), care este în prezent gol. În prezent, DOM nu conține niciunul <rect> element. Prin urmare, the apmetoda pend () acceptă un argument „rect”, care creează a new element din DOM pentru fiecare singlee object în volData.

Apoi, adăugați legendele cu proprietățile lor respective. Procesăm în continuare valorile prin conversia prețurilor la 2 zecimale. De asemenea, setăm obiectul dată la locale implicite pentru lizibilitate.

Acesta va fi rezultatul final:

Cum sa construiti diagrame istorice ale preturilor cu D3js
Punct de control # 4: Treceți cu mouse-ul peste orice parte a graficului!

Gânduri de închidere

Felicitări! Ați ajuns la sfârșitul acestui tutorial. După cum s-a demonstrat mai sus, D3.js este simplu, dar dinamic. Vă permite să creați vizualizări personalizate pentru toate seturile de date. În următoarele săptămâni, voi lansa a doua parte a acestei serii, care se va scufunda profund în modelul de intrare-actualizare-ieșire al lui D3.js. Între timp, vă recomandăm să verificați Documentația API, mai multe tutoriale, și alte vizualizări interesante construite cu D3.js.

Simțiți-vă liber să verificați cod sursa la fel de bine ca demonstrație completă din acest tutorial. Mulțumesc și sper că ai învățat ceva nou astăzi!

Mulțumiri speciale Debbie Leong pentru că a revizuit acest articol.