Å bygge en Donut Diagram med Vue og SVG

0
10

Mmm… forbudt donut.”

– Homer Simpson

Jeg har nylig som trengs for å gjøre en bolle diagram for en rapporteringsmulighet på jobb. Mock-up som jeg fikk sett noe som dette:

Min diagram hadde et par grunnleggende kravene. Det er nødvendig for å:

  • Dynamisk beregne sine segmenter basert på et vilkårlig sett av verdier
  • Har etiketter
  • Skala godt på alle skjermstørrelser og enheter
  • Å være cross-browser kompatibel tilbake til Internet Explorer 11
  • Være tilgjengelig
  • Være gjenbrukbare over mitt arbeid er Vue.js front end

Jeg ønsket også noe om at jeg kunne animere senere hvis jeg trengte det. Alt dette hørtes ut som en jobb for SVG.

SVGs er tilgjengelig ut-av-boksen (W3C har en hel del på dette) og kan gjøres mer tilgjengelig gjennom ytterligere innspill. Og, fordi de er drevet av data, de er et perfekt kandidat for dynamisk visualisering.

Det er nok av artikler på emnet, inkludert to av Chris (her og her) og en super nylig en av Burke Holland. Jeg fikk ikke bruke D3 for dette prosjektet fordi søknaden ikke trenger overhead av at biblioteket.

Jeg opprettet dette diagrammet som en Vue komponent for prosjektet mitt, men du kan like enkelt gjøre dette med vanilje JavaScript, HTML og CSS.

Her er det ferdige produktet:

Se Penn Vue Donut Diagram – Siste Versjon av Salomone Baquis (@soluhmin) på CodePen.

Gjenoppfinne hjulet sirkel

Som enhver selvrespekt utvikler, det første jeg gjorde var å Google for å se om noen andre allerede hadde gjort dette. Da, som samme sa utvikler, jeg hugget pre-bygget løsning i favør av min egen.

Toppen hit for “SVG-donut-diagrammet” er denne artikkelen, som beskriver hvordan du bruker hjerneslag-dasharray og hjerneslag-dashoffset å trekke flere kledde sirkler og skape en illusjon av en enkelt segmentert sirkel (mer om dette snart).

Jeg liker overlay-konseptet, men fant å beregne både slag-dasharray og hjerneslag-dashoffset verdier forvirrende. Hvorfor ikke sette en fast hjerneslag-dasharrary verdi, og roter deretter hver sirkel med en forvandle? Jeg hadde også behov for å legge til etiketter på hvert segment, som ikke var dekket i opplæringen.

Tegne en linje

Før vi kan lage en dynamisk donut diagram, må vi først forstå hvordan SVG-linje tegning fungerer. Hvis du ikke har lest Jake Archibald er utmerket Animerte strektegning i SVG. Chris har også en god oversikt.

Disse artiklene gir de fleste av den sammenheng trenger du, men kort, SVG har to presentasjon attributter: slag-dasharray og hjerneslag-dashoffset.

hjerneslag-dasharray definerer en rekke bindestreker og mellomrom brukt for å teikne omrisset av en form. Det kan ta null, ett eller to verdier. Den første verdien definerer dash lengde; den andre definerer gapet lengde.

hjerneslag-dashoffset, på den annen side, definerer hvor den sett av bindestreker og mellomrom begynner. Hvis hjerneslag-dasharray og hjerneslag-dashoffset verdier er lengden av linjen og like, hele linjen er synlig fordi vi forteller offset (der dash-array starter) til å begynne på slutten av linjen. Hvis hjerneslag-dasharray er lengden av linjen, men den hjerneslag-dashoffset er 0, og deretter linjen er usynlig fordi vi er utligne gjengis en del av dashbordet ved hele sin lengde.

Chris’ eksempel demonstrerer dette pent:

Se Pen Grunnleggende Eksempel på SVG-Linje Tegning, Bakover og Fremover ved Chris Coyier (@chriscoyier) på CodePen.

Hvordan vil vi bygge kartet

For å skape smultring diagram segmenter, vil vi lage en egen sirkel for hver og en, legge den sirkler på toppen av hverandre, og deretter bruke slag, slag-dasharray, og slag-dashoffset for å vise bare en del av det slag i hver sirkel. Vi vil deretter rotere hver synlig del i riktig posisjon, skape illusjonen av en enkelt form. Når vi gjør dette, vil vi også beregne koordinatene for tekst etiketter.

Her er et eksempel som viser disse rotasjoner og maler:

Se Pen Sirkel Verdier ved Salomone Baquis (@soluhmin) på CodePen.

Grunnleggende oppsett

La oss starte med å sette opp vår struktur. Jeg bruker x-mal for demo formål, men jeg vil anbefale å lage en enkelt fil komponent for produksjon.

<div id=”app”>
<donut-diagram></donut-diagram>
</div>
<script type=”text/x-mal” id=”donutTemplate”>
<svg-height=”160″ width=”160″ viewBox=”0 0 160 160″>
<g v=”(verdi, indeks) i initialValues”>
<sirkel :cx=”cx” :cy=”cy” :r=”radius” fylle=”transparent” :slag=”farger[indeks]” :slag-bredde=”strokeWidth” ></sirkel>
<text></text>
</g>
</svg>
</script>
Vue.komponent(‘donutChart’, {
mal: ‘#donutTemplate’,
rekvisittar: [“initialValues”],
data() {
tilbake {
chartData: [],
farger: [“#6495ED”, “gullris”, “#cd5c5c”, “thistle”, “lightgray”],
cx: 80,
cy: 80,
radius: 60,
sortedValues: [],
strokeWidth: 30,
}
}
})
nye Vue({
el: “#app”,
data() {
tilbake {
verdier: [230, 308, 520, 130, 200]
}
},
});

Med dette kan vi:

  • Lage vår Vue eksempel og vår donut diagram komponent, deretter fortelle våre donut-komponent til å forvente noen verdier (vårt datasett) som rekvisitter
  • Etablere vår grunnleggende SVG-former: <sirkel> for de segmenter og <tekst> for etiketter med de grunnleggende dimensjoner, strekbredde, og fargene som er definert
  • Pakk disse figurene i en <g> – element, som dem sammen grupper
  • Legg til en v-for-løkke til g> – element, som vi skal bruke til å gå gjennom hver verdi som den komponenten som mottar
  • Opprett en tom sortedValues array, som vi skal bruke til å holde en sortert versjon av våre data
  • Opprett en tom chartData utvalg, som vil inneholde våre viktigste posisjonering data

Sirkel lengde

Vårt slag-dasharray bør være lengden av hele sirkelen, gi oss en enkel baseline antall som vi kan bruke til å beregne hvert slag-dashoffset verdi. Husker at lengden av en sirkel er dens omkrets og formelen for omkretsen er 2nr (du husker dette, ikke sant?).

Vi kan gjøre dette til en beregnet eiendom i våre komponent.

beregnet: {
omkrets() {
return 2 * Matematikk.PI * dette.radius
}
}

…og binde verdi til våre mal markup.

<svg-height=”160″ width=”160″ viewBox=”0 0 160 160″>
<g v=”(verdi, indeks) i initialValues”>
<sirkel :cx=”cx” :cy=”cy” :r=”radius” fylle=”transparent” :slag=”farger[indeks]” :slag-bredde=”strokeWidth” :slag-dasharray=”omkrets” ></sirkel>
<text></text>
</g>
</svg>

I den første mockup, så vi at de segmentene som gikk fra største til minste. Vi kan lage en annen beregnet eiendom for å sortere disse. Vi vil lagre sortert versjon inni sortedValues utvalg.

sortInitialValues() {
gå tilbake til denne.sortedValues = dette.initialValues.sorter((a,b) => b-a)
}

Til slutt, i rekkefølge for disse sortert verdier til å være tilgjengelig til Vue før diagrammet blir gjengitt, og vi vil referere til denne beregnede eiendom fra en som er montert() lifecycle kroken.

montert() {
dette.sortInitialValues
}

Akkurat nå, i vår diagrammet ser ut som dette:

Se Pen Bolle Diagram – Ingen Segmenter av Salomone Baquis (@soluhmin) på CodePen.

Ingen segmenter. Bare en solid-farget donut. Som HTML, SVG elementene er gjengitt i den rekkefølgen de vises i markeringen. Fargen som vises, er fargen på det siste sirkel i SVG. Fordi vi ikke har lagt til noen slag-dashoffset verdier ennå, hver sirkel er hjerneslag går hele veien rundt. La oss løse dette ved å opprette segmenter.

Opprette segmenter

For å få hver av sirkelen segmenter, trenger vi å:

  1. Beregne hvor stor andel av hver dataverdi fra den totale data verdier som vi passere i
  2. Multipliser dette prosentandel av omkretsen til å få den lengden av den synlige hjerneslag
  3. Trekk fra denne lengden fra omkretsen til å få hjerneslag-offset

Det høres mer komplisert ut enn det er. La oss starte med noen helper funksjoner. Vi må først totalt opp dataene våre verdier. Vi kan bruke en beregnet eiendom å gjøre dette.

dataTotal() {
gå tilbake til denne.sortedValues.redusere((acc, val) => acc + val)
},

For å beregne hvor stor andel av hver dataverdi, vi trenger for å passere i verdier fra v-for-løkke, som vi har opprettet tidligere, noe som betyr at vi må legge til en metode.

metoder: {
dataPercentage(dataVal) {
tilbake dataVal / dette.dataTotal
}
},

Vi har nå nok informasjon til å beregne vårt slag-offset verdier, som vil etablere vår sirkel segmenter.

Igjen, vi ønsker å: (a) multiplisere våre data prosentandel av circle omkrets for å få den lengden av den synlige slag, og (b) trekke denne lengden fra omkretsen til å få hjerneslag-offset.

Her er metoden for å få vårt slag-kvoter:

calculateStrokeDashOffset(dataVal, omkrets) {
const strokeDiff = dette.dataPercentage(dataVal) * omkrets
tilbake omkrets – strokeDiff
},

…som vi binder seg til vår sirkel i HTML-med:

:slag-dashoffset=”calculateStrokeDashOffset(verdi, omkrets)”

Og nå! Vi bør ha noe sånt som dette:

Se Pen Bolle Diagram – Ingen-Rotasjoner av Salomone Baquis (@soluhmin) på CodePen.

Roterende segmenter

Nå er den morsomme delen. Alle segmenter begynner på 3 o ‘ clock, som er standard utgangspunkt for SVG-kretser. Å få dem på rett sted, vi trenger å rotere hvert segment til riktig posisjon.

Vi kan gjøre dette ved å finne hver del er forholdet ut av 360 grader og deretter utlignet at beløp av den totale grader som kom før den.

Først, la oss legge til en data-egenskapen til å holde styr på offset:

angleOffset: -90,

Så vår beregning (dette er en beregnet eiendel):

calculateChartData() {
dette.sortedValues.forEach((dataVal, index) => {
const data = {
grader: dette.angleOffset,
}
dette.chartData.trykk(data)
dette.angleOffset = dette.dataPercentage(dataVal) * 360 + dette.angleOffset
})
},

Hver sløyfe oppretter et nytt objekt med en “grader” eiendom, presser det inn i vår chartValues array at vi har opprettet tidligere, og oppdaterer deretter angleOffset for neste loop.

Men vent, hva er opp med -90 verdi?

Vel, ser tilbake på vår opprinnelige mockup, det første segmentet er vist på 12 klokken posisjon, eller -90 grader fra startpunktet. Ved å sette våre angleOffset på -90, sikrer vi at vår største donut segment starter fra toppen.

For å rotere disse segmentene i HTML-koden, vil vi bruke forvandle presentasjon attributt med vri-funksjonen. La oss opprette en beregnet eiendom, slik at vi kan returnere en fin, formatert streng.

returnCircleTransformValue(indeks) {
tilbake rotere(${dette.chartData[index].grader}, ${dette.cx}, ${dette.cy})`
},

Roter funksjonen tar tre argumenter: en vinkel som rotasjon og x-og y-koordinater rundt som vinkelen roterer. Hvis vi ikke leverer cx og cy-koordinater, så våre segmenter vil rotere rundt hele SVG-koordinatsystemet.

Neste, vi binder dette til våre sirkel markup.

:forvandle=”returnCircleTransformValue(indeks)”

Og, siden vi trenger å gjøre alle disse beregninger før diagrammet er gjort, vil vi legge til vår calculateChartData beregnet eiendom i montert krok:

montert() {
dette.sortInitialValues
dette.calculateChartData
}

Til slutt, hvis vi ønsker at søt, søt gapet mellom hvert segment, kan vi trekke to fra omkrets, og bruke dette som vår nye hjerneslag-dasharray.

adjustedCircumference() {
gå tilbake til denne.omkrets – 2
},
:slag-dasharray=”adjustedCircumference”

Segmenter, baby!

Se Pen Bolle Diagram – Segmenter Bare ved Salomone Baquis (@soluhmin) på CodePen.

Etiketter

Vi har våre segmenter, men nå trenger vi for å lage etiketter. Dette betyr at vi trenger å plassere vår <text> – elementer med x-og y-koordinater på ulike punkter langs sirkelen. Du kan mistenke at dette krever matematikk. Dessverre, du er riktige.

Heldigvis, dette er ikke den type matematikk der vi må bruke Ekte Konsepter; dette er mer den typen der vi Google formler og ikke stille for mange spørsmål.

I henhold til Internett, formler for å beregne x-og y-punkter langs en sirkel er:

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

…der r er radius, t er den vinkel, og a og b er x og y midtpunktet av kvoter.

Vi har allerede de fleste av dette: vi vet at våre radius, vi vet hvordan vi skal beregne vårt segment vinkler, og vi vet at våre center offset verdier (cx og cy).

Det er en fange, men i disse formlene, t er i *radianer*. Vi jobber i grader, noe som betyr at vi trenger å gjøre noen konverteringer. Igjen, et raskt søk dukker opp en formel:

radianer = grader * (π / 180)

…som vi kan representere i en metode:

degreesToRadians(vinkel) {
tilbake vinkel * (Matematikk.PI / 180)
},

Vi har nå nok informasjon til å beregne vår x-og y-tekst-koordinater:

calculateTextCoords(dataVal, angleOffset) {
const vinkel = (denne.dataPercentage(dataVal) * 360) / 2 + angleOffset
const radianer = dette.degreesToRadians(vinkel)

const textCoords = {
x: (denne.radius * Matematikk.cos(radianer) + dette.cx),
y: (denne.radius * Matematikk.synd(radianer) + dette.cy)
}
tilbake textCoords
},

For det første kan vi beregne vinkelen på vårt segment ved å multiplisere forholdet mellom våre data verdi av 360, men vi vil faktisk halvparten av dette fordi vår tekst etiketter er i midten av segmentet snarere enn slutten. Vi må legge til vinkelen offset som vi gjorde da vi opprettet segmentene.

Våre calculateTextCoords metoden kan nå brukes i calculateChartData beregnet eiendom:

calculateChartData() {
dette.sortedValues.forEach((dataVal, index) => {
const { x, y } = dette.calculateTextCoords(dataVal dette.angleOffset)
const data = {
grader: dette.angleOffset,
textX: x,
textY: y
}
dette.chartData.trykk(data)
dette.angleOffset = dette.dataPercentage(dataVal) * 360 + dette.angleOffset
})
},

La oss også legge til en metode for å gå tilbake til etiketten streng:

percentageLabel(dataVal) {
tilbake `${Math.runde(denne.dataPercentage(dataVal) * 100)}%`
},

Og, i markeringen:

<tekst :x=”chartData[index].textX” :y=”chartData[index].textY”>{{ percentageLabel(verdi) }}</text>

Nå har vi fått etiketter:

Se Pen Bolle Diagram – Formatert Etiketter ved Salomone Baquis (@soluhmin) på CodePen.

Blech, så off-center. Vi kan løse dette med tekst-anker presentasjon attributtet. Avhengig av skrift og skrift-størrelse, kan du justere plasseringen som godt. Sjekk ut dx og dy for dette.

Fornyet tekst-element:

<tekst-anker=”middle” dy=”3px” :x=”chartData[index].textX” :y=”chartData[index].textY”>{{ percentageLabel(verdi) }}</text>

Hmm, det ser ut som om vi har små prosenter, etiketter gå på utsiden av segmenter. La oss legge til en metode for å kontrollere for dette.

segmentBigEnough(dataVal) {
tilbake Matematikk.runde(denne.dataPercentage(dataVal) * 100) > 5
}
<tekst v-hvis=”segmentBigEnough(verdi)” tekst-anker=”middle” dy=”3px” :x=”chartData[index].textX” :y=”chartData[index].textY”>{{ percentageLabel(verdi) }}</text>

Nå, vi vil bare legge til etiketter på segmenter som er større enn 5%.

Og vi er ferdig! Vi har nå et gjenbrukbart donut diagram komponent som kan akseptere ethvert sett av verdier og opprette segmenter. Super kul!

Det ferdige produktet:

Se Penn Vue Donut Diagram – Siste Versjon av Salomone Baquis (@soluhmin) på CodePen.

Neste trinn

Det er mange måter at vi kan endre eller forbedre dette nå som det er bygget. For eksempel:

  • Legge til elementer for å forbedre tilgjengelighet, for eksempel <title> og <desc> – kodene, aria-etiketter og aria rolle attributter.
  • Å lage animasjoner med CSS eller biblioteker som Greensock å lage iøynefallende effekter når diagrammet kommer til syne.
  • Å spille med fargevalg.

Jeg vil gjerne høre hva du synes om denne implementeringen og andre opplevelser du har hatt med SVG-diagrammer. Dele i kommentarfeltet!

Jetpack WordPress plugin går på dette nettstedet, slår ikke bare relaterte innlegg nedenfor, men den sosiale deling av koblinger ovenfor, sikkerhet og backup, Markdown-støtte, søk nettstedet, kommentar skjemaet, positing til sosiale nettverk-tilkoblinger, og mer!