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:
- Încărcarea și analizarea datelor
- Element SVG
- Axele X și Y
- Închideți graficul cu linii de preț
- Diagramă simplă a curbei medii mobile cu câteva calcule
- Diagrama cu bare în serie a volumului
- 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 <d
iv> 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 responsi
metoda 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:

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:

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 <re
ct> element. Prin urmare, the ap
metoda pend () acceptă un argument
„rect”, care creează a new
e objec
t î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:

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 <re
ct> element. Prin urmare, the ap
metoda pend () acceptă un argument
„rect”, care creează a new
e objec
t î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:

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.