Sådan oprettes historiske prisdiagrammer med D3.js

En trin for trin i retning af visualisering af finansielle datasæt

Foto af Kevin Ku fra Pexels

Det er en udfordring at kommunikere data og vise disse visualiseringer på flere enheder og platforme.

”Data er ligesom rå. Det er værdifuldt, men hvis det ikke er raffineret, kan det ikke rigtigt bruges. ”- Michael Palmer

D3 (datastyrede dokumenter) løser dette ældgamle dilemma. Det giver udviklere og analytikere muligheden for at opbygge tilpassede visualiseringer til Internettet med fuld frihed. D3.js giver os mulighed for at binde data til DOM (Document Object Model). Anvend derefter datadrevne transformationer for at skabe raffinerede visualiseringer af data.

I denne tutorial vil vi forstå, hvordan vi kan få D3.js-biblioteket til at fungere for os.

Kom godt i gang

Vi bygger et diagram, der illustrerer bevægelsen af ​​et finansielt instrument over en periode. Denne visualisering ligner pristabellerne leveret af Yahoo Finance. Vi opdeler de forskellige komponenter, der kræves for at gengive et interaktivt prisoversigt, der sporer et bestemt lager.

Nødvendige komponenter:

  1. Indlæsning og analyse af data
  2. SVG-element
  3. X- og Y-akser
  4. Luk prislinjediagram
  5. Enkelt glidende gennemsnitskurvediagram med nogle beregninger
  6. Søjlediagram for lydstyrkeserie
  7. Flytende krydshår og legende

Indlæsning og parsing af data

const loadData = d3.json ('sample-data.json'). derefter (data => {
  const chartResultsData = data ['diagram'] ['resultat'] [0];
  const quoteData = chartResultsData ['indicator'] ['quote'] [0];
  return chartResultsData ['timestamp']. map ((tid, indeks) => ({
    dato: ny dato (tid * 1000),
    high: quoteData ['high'] [index],
    low: quoteData ['low'] [index],
    open: quoteData ['open'] [index],
    close: quoteData ['close'] [index],
    volume: quoteData ['volume'] [index]
  }));
});

Først vil vi bruge hentemodulet til at indlæse vores eksempeldata. D3-fetch understøtter også andre formater, såsom TSV og CSV-filer. Dataene behandles derefter yderligere for at returnere en række objekter. Hvert objekt indeholder handelstidstempel, høj pris, lav pris, åben pris, tæt pris og handelsvolumen.

krop {
  baggrund: # 00151c;
}
#chart {
  baggrund: # 0e3040;
  farve: # 67809f;
}

Tilføj ovenstående CSS-egenskaber for at tilpasse stilen på dit diagram for maksimal visuel appel.

Tilføjelse af SVG-elementet

const initialiseChart = data => {
  const margin = {top: 50, højre: 50, bund: 50, venstre: 50};
  const bredde = windows.innerWidth - margin.left - margin.right;
  const højde = windows.innerHøjde - margin.top - margin.bottom;
  // tilføj SVG til siden
  const svg = d3
    .Select ( '# diagram')
    .append (SVG)
    .attr ('bredde', bredde + margin ['venstre'] + margin ['højre'])
    .attr ('højde', højde + margen ['top'] + margin ['bund'])
    .call (responsivefy)
    .append ( 'g')
    .attr ('transform', `translate ($ {margin ['left']}, $ {margin ['top']})`);

Efterfølgende kan vi bruge metoden append () til at tilføje SVG-elementet til

-elementet med id-kortet. Dernæst bruger vi metoden attr () til at tildele SVG-elementets bredde og højde. Vi kalder derefter metoden responsivefy () (oprindeligt skrevet af Brendan Sudol). Dette gør det muligt for SVG-elementet at have lydhøre egenskaber ved at lytte til begivenheder i størrelse af størrelse.

Husk at tilføje SVG-gruppeelementet til ovennævnte SVG-element, før du oversætter det ved hjælp af værdierne fra marginkonstanten.

Rendering af X- og Y-akserne

Før vi gengiver aksekomponenten, bliver vi nødt til at definere vores domæne og interval, som derefter bruges til at oprette vores skalaer for akserne

// find datainterval
const xMin = d3.min (data, d => {
  returner d ['date'];
});
const xMax = d3.max (data, d => {
  returner d ['date'];
});
const yMin = d3.min (data, d => {
  returner d ['close'];
});
const yMax = d3.max (data, d => {
  returner d ['close'];
});
// skalaer for diagrammerne
const xScale = d3
  .scaleTime ()
  .domæne ([xMin, xMax])
  .range ([0, bredde]);
const yScale = d3
  .scaleLinear ()
  .domæne ([yMin - 5, yMax])
  .range ([højde, 0]);

X- og y-akserne for linjediagrammet for den nære pris består af henholdsvis handelsdato og lukkepris. Derfor er vi nødt til at definere minimums- og maksimumsværdierne for x og y ved hjælp af d3.max () og d3.min (). Vi kan derefter bruge D3-skalaens skalaTid () og skalaLinear () til at oprette tidsskalaen på henholdsvis x-aksen og den lineære skala på y-aksen. Vægtenes rækkevidde defineres af bredden og højden af ​​vores SVG-element.

// oprette aksekomponenten
svg
  .append ( 'g')
  .attr ('id', 'xAxis')
  .attr ('transform', 'oversætte (0, $ {højde})')
  .call (d3.axisBottom (xSkala));
svg
  .append ( 'g')
  .attr ('id', 'yAxis')
  .attr ('transform', 'oversætte ($ {bredde}, 0)')
  .call (d3.axisRight (yscale));

Efter dette trin skal vi tilføje det første g-element til SVG-elementet, der kalder metoden d3.axisBottom (), idet vi tager xScale ind som parameter for at generere x-aksen. X-aksen oversættes derefter til bunden af ​​kortområdet. På lignende måde genereres y-aksen ved at tilføje g-elementet ved at kalde d3.axisRight () med yScale som parameteren, før y-aksen oversættes til højre for diagramområdet.

Fremvisning af det nære prislinjediagram

// genererer tæt prislinjediagram, når det kaldes
const line = d3
  .line ()
  .x (d => {
    returner xScale (d ['date']);
  })
  .y (d => {
    return yScale (d ['close']);
  });
// Tilføj stien og bind data
svg
 .append ( 'path')
 Data, ([data])
 .style ('fyld', 'ingen')
 .attr ('id', 'prisoversigt')
 .attr ('slag', 'stålblå')
 .attr ('slag-bredde', '1,5')
 .attr ('d', linje);

Nu kan vi tilføje sti-elementet i vores vigtigste SVG-element, efterfulgt af at videregive vores analyserede datasæt, data. Vi indstiller attributten d med vores hjælperfunktion, linje. som kalder metoden d3.line (). X- og y-attributterne på linjen accepterer de anonyme funktioner og returnerer henholdsvis dato og lukkepris.

På nuværende tidspunkt skal sådan udseende se ud:

Kontrolpunkt 1: Luk prislinjediagram med X- og Y-akserne.

Gengivelse af den enkle bevægende gennemsnitskurve

I stedet for kun at stole på den nære pris som vores eneste form for teknisk indikator, bruger vi Simple Moving Average. Dette gennemsnit identificerer op- og nedtrend for den bestemte sikkerhed.

const movingAverage = (data, numberOfPricePoints) => {
  return data.map ((række, indeks, samlet) => {
    const start = Math.max (0, index - numberOfPricePoints);
    const end = indeks;
    const subset = total.slice (start, slut + 1);
    const sum = subset.reduce ((a, b) => {
      returner a + b ['close'];
    }, 0);
    Vend tilbage {
      dato: række ['dato'],
      gennemsnit: sum / delmængde
    };
  });
};

Vi definerer vores hjælperfunktion, flytende gennemsnit for at beregne det enkle bevægelsesmiddel. Denne funktion accepterer to parametre, nemlig datasættet, og antallet af prispoint eller perioder. Derefter returneres en række objekter, hvor hvert objekt indeholder datoen og gennemsnittet for hvert datapunkt.

// beregner simpelt glidende gennemsnit over 50 dage
const movingAverageData = movingAverage (data, 49);
// genererer glidende gennemsnitskurve, når det kaldes
const movingAverageLine = d3
 .line ()
 .x (d => {
  returner xScale (d ['date']);
 })
 .y (d => {
  return yScale (d ['gennemsnit']);
 })
  .curve (d3.curveBasis);
svg
  .append ( 'path')
  Data, ([movingAverageData])
  .style ('fyld', 'ingen')
  .attr ('id', 'movingAverageLine')
  .attr ('slag', '# FF8900')
  .attr ('d', movingAverageLine);

For vores nuværende kontekst beregner movingAverage () det enkle bevægende gennemsnit over en periode på 50 dage. I lighed med det lave prislinjediagram tilføjer vi stienelementet i vores vigtigste SVG-element, efterfulgt af at videregive vores bevægende gennemsnit-datasæt og indstille attributten d med vores hjælperfunktion, moveAverageLine. Den eneste forskel fra ovenstående er, at vi passerede d3.curveBasis til d3.line () .kurve () for at opnå en kurve.

Dette resulterer i den enkle bevægende gennemsnitskurve, der er lagt over toppen af ​​vores nuværende diagram:

Kontrolpunkt 2: Orange kurve, der viser det enkle glidende gennemsnit. Dette giver os en bedre idé om prisbevægelsen.

Fremvisning af søjlediagrammet for volumserien

For denne komponent gengiver vi handelsvolumen i form af et farvekodet søjlediagram, der besætter det samme SVG-element. Søjlerne er grønne, når bestanden lukker højere end den foregående dags tætte pris. De er røde, når bestanden lukker lavere end den foregående dags tætte pris. Dette illustrerer den mængde, der handles for hver handelsdato. Dette kan derefter bruges sammen med ovenstående diagram til at analysere prisbevægelser.

/ * Linjer i lydstyrkeserie * /
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 ()
  .domæne ([yMinVolume, yMaxVolume])
  .range ([højde, 0]);

X- og y-akserne for søjlediagrammet for volumenserier består af henholdsvis handelsdato og -volumen. Således bliver vi nødt til at omdefinere minimum og maksimal y-værdier og gøre brug af skalaLinear () på y-aksen. Omfanget af disse skalaer er defineret af bredden og højden af ​​vores SVG-element. Vi vil genbruge xScale, da x-aksen i søjlediagrammet svarer til handelsdatoen.

svg
  .Vælg alle()
  Data, (volData)
  .gå ind()
  .append (rect ')
  .attr ('x', d => {
    returner xScale (d ['date']);
  })
  .attr ('y', d => {
    return yVolumeScale (d ['volume']);
  })
  .attr ('udfyld', (d, i) => {
    hvis (i === 0) {
      retur '# 03a678';
    } andet {
      return volData [i - 1] .close> d.close? '# c0392b': '# 03a678';
    }
  })
  .attr ('bredde', 1)
  .attr ('højde', d => {
    returhøjde - yVolumeScale (d ['volume']);
  });

Dette afsnit er afhængig af din forståelse af, hvordan denne selektiveAlle () -metode fungerer med enter () og tilføj () metoderne. Du ønsker muligvis at læse dette (skrevet af Mike Bostock selv), hvis du ikke kender disse metoder. Dette kan være vigtigt, da disse metoder bruges som en del af enter-update-exit-mønsteret, som jeg måske dækker i en efterfølgende tutorial.

For at gengive bjælkerne bruger vi først .selectAll () til at returnere et tomt valg eller et tomt array. Derefter passerer vi volData for at definere højden på hver bjælke. Metoden enter () sammenligner volData-datasættet med markeringen fra selectAll (), som i øjeblikket er tom. I øjeblikket indeholder DOM ikke noget -element. Således accepterer append () -metoden et argument 'rekt', som skaber et nyt element i DOM for hvert enkelt objekt i volData.

Her er en oversigt over søjlernes attributter. Vi bruger følgende attributter: x, y, udfyldning, bredde og højde.

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

Den første attr () -metode definerer x-koordinaten. Den accepterer en anonym funktion, der returnerer datoen. Tilsvarende definerer den anden attr () -metode y-koordinaten. Den accepterer en anonym funktion, der returnerer lydstyrken. Disse definerer placeringen af ​​hver bjælke.

.attr ('bredde', 1)
.attr ('højde', d => {
  returhøjde - yVolumeScale (d ['volume']);
});

Vi tildeler en bredde på 1 pixel til hver bjælke. For at få stangen til at strække sig fra toppen (defineret af y) til x-aksen skal du blot trække højden med y-værdien.

.attr ('udfyld', (d, i) => {
  hvis (i === 0) {
    retur '# 03a678';
  } andet {
    return volData [i - 1] .close> d.close? '# c0392b': '# 03a678';
  }
})

Kan du huske, hvordan søjlerne bliver farvekodet? Vi bruger fyldattributten til at definere farverne på hver bjælke. For lagre, der har lukket højere end den foregående dags tætte pris, vil søjlen være grøn. Ellers vil søjlen være rød.

Sådan ser dit aktuelle diagram ud:

Checkpoint nr. 3: Diagram over volumenserier repræsenteret af røde og grønne søjler.

Rendering Crosshair og Legend for interaktivitet

Vi er nået til det sidste trin i denne tutorial, hvor vi genererer et overkryds med mus over, der viser droplines. Mousing over de forskellige punkter i diagrammet får legenderne til at blive opdateret. Dette giver os den fulde information (åben pris, tæt pris, høj pris, lav pris og volumen) for hver handelsdato.

Følgende afsnit henvises til fra Micah Stubbs fremragende eksempel.

// gengiver x og y krydshår
const focus = svg
  .append ( 'g')
  .attr ('klasse', 'fokus')
  .style ('display', 'ingen');
fokus.append ('cirkel'). attr ('r', 4.5);
fokus.append ('linje'). klasset ('x', sandt);
fokus.append ('linje'). klasset ('y', sandt);
svg
  .append (rect ')
  .attr ('klasse', 'overlay')
  .attr ('bredde', bredde)
  .attr ('højde', højde)
  .on ('mouseover', () => focus.style ('display', null))
  .on ('mouseout', () => focus.style ('display', 'none'))
  .on ('mousemove', createCrosshair);
d3.select ('. overlay'). style ('fill', 'none');
d3.select ('. overlay'). style ('pointer-events', 'all');
d3.selectAll ('. fokuslinje') .stil ('fyld', 'ingen');
d3.selectAll ('. fokuslinje'). stil ('slag', '# 67809f');
d3.selectAll ('. fokuslinje'). stil ('slag-bredde', '1,5 px');
d3.selectAll ('. fokuslinje'). stil ('stroke-dasharray', '3 3');

Korshåret består af en gennemskinnelig cirkel med dråbe linjer bestående af bindestreger. Ovenstående kodeblok giver stylingen af ​​de enkelte elementer. Ved overgang til generering vil det generere krydset baseret på nedenstående funktion.

const bisectDate = d3.bisector (d => d.date). venstre;
funktion generereCrosshair () {
  // returnerer den tilsvarende værdi fra domænet
  const matchingDate = xScale.invert (d3.mouse (dette) [0]);
  // får indsættelsespunkt
  const i = bisectDate (data, tilsvarende dato, 1);
  const d0 = data [i - 1];
  const d1 = data [i];
  const currentPoint = matchingDato - d0 ['dato']> d1 ['dato'] - tilsvarende dato? d1: d0;
  
  focus.attr ('transform', 'translate ($ {xScale (nuværendePoint [' dato '))}, $ {yScale (currentPoint [' close '])})');
fokus
  .Select (line.x ')
  .attr ('x1', 0)
  .attr ('x2', bredde - xSkala (nuværendePoint ['dato']))
  .attr ('y1', 0)
  .attr ('y2', 0);
fokus
  .Select (line.y ')
  .attr ('x1', 0)
  .attr ('x2', 0)
  .attr ('y1', 0)
  .attr ('y2', højde - yScale (currentPoint ['close']));
 updateLegends (currentPoint);
}

Vi kan derefter gøre brug af metoden d3.bisector () til at lokalisere indsættelsespunktet, der vil fremhæve det nærmeste datapunkt på den nære prislinjegraf. Efter bestemmelse af den nuværendePoint opdateres droplines. Metoden updateLegends () bruger currentPoint som parameter.

const updateLegends = currentData => {
  . D3.selectAll (. LineLegend «) fjerne ();
  const legendKeys = Object.keys (data [0]);
  const lineLegend = svg
    .selectAll ( 'lineLegend')
    .data (legendKeys)
    .gå ind()
    .append ( 'g')
    .attr ('klasse', 'lineLegend')
    .attr ('transform', (d, i) => {
      returner 'translate (0, $ {i * 20}) `;
    });
  lineLegend
    .append ( 'tekst')
    .text (d => {
      if (d === 'dato') {
        returner '$ {d}: $ {currentData [d] .toLocaleDateString ()}';
      } andet hvis (d === 'høj' || d === 'lav' || d === 'åben' || d === 'luk') {
        returner '$ {d}: $ {currentData [d] .toFixed (2)}';
      } andet {
        returner $ $ d}: $ {currentData [d]} `;
      }
    })
    .style ('fyld', 'hvid')
    .attr ('transform', 'oversætte (15,9)');
  };

Metoden updateLegends () opdaterer sagnet ved at vise dato, åben pris, luk pris, høj pris, lav pris og volumen på det valgte overgangspunkt på det nære linjegraf. I lighed med volumenstabeldiagrammerne bruger vi metoden selectAll () med metoderne enter () og append ().

For at gengive legenderne bruger vi.selectAll ('. LineLegend') til at vælge legenderne, efterfulgt af at kalde metoden remove () for at fjerne dem. Dernæst passerer vi tasterne til legenderne, legendKeys, som vil blive brugt til at definere højden på hver bjælke. Enter-metoden kaldes, som sammenligner volData-datasættet og ved markeringen fra selectAll (), som i øjeblikket er tom. I øjeblikket indeholder DOM ikke noget -element. Således accepterer append () -metoden et argument 'rekt', som skaber et nyt element i DOM for hvert enkelt objekt i volData.

Tilføj derefter legenderne med deres respektive egenskaber. Vi behandler værdierne yderligere ved at konvertere priserne til 2 decimaler. Vi indstiller også datoobjektet til standardlandet for læsbarhed.

Dette bliver slutresultatet:

Kontrolpunkt 4: Overflyt enhver del af diagrammet!

Lukende tanker

Tillykke! Du er nået slutningen af ​​denne tutorial. Som vist ovenfor er D3.js enkel, men alligevel dynamisk. Det giver dig mulighed for at oprette tilpassede visualiseringer til alle dine datasæt. I de kommende uger frigiver jeg den anden del af denne serie, som dybt dykker ned i D3.js 'enter-update-exit-mønster. I mellemtiden kan du tjekke API-dokumentationen, flere selvstudier og andre interessante visualiseringer bygget med D3.js.

Du er velkommen til at tjekke kildekoden såvel som den fulde demonstration af denne tutorial. Tak, og jeg håber, at du har lært noget nyt i dag!

Særlig tak til Debbie Leong for gennemgangen af ​​denne artikel.