Att bygga ett ringdiagram med Vue och SVG

0
14

Mmm… förbjudet munk.”

– Homer Simpson

Jag har nyligen behövs för att göra ett ringdiagram för en redovisning på instrumentpanelen arbete. Den mock-up som jag fick såg något liknande detta:

Mitt schema hade några grundläggande krav. Det som behövs för att:

  • Dynamiskt beräkna sitt segment baserat på en godtycklig uppsättning av värden
  • Har etiketter
  • Skala och i alla skärmstorlekar och enheter
  • Vara webbläsar-kompatibla tillbaka till Internet Explorer 11
  • Att vara tillgänglig
  • Att vara återanvändbara över mitt arbete Vue.js front end

Jag också ville ha något som jag kan animera senare om jag behövde. Allt detta lät som ett jobb för SVG.

SVGs är tillgänglig out-of-the-box (W3C har ett helt avsnitt om detta) och kan göras mer tillgänglig genom ytterligare synpunkter. Och eftersom de är drivna av data, de är en perfekt kandidat för dynamisk visualisering.

Det finns massor av artiklar i ämnet, varav två av Chris (här och här) och en super nyligen en av Burke Holland. Jag använder inte D3 för detta projekt eftersom ansökan inte behöver omkostnader för biblioteket.

Jag har skapat diagrammet som en Vue komponent för mitt projekt, men du kan lika lätt göra detta med vanilj JavaScript, HTML och CSS.

Här är den färdiga produkten:

Se Pennan Vue ringdiagram – Final Version av Salomone Baquis (@soluhmin) på CodePen.

Att uppfinna hjulet på nytt cirkel

Precis som någon självrespekt utvecklare, det första jag gjorde var att Google för att se om någon annan redan hade gjort detta. Sedan, som sade samma utvecklare, jag skrotade den färdiga lösningen till förmån för min egen.

Den översta träffen för “SVG ringdiagram” är denna artikel som beskriver hur man använder stroke-dasharray och stroke-dashoffset att rita flera överdrog cirklar och skapa illusionen av en enda segmenterade cirkel (mer om detta strax).

Jag gillar verkligen det överlägg som begrepp, men fann att räkna både stroke-dasharray och stroke-dashoffset värden förvirrande. Varför inte ställa en fastställd stroke-dasharrary värde och rotera sedan varje cirkel med en transformering? Jag behövde också att lägga till etiketter till varje segment, som inte var täckt i handledningen.

Rita en linje

Innan vi kan skapa en dynamisk ringdiagram, måste vi först förstå hur SVG-linje rita fungerar. Om du inte har läst Jake Archibald ‘ s utmärkta Animerade Raden Ritning i SVG. Chris har också en bra översikt.

Dessa artiklar ger de flesta av de sammanhang du behöver, men kortfattat, SVG har två presentation attribut: stroke-dasharray och stroke-dashoffset.

stroke-dasharray definierar en matris av streck och luckor som används för att måla konturerna av en form. Det kan ta noll, ett eller två värden. Det första värdet som definierar dash längd, den andra anger gap längd.

stroke-dashoffset, å andra sidan, definieras där uppsättning av streck och luckor börjar. Om stroke-dasharray och stroke-dashoffset värden är längden på linjen och lika i hela raden är synligt, eftersom vi talar offset (där dash-array börjar) för att börja i slutet av raden. Om stroke-dasharray är längden på linjen, men den stroke-dashoffset är 0, då linjen är osynliga på grund av att vi motverkar återges en del av streck genom hela dess längd.

Chris exempel visar att det är snyggt:

Se Pennan Grundläggande Exempel på SVG Raden Ritning, Bakåt och Framåt genom att Chris Coyier (@chriscoyier) på CodePen.

Hur vi ska bygga diagram

För att skapa ringdiagram är segment, vi ska göra en separat cirkel för var och en, overlay cirklar ovanpå varandra, för att sedan använda stroke, stroke-dasharray, och stroke-dashoffset att visa bara en del av stroke varje cirkel. Vi kommer då att rotera varje synlig del i rätt position, vilket skapar en illusion av en enda form. När vi gör det, vi kommer också att beräkna koordinaterna för textetiketter.

Här är ett exempel som visar dessa rotationer och överlägg:

Se Pennan Cirkel Överlägg av Salomone Baquis (@soluhmin) på CodePen.

Grundläggande inställningar

Låt oss börja med att sätta upp vår struktur. Jag använder x-mall för demosyfte, men jag skulle rekommendera att du skapar en enskild fil komponent för produktion.

<div id=”app”>
<donut-diagrammet></donut-diagrammet>
</div>
<script type=”text/x-mall” id=”donutTemplate”>
<svg height=”160″ width=”160″ viewBox=”0 0 160 160″>
<g v-for=”(värde index) i initialValues”>
<cirkel :cx=”cx” :cy=”cy” r=”radie” fylla=”transparent” :stroke=”färger[index]” :stroke-width=”strokeWidth” ></circle>
<text></text>
<g>
</svg>
</script>
Vue.komponent(‘donutChart’, {
mall: ‘#donutTemplate’,
rekvisita: [“initialValues”],
data() {
return {
chartData: [],
färger: [“#6495ED”, “gult”, “#cd5c5c”, “tistel”, “lightgray”],
cx: 80,
cy: 80,
radie: 60,
sortedValues: [],
strokeWidth: 30,
}
}
})
nya Vue({
el: “#app”,
data() {
return {
värderingar: [230, 308, 520, 130, 200]
}
},
});

Med detta, vi:

  • Skapa vår Vue exempel och våra ringdiagram komponent, sedan berätta för våra donut komponent att förvänta sig vissa värderingar (vårt dataset) som rekvisita
  • Etablera vår grundläggande SVG former: <cirkel> för segment och <text> för etiketter, med grundläggande dimensioner, linjebredd, och färger som definierats
  • Linda dessa former av <g> – elementet, som grupperar dem tillsammans
  • Lägg till en v-slinga till g> – elementet, som vi använder för att iterera igenom alla värden som den komponent som får
  • Skapa en tom sortedValues array, som vi ska använda för att hålla en sorterad version av vår data
  • Skapa en tom chartData array, som kommer att innehålla vår huvudsakliga placering av data

Cirkeln längd

Våra stroke-dasharray bör vara längden av hela cirkeln, vilket ger oss en enkel grundläggande nummer som vi kan använda för att beräkna varje slag dashoffset värde. Minns att längden av en cirkel är dess omkrets och formeln för omkretsen är 2nr (du minns det, eller hur?).

Vi kan göra detta till en beräknad egendom i vårt komponent.

beräknad: {
omkrets() {
return 2 * Matematik.PI * denna.radie
}
}

…och binda värde för vår template-kod.

<svg height=”160″ width=”160″ viewBox=”0 0 160 160″>
<g v-for=”(värde index) i initialValues”>
<cirkel :cx=”cx” :cy=”cy” r=”radie” fylla=”transparent” :stroke=”färger[index]” :stroke-width=”strokeWidth” :stroke-dasharray=”omkretsen” ></circle>
<text></text>
<g>
</svg>

I den inledande mockup, vi såg att de segment som gick från den största till den minsta. Kan vi göra en annan beräknade egendom att sortera dessa. Vi ska lagra sorterad version inuti sortedValues array.

sortInitialValues() {
tillbaka detta.sortedValues = detta.initialValues.typ (a,b) => b-a)
}

Slutligen, för att dessa sorteras värden ska vara tillgängliga för Vue innan diagrammet blir utförda, vi vill att referera till denna beräknade egendom från den monterade() lifecycle krok.

monterad() {
detta.sortInitialValues
}

Just nu, vår diagrammet ser ut så här:

Se Pennan ringdiagram – Inga Segment av Salomone Baquis (@soluhmin) på CodePen.

Inga segment. Bara en enfärgad donut. Som HTML, SVG delar återges i den ordning som de visas i koden. Den färg som visas är stroke färg av den sista cirkeln i SVG. Eftersom vi inte har lagt någon stroke-dashoffset värden men ändå, varje cirkel är stroke går hela vägen runt. Låt oss åtgärda det genom att skapa segment.

Skapa segment

För att få varje av cirkel-segment, vi kommer att behöva:

  1. Beräkna den procentuella andelen av varje värde från den totala värden som vi passerar i
  2. Multiplicera denna siffra med omkretsen för att få längden på den synliga stroke
  3. Subtrahera denna längd från omkrets för att få stroke-offset

Det låter mer komplicerat än det är. Låt oss börja med lite hjälp funktioner. Först måste vi totalt upp våra data värden. Vi kan använda ett beräknat bostad för att göra detta.

dataTotal() {
tillbaka detta.sortedValues.minska((acc, val) => acc + val)
},

För att beräkna den procentuella andelen av varje datavärde, vi kommer att behöva passera i värden från v-slinga som vi skapade tidigare, vilket innebär att vi behöver lägga till en metod.

metoder: {
dataPercentage(dataVal) {
tillbaka dataVal / denna.dataTotal
}
},

Vi har nu tillräckligt med information för att beräkna vår stroke-offset-värden, som kommer att utgöra vår cirkel-segment.

Återigen, vi vill att: (a) multiplicera våra data procent av cirkelns omkrets för att få längden på den synliga stroke, och (b) subtrahera denna längd från omkrets för att få stroke-offset.

Här är metoden för att få våra stroke-förskjutningar:

calculateStrokeDashOffset(dataVal, omkrets) {
const strokeDiff = detta.dataPercentage(dataVal) * omkrets
tillbaka omkrets – strokeDiff
},

…som vi binder till vår cirkel i HTML med:

:stroke-dashoffset=”calculateStrokeDashOffset(värde, omkrets)”

Och voilà! Vi borde ha något liknande detta:

Se Pennan ringdiagram – Inga Rotationer av Salomone Baquis (@soluhmin) på CodePen.

Roterande segment

Nu till den roliga biten. Alla segment börjar klockan 3, vilket är standard utgångspunkt för SVG cirklar. För att få dem på rätt plats, vi behöver för att rotera varje segment till sin rätta position.

Vi kan göra detta genom att hitta varje segment andel av 360 grader och sedan kompensera det belopp av den totala grader som kom före det.

Första, låt oss lägga till en data egendom för att hålla koll på offset:

angleOffset: -90,

Då vår beräkning (detta är ett beräknat egendom):

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

Varje slinga skapar ett nytt objekt med en “grader” egendom, skjuter in våra chartValues array som vi skapade tidigare, och uppdaterar sedan angleOffset för nästa slinga.

Men vänta, vad är upp med -90 värde?

Tja, tittar tillbaka på vår ursprungliga modell, det första segmentet visas i läget klockan 12, eller -90 grader från utgångspunkten. Genom att ställa våra angleOffset på -90, vi säkerställa att våra största donut segment börjar från toppen.

För att rotera dessa segment i HTML, vi kommer att använda förvandla presentationen attribut med rotera funktion. Låt oss skapa en annan beräknade egendom så att vi kan återvända och en trevlig, formaterad sträng.

returnCircleTransformValue(index) {
tillbaka rotera(${detta.chartData[index].grader}, ${detta.cx}, ${detta.cy})`
},

Rotera funktion som tar tre argument: en vinkel av rotation och x-och y-koordinater kring vilken vinkel roterar. Om vi inte levererar cx och cy koordinater, då våra segment kommer att rotera runt hela SVG-koordinatsystemet.

Därefter kommer vi att binda detta till vår krets markup.

:omvandla=”returnCircleTransformValue(index)”

Och, eftersom vi behöver för att göra alla dessa beräkningar innan diagrammet återges, vi kommer att lägga vår calculateChartData beräknas egendom i den monterad krok:

monterad() {
detta.sortInitialValues
detta.calculateChartData
}

Slutligen, om vi vill ha det sweet, sweet klyftan mellan varje segment kan vi subtrahera två från omkrets och använda detta som vår nya stroke-dasharray.

adjustedCircumference() {
tillbaka detta.omkrets – 2
},
:stroke-dasharray=”adjustedCircumference”

Segment, baby!

Se Pennan ringdiagram – Segment Endast av Salomone Baquis (@soluhmin) på CodePen.

Etiketter

Vi har våra segment, men nu behöver vi för att skapa etiketter. Detta innebär att vi behöver lägga vår <text> – element med x-och y-koordinater på olika punkter längs cirkeln. Du kanske misstänker att detta kräver matematik. Tyvärr, du har rätt.

Lyckligtvis, detta är inte den typ av matematik där vi behöver tillämpa Verkliga Begrepp, detta är mer typ där vi Google formler och inte ställa för många frågor.

Enligt Internet, formler för att beräkna x och y punkter längs en cirkel är:

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

…där r är radien, t är den vinkel, och a och b är x-och y-center point förskjutningar.

Vi har redan det mesta av detta: vi vet att våra radie, vi vet hur vi ska beräkna vårt segment vinklar, och vi vet att vår centrum offset-värden (cx och cy).

Det finns en hake dock: i de formler, t i *radianer*. Vi arbetar i grader, vilket innebär att vi behöver göra några omvandlingar. Igen, en snabb sökning visar upp en formel:

radianer = grader * (π / 180)

…som vi kan representera i en metod:

degreesToRadians(vinkel) {
tillbaka vinkel * (Matematik.PI / 180)
},

Vi har nu tillräckligt med information för att beräkna vår x-och y-sms: a koordinater:

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

const textCoords = {
x: (.radie * Matte.cos(radianer) + detta.cx),
y: (.radie * Matte.synd(radianer) + detta.cy)
}
tillbaka textCoords
},

Först måste vi beräkna vinkel av våra segment genom att multiplicera kvoten av våra data värde av 360, men att vi egentligen vill ha hälften av detta eftersom vår text etiketter i mitten av segmentet i stället för i slutet. Vi behöver lägga till i vinkel offset som vi gjorde när vi skapade det segment.

Våra calculateTextCoords metod kan nu användas i calculateChartData beräknas egendom:

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

Låt oss också lägga till en metod för att returnera etikett sträng:

percentageLabel(dataVal) {
return `${Math.omgång(i detta.dataPercentage(dataVal) * 100)}%`
},

Och, i uppmärkning:

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

Nu har vi fått etiketter:

Se Pennan ringdiagram – Oformaterat Etiketter av Salomone Baquis (@soluhmin) på CodePen.

Blech, så off-center. Vi kan fixa detta med text-anchor presentation attribut. Beroende på ditt typsnitt och-storlek, kanske du vill justera positioneringen. Kolla in dx och dy för detta.

Omarbetad text element:

<text-anchor=”middle” dy=”3px” :x=”chartData[index].textX” :y=”chartData[index].textY”>{{ percentageLabel(värde) }}</text>

Hmm, det verkar som om vi har en liten procentandel, etiketter gå utanför segment. Låt oss lägga till en metod för att kontrollera för detta.

segmentBigEnough(dataVal) {
tillbaka Matematik.omgång(i detta.dataPercentage(dataVal) * 100) > 5
}
<text v-if=”segmentBigEnough(värde)” text-anchor=”middle” dy=”3px” :x=”chartData[index].textX” :y=”chartData[index].textY”>{{ percentageLabel(värde) }}</text>

Nu ska vi bara lägga till etiketter till segment som är större än 5%.

Och vi är klara! Vi har nu en återanvändbar ringdiagram komponent som kan acceptera en uppsättning värderingar och skapa segment. Super cool!

Den färdiga produkten:

Se Pennan Vue ringdiagram – Final Version av Salomone Baquis (@soluhmin) på CodePen.

Nästa steg

Det finns massor av sätt att vi kan ändra eller förbättra det här nu när det är byggt. Till exempel:

  • Lägga till element för att förbättra tillgängligheten, som <title> och <desc> – taggar, aria-etiketter och aria roll attribut.
  • Skapa animeringar med CSS eller bibliotek som Greensock för att skapa iögonfallande effekter när diagrammet kommer i sikte.
  • Att leka med färger.

Jag vill gärna höra vad du tycker om detta genomförande, och övriga erfarenheter du har haft med SVG-diagram. Dela i kommentarerna!

Jetpack WordPress plugin som körs på denna webbplats, driver inte bara relaterade inlägg nedan, men den sociala dela länkar ovan, säkerhet och backup, Wiki-stöd, sök på sajten, kommentera form, sättande till sociala nätverk, och mycket mer!