Het bouwen van een Donut Grafiek met Vue en de SVG

0
16

Mmm… verboden donut.”

– Homer Simpson

Onlangs heb ik nodig om een donut grafiek voor een rapportage-dashboard op het werk. De mock-up die ik kreeg zag iets als dit:

Mijn kaart had een paar fundamentele eisen. Is het nodig om:

  • Dynamisch berekenen van de segmenten op basis van een willekeurige set van waarden
  • Etiketten
  • Schaal goed in alle schermformaten en apparaten
  • Cross-browser compatible terug naar Internet Explorer 11
  • Toegankelijk
  • Herbruikbaar zijn over mijn werk Vue.js front-end

Ik wilde ook iets dat ik kon animeren later als ik nodig. Dit alles klonk als een taak voor SVG.

SVGs toegankelijk zijn out-of-the-box (het W3C heeft een hele sectie op deze) en beter toegankelijk gemaakt kan worden door middel van extra ingang. En, omdat ze worden aangedreven door data, ze zijn een perfecte kandidaat voor dynamische visualisatie.

Er zijn tal van artikelen over het onderwerp, waaronder twee door Chris (hier en hier) en een super recente één van Burke Holland. Ik heb geen gebruik D3 voor dit project, omdat de applicatie niet nodig de overhead van de bibliotheek.

Ik heb de grafiek als een Vue-component voor mijn project, maar kan je dit gemakkelijk doen met vanille JavaScript, HTML en CSS.

Hier is het eindproduct:

Zie de Pen Vue Donut Grafiek – Laatste Versie door Salomone Baquis (@soluhmin) op CodePen.

Opnieuw het wiel uitvinden cirkel

Zoals elke zichzelf respecterende ontwikkelaar, het eerste wat ik deed was Google om te zien als iemand anders het al had gemaakt. Vervolgens, zoals dezelfde zegt ontwikkelaar, ik gesloopt van de pre-built oplossing in het voordeel van mijn eigen.

De top hit voor “SVG-donut grafiek” is dit artikel wordt beschreven hoe u een beroerte-dasharray en beroerte-dashoffset te tekenen meerdere overtrok kringen en creëren de illusie van een enkele gesegmenteerde cirkel (meer hierover binnenkort).

Ik hou echt van de overlay-concept, maar vond het herberekenen van zowel slag-dasharray en beroerte-dashoffset waarden verwarrend. Waarom niet één vaste stroke-dasharrary waarde en draai vervolgens elke cirkel met een transformatie? Ik moest ook labels toevoegen aan elk segment, die werd niet behandeld in de les.

Het tekenen van een lijn

Voordat we maken een dynamische donut grafiek, moeten we eerst begrijpen hoe SVG-lijn tekenen werken. Als u nog niet lezen Jake Archibald ‘ s uitstekende Geanimeerde Tekening in SVG. Chris heeft ook een goed overzicht.

Deze artikelen bieden de meeste van de context die je nodig hebt, maar kort, SVG heeft twee presentatie kenmerken: stroke-dasharray en beroerte-dashoffset.

beroerte-dasharray definieert een reeks streepjes en gaten worden gebruikt voor het schilderen van de omtrek van een vorm. Het kan nul, één of twee waarden. De eerste waarde definieert de dash lengte; de tweede bepaalt de lengte van de tussenruimten.

beroerte-dashoffset, aan de andere kant, bepaalt waar de set van streepjes en hiaten begint. Als de stroke-dasharray en de stroke-dashoffset waarden zijn de lengte van de lijn en gelijk, de gehele lijn is zichtbaar, want we vertellen de offset (waarbij het dash-array start) om te beginnen aan het einde van de lijn. Als de stroke-dasharray is de lengte van de lijn, maar de lijn-dashoffset 0 is, dan is de lijn is onzichtbaar omdat we het compenseren van de verrichte deel van de race door de hele lengte.

Chris’ voorbeeld illustreert dit mooi:

Zie de Pen eenvoudig Voorbeeld van het SVG-Lijn Tekenen, Achteruit en Vooruit door Chris Coyier (@chriscoyier) op CodePen.

Hoe bouwen we de grafiek

Voor het maken van de donut grafiek segmenten, dan maken we er een aparte kring voor elk, met een overlay van de cirkels op de top van een ander, dan gebruik slag, slag-dasharray, en beroerte-dashoffset om slechts een deel van de slag van elke cirkel. Vervolgens zullen We draaien elke zichtbare gedeelte in de juiste positie, het creëren van de illusie van een enkele vorm. Als we dat doen, kunnen we ook berekenen van de coördinaten voor de tekstlabels.

Hier is een voorbeeld dat aantoont dat deze rotaties en overlays:

Zie de Pen Cirkel Overlays door Salomone Baquis (@soluhmin) op CodePen.

Basic setup

Laten we beginnen met het opzetten van de structuur. Ik ben met behulp van x-sjabloon voor demo doeleinden, maar ik zou aanraden het maken van een enkel bestand component voor de productie.

<div id=”app”>
<donut-diagram></donut-diagram>
</div>
<script type=”text/x-sjabloon” id=”donutTemplate”>
<svg-height=”160″ width=”160″ viewBox=”0 0 160 160″>
<g v-for=”(value, index) in initialValues”>
<circle cx=”cx” :cy=”cy” :r=”radius” fill=”transparent” :stroke=”kleuren[index]” :stroke-width=”strokeWidth” ></cirkel>
<text></text>
</g>
</svg>
</script>
Vue.component(‘donutChart’, {
sjabloon: ‘#donutTemplate’,
rekwisieten: [“initialValues”],
gegevens() {
terug {
grafiek gegevens: [],
kleuren: [“#6495ED”, “guldenroede”, “#cd5c5c”, “de distel”, “lightgray”],
cx: 80,
cy: 80,
straal: 60,
sortedValues: [],
strokeWidth: 30,
}
}
})
nieuwe Vue({
el: “#app”,
gegevens() {
terug {
waarden: [230, 308, 520, 130, 200]
}
},
});

Met deze, we:

  • Onze Vue-exemplaar en onze donut diagramonderdeel, dan vertellen onze donut onderdeel te verwachten dat een aantal waarden (onze dataset) als rekwisieten
  • Opzetten van onze basic SVG-vormen: <cirkel> voor de segmenten en <tekst> voor de labels, met de basis afmetingen, breedte streek en de kleuren zijn gedefinieerd
  • Wikkel deze vormen in een <g> – element, welke groepen ze samen
  • Het toevoegen van een v-voor de lus om de g> – element, die we gebruiken om uitgevoerd te worden door elke waarde die de component ontvangt
  • Maak een lege sortedValues array, die we gebruiken om een gesorteerde versie van onze gegevens
  • Maak een lege grafiek gegevens array bevat onze belangrijkste plaatsing van gegevens

Cirkel, lengte

Onze stroke-dasharray moet de lengte van de hele cirkel, geven ons een eenvoudige baseline aantal die we kunnen gebruiken voor het berekenen van elke slag-dashoffset waarde. Herinner u dat de lengte van een cirkel is de omtrek en de formule voor de omtrek is 2nr (je onthoud dit, toch?).

Kunnen We dit van een berekende goederen in onze component.

berekend: {
omtrek() {
return 2 * Wiskunde.PI *.straal
}
}

…en binden van de waarde aan onze template opmaak.

<svg-height=”160″ width=”160″ viewBox=”0 0 160 160″>
<g v-for=”(value, index) in initialValues”>
<circle cx=”cx” :cy=”cy” :r=”radius” fill=”transparent” :stroke=”kleuren[index]” :stroke-width=”strokeWidth” :stroke-dasharray=”omtrek” ></cirkel>
<text></text>
</g>
</svg>

In de eerste mock-up zagen we dat de segmenten ging van de kleinste tot de grootste. We kunnen nog berekend goederen te sorteren. We slaan de gesorteerde versie in de sortedValues array.

sortInitialValues() {
retourneren.sortedValues = dit.initialValues.sorteer (a,b) => b-a)
}

Eindelijk, om voor deze gesorteerde waarden beschikbaar Vue voor de grafiek wordt weergegeven, we willen verwijzen naar deze berekend eigendom van de gemonteerde() levenscyclus van de haak.

gemonteerd() {
deze.sortInitialValues
}

Nu, onze diagram er als volgt uitzien:

Zie de Pen Donut Grafiek – Geen Segmenten door Salomone Baquis (@soluhmin) op CodePen.

Geen segmenten. Gewoon een effen donut. Zoals HTML, SVG elementen worden weergegeven in de volgorde waarin ze worden weergegeven in de markup. De kleur die wordt weergegeven is de lijnkleur van de laatste cirkel in de SVG. Omdat we nog niet toegevoegd stroke-dashoffset waarden nog, elke cirkel de slag gaat helemaal rond. Laten we dit oplossen door het maken van segmenten.

Het maken van segmenten

Aan elk van de cirkel segmenten, moeten we:

  1. De berekening van het percentage van elk data-waarde van het totaal van de waarden die we passeren in
  2. Vermenigvuldig dit percentage door de omtrek te krijgen van de lengte van de zichtbare penseelstreek
  3. Aftrekken is dit de lengte van de omtrek van de cirkel te krijgen van de beroerte-verschuiving

Het klinkt ingewikkelder dan het is. Laten we beginnen met een helper functies. Eerst moeten We tot onze waarden. We kunnen gebruik maken van een berekend eigendom om dit te doen.

dataTotal() {
retourneren.sortedValues.verminderen((acc, val) => acc + val)
},

Voor het berekenen van het percentage van elke gegevenswaarde, we nodig hebben om in de waarden van de v-for-lus die we eerder hebt gemaakt, wat betekent dat we nog een methode toevoegen.

methoden: {
dataPercentage(dataVal) {
terug dataVal /.dataTotal
}
},

We hebben nu voldoende informatie om te berekenen onze stroke-offset waarden, die zal zorgen voor onze cirkel segmenten.

Nogmaals, we willen is om: (a) te vermenigvuldigen onze gegevens percentage door de cirkel omtrek te krijgen van de lengte van de zichtbare lijn en (b) aftrekken van de lengte van de omtrek van de cirkel te krijgen van de beroerte-offset.

Hier is de methode om onze stroke-offsets:

calculateStrokeDashOffset(dataVal, omtrek) {
const strokeDiff = dit.dataPercentage(dataVal) * omtrek
terug omtrek – strokeDiff
},

…die we binden onze kring in de HTML:

:slag-dashoffset=”calculateStrokeDashOffset(waarde, omtrek)”

En voilà! We moeten iets als dit:

Zie de Pen Donut Grafiek – Geen Rotaties door Salomone Baquis (@soluhmin) op CodePen.

Roterende segmenten

Nu het leuke gedeelte. Alle segmenten beginnen om 3 uur, dat is het standaard uitgangspunt voor SVG-kringen. Om ze te krijgen op de juiste plaats, we moeten draaien elk segment de juiste positie.

Dit kunnen We doen door het vinden van een ieder segment van de verhouding van 360 graden en dan gecompenseerd dat bedrag te delen door het totaal graden die ervoor kwam.

Laten we eerst het toevoegen van een data-eigendom te houden van de compensatie:

angleOffset: -90,

Dan is onze berekening (dit is een berekende eigendom):

calculateChartData() {
deze.sortedValues.forEach((dataVal, index) => {
const data = {
graden: dit.angleOffset,
}
deze.grafiek gegevens.push(gegevens)
deze.angleOffset = dit.dataPercentage(dataVal) * 360 +.angleOffset
})
},

Elke lus maakt een nieuw object met een “graden” goederen, duwt die in onze chartValues matrix die we eerder hebt gemaakt en vervolgens de updates van de angleOffset voor de volgende lus.

Maar wacht, wat is er met de -90 waarde?

Goed, kijkt terug op onze originele mock-up, de eerste segment wordt weergegeven op de 12 uur positie, of -90 graden vanaf het startpunt. Door onze angleOffset op -90, wij zorgen ervoor dat onze grootste donut segment begint vanaf de top.

Te draaien van deze segmenten in de HTML-code gebruiken we de transformatie presentatie attribuut met de functie van de rotatie. We maken nog een berekend woning, zodat we kunnen terug een mooie, geformatteerde string.

returnCircleTransformValue(index) {
terug draaien(${dit.grafiek gegevens[index].graden}, ${dit.cx}, ${dit.cy})`
},

Het draaien functie heeft drie argumenten: een hoek van de rotatie en de x-en y-coördinaten van de hoek draait. Als we niet leveren cx en cy coördinaten, dan is onze segmenten zal draaien rond de hele SVG-systeem voor coördinaten.

Volgende, binden wij dit om onze cirkel markup.

:transform=”returnCircleTransformValue(index)”

En, omdat we er alles aan moeten doen van deze berekeningen voor de grafiek wordt weergegeven, voegen we onze calculateChartData berekend eigendom in de haak gemonteerd:

gemonteerd() {
deze.sortInitialValues
deze.calculateChartData
}

Ten slotte, als we willen dat zoete, zoete kloof tussen elk segment, we kunnen aftrekken van twee van de omtrek en dit gebruiken als onze nieuwe stroke-dasharray.

adjustedCircumference() {
retourneren.omtrek – 2
},
:slag-dasharray=”adjustedCircumference”

Segmenten, baby!

Zie de Pen Donut Grafiek – Segmenten Alleen door Salomone Baquis (@soluhmin) op CodePen.

Etiketten

We hebben onze segmenten, maar nu moeten we om etiketten te maken. Dit betekent dat we nodig hebben om onze <tekst> – elementen met x-en y-coördinaten op verschillende punten langs de cirkel. Je zou kunnen vermoeden dat dit vereist wiskunde. Helaas, je hebt gelijk.

Gelukkig, dit is niet de aard van de wiskunde waar we moeten toepassen Echte Concepten; dit is meer het soort waar we Google formules en vraag niet te veel vragen.

Volgens het Internet, de formules voor de berekening van x-en y-punten langs een cirkel zijn:

x = r cos(t) + a
y = r sin(t) + b

…waarin r de straal is t de hoek, en a en b zijn de x-en y middelpunt offsets.

We hebben al de meeste van deze: we weten dat onze straal, we weten hoe we voor het berekenen van ons segment hoeken, en we weten dat onze center offset waarden (cx en cy).

Er is één addertje onder het gras: in deze formules, t is in *radialen*. We werken in de graden, dat betekent dat we moeten doen wat omzettingen. Weer een snelle zoekactie blijkt een formule:

radialen = graden * (π / 180)

…die we kunnen vertegenwoordigen in een methode:

degreesToRadians(hoek) {
terug hoek * (Math.PI / 180)
},

We hebben nu voldoende informatie voor het berekenen van de x-en y-tekst coördinaten:

calculateTextCoords(dataVal, angleOffset) {
const hoek = (.dataPercentage(dataVal) * 360) / 2 + angleOffset
const radialen = dit.degreesToRadians(hoek)

const textCoords = {
x: (.radius * Math.cos(radians) +.cx),
y: (.radius * Math.sin(radialen) +.cy)
}
terug textCoords
},

We berekenen eerst de hoek van ons segment door vermenigvuldiging met de verhouding van onze gegevens waarde door 360; echter, wij willen eigenlijk de helft van dit omdat onze teksten zijn in het midden van het segment eerder dan het einde. Moeten We de hoek offset, zoals we deden toen hebben we de segmenten.

Onze calculateTextCoords methode kan nu gebruikt worden in de calculateChartData berekend eigendom:

calculateChartData() {
deze.sortedValues.forEach((dataVal, index) => {
const { x, y } = dit.calculateTextCoords(dataVal, dit.angleOffset)
const data = {
graden: dit.angleOffset,
textX: x,
textY: y
}
deze.grafiek gegevens.push(gegevens)
deze.angleOffset = dit.dataPercentage(dataVal) * 360 +.angleOffset
})
},

Laten we ook een methode om terug te keren van het label tekenreeks:

percentageLabel(dataVal) {
return `${Math.ronde(deze.dataPercentage(dataVal) * 100)}%`
},

En, in de markup:

<text x=”grafiek gegevens[index].textX” :y=”grafiek gegevens[index].textY”>{{ percentageLabel(waarde) }}</text>

Nu hebben we de labels:

Zie de Pen Donut Grafiek – niet-opgemaakte Labels door Salomone Baquis (@soluhmin) op CodePen.

Blech, dus off-center. We kunnen dit oplossen door de text-anchor presentatie kenmerk. Afhankelijk van het lettertype en tekengrootte, mag u wilt aanpassen van de positionering. Check out dx en dy.

Vernieuwde tekst element:

<text-anchor=”middle” dy=”3px” :x=”grafiek gegevens[index].textX” :y=”grafiek gegevens[index].textY”>{{ percentageLabel(waarde) }}</text>

Hmm, het lijkt erop dat als we de kleine percentages, de labels naar buiten gaan van de segmenten. Nu nog een methode om te controleren of deze.

segmentBigEnough(dataVal) {
terug Math.ronde(deze.dataPercentage(dataVal) * 100) > 5
}
<tekst v-als=”segmentBigEnough(waarde)” text-anchor=”middle” dy=”3px” :x=”grafiek gegevens[index].textX” :y=”grafiek gegevens[index].textY”>{{ percentageLabel(waarde) }}</text>

Nu, we zullen alleen labels toevoegen aan de segmenten groter is dan 5%.

En we zijn klaar! We hebben nu een herbruikbare donut diagramonderdeel kan het accepteren van een set van waarden en het maken van segmenten. Super cool!

Het eindproduct:

Zie de Pen Vue Donut Grafiek – Laatste Versie door Salomone Baquis (@soluhmin) op CodePen.

Volgende stappen

Er zijn tal van manieren die we kunnen veranderen of verbeteren dit nu dat het gebouwd is. Bijvoorbeeld:

  • Het toevoegen van elementen voor het verbeteren van de toegankelijkheid, zoals de <title> en <desc> tags, aria-etiketten en aria rol attributen.
  • Het maken van animaties met CSS of bibliotheken als Greensock te maak in het oog springende effecten wanneer de grafiek in beeld komt.
  • Spelen met kleuren schema ‘ s.

Ik zou graag horen wat u denkt over deze uitvoering en andere ervaringen die je hebt gehad met SVG-diagrammen. Delen in de comments!

De Jetpack WordPress plugin draait op deze site, het voeden niet alleen de gerelateerde berichten hieronder, maar de social sharing links boven, beveiliging en back-ups, Markdown ondersteuning, site search, het reactieformulier, positionering, sociale netwerk-verbindingen, en meer!