Die Idee dazu kam mir, als ich auf dieses interaktive SVG-Tortendiagramm stieß. Während der SVG-Code so kompakt wie möglich ist (ein einziges <circle>-Element!), ist die Verwendung von Strichen zur Erstellung von Tortendiagrammsegmenten problematisch, da wir unter Rendering-Problemen unter Windows für Firefox und Edge leiden. Außerdem können wir 2018 mit viel weniger JavaScript viel mehr erreichen!
Ich habe es geschafft, das folgende Ergebnis mit einem einzigen HTML-Element für das Diagramm und sehr wenig JavaScript zu erzielen. Die Zukunft sollte die Notwendigkeit von JavaScript vollständig eliminieren, aber dazu später mehr.

Einige von Ihnen erinnern sich vielleicht an Lea Verou's Missing Slice-Vortrag – meine Lösung basiert auf ihrer Technik. Dieser Artikel zerlegt, wie das alles funktioniert, und zeigt, was wir in Bezug auf graceful degradation tun können und wie diese Technik sonst noch eingesetzt werden kann.
Das HTML
Wir verwenden Pug, um das HTML aus einem data-Objekt zu generieren, das einheitenlose Prozentwerte für die letzten drei Jahre enthält.
- var data = { 2016: 20, 2017: 26, 2018: 29 }
Wir platzieren alle unsere Elemente in einem .wrap-Element. Dann durchlaufen wir unser data-Objekt und erstellen für jede seiner Eigenschaften einen Radio-input mit einem entsprechenden label. Danach fügen wir ein .pie-Element hinzu.
- var darr = [], val;
.wrap
- for(var p in data) {
- if(!val) val = data[p];
input(id=`o${p}` name='o' type='radio' checked=val == data[p])
label(for=`o${p}`) #{p}
- darr.push(`${data[p]}% for year ${p}`)
- }
.pie(aria-label=`Value as pie chart. ${darr.join(', ')}.`
role='graphics-document group')
Der obige Pug-Code wird zu folgendem HTML kompiliert:
<style><div class="wrap">
<input id="o2016" name="o" type="radio" checked="checked"/>
<label for="o2016">2016</label>
<input id="o2017" name="o" type="radio"/>
<label for="o2017">2017</label>
<input id="o2018" name="o" type="radio"/>
<label for="o2018">2018</label>
<div class="pie" aria-label="Value as pie chart. 20% for year 2016, 26% for year 2017, 29% for year 2018." role="graphics-document group"></div>
</div>
Beachten Sie, dass wir auch sichergestellt haben, dass nur der erste Radio-input ausgewählt ist.
Übergabe benutzerdefinierter Eigenschaften an das CSS
Ich mag es normalerweise nicht, Stile in HTML zu platzieren, aber in diesem speziellen Fall ist es eine sehr nützliche Möglichkeit, benutzerdefinierte Eigenschaftswerte an das CSS zu übergeben und sicherzustellen, dass wir nur an einer Stelle etwas ändern müssen, wenn wir Datenpunkte ändern müssen – dem Pug-Code. Das CSS bleibt dasselbe.
Der Trick besteht darin, auf dem .pie-Element für jeden Radio-input, der ausgewählt sein könnte, ein einheitenloses Prozentzeichen --p zu setzen.
style
- for(var p in data) {
| #o#{p}:checked ~ .pie { --p: #{data[p]} }
- }
Wir verwenden dieses Prozentzeichen für einen conic-gradient() auf dem .pie-Element, nachdem wir sichergestellt haben, dass keine seiner Dimensionen (einschließlich border und padding) 0 ist.
$d: 20rem;
.wrap { width: $d; }
.pie {
padding: 50%;
background: conic-gradient(#ab3e5b calc(var(--p)*1%), #ef746f 0%);
}
Beachten Sie, dass dies native conic-gradient()-Unterstützung erfordert, da das Polyfill nicht mit CSS-Variablen funktioniert. Derzeit beschränkt dies die Unterstützung auf Blink-Browser mit aktiviertem Flag Experimental Web Platform features, aber die Dinge werden sich wahrscheinlich verbessern.

Update: Chrome 69+ unterstützt conic-gradient() nun auch nativ ohne das Flag.
Wir haben jetzt ein funktionierendes Grundgerüst unserer Demo – die Auswahl eines anderen Jahres über die Radiobuttons führt zu einem anderen conic-gradient()!

Anzeige des Werts
Der nächste Schritt ist die tatsächliche Anzeige des aktuellen Werts, und das tun wir über ein Pseudo-Element. Leider können zahlenbasierte CSS-Variablen nicht für den Wert der content-Eigenschaft verwendet werden. Daher umgehen wir dies mit dem counter()-Hack.
.pie:after {
counter-reset: p var(--p);
content: counter(p) '%';
}
Wir haben auch die Eigenschaften color und font-size angepasst, damit unser Pseudo-Element etwas besser sichtbar ist.

Verfeinerung
Wir wollen keine abrupten Änderungen zwischen den Werten, daher glätten wir die Übergänge mit Hilfe einer CSS-transition. Bevor wir die --p-Variable überblenden oder animieren können, müssen wir sie in JavaScript registrieren.
CSS.registerProperty({
name: '--p',
syntax: '<integer>',
initialValue: 0,
inherits: true
});
Beachten Sie, dass die Verwendung von <number> anstelle von <integer> dazu führt, dass der angezeigte Wert während des transition auf 0 fällt, da unser Zähler eine Ganzzahl benötigt. Danke an Lea Verou, die mir geholfen hat, das herauszufinden!
Beachten Sie auch, dass die explizite Einstellung von inherits zwingend erforderlich ist. Dies war bis vor kurzem nicht der Fall.
Das ist alles JavaScript, was wir für diese Demo benötigen. Zukünftig wird nicht einmal mehr so viel benötigt, da wir benutzerdefinierte Eigenschaften aus CSS registrieren können.
Damit ist die Funktionalität abgeschlossen, wir können eine transition zu unserem .pie-Element hinzufügen.
.pie {
/* same styles as before */
transition: --p .5s;
}
Und das war's mit der Funktionalität! Alles erledigt mit einem Element, einer benutzerdefinierten Variable und einer Prise Houdini-Magie!

Schickmacher-Details
Während unsere Demo funktionsfähig ist, sieht sie zu diesem Zeitpunkt alles andere als hübsch aus. Also kümmern wir uns auch darum!
Das Tortendiagramm zu einem Tortendiagramm machen!
Da die Anwesenheit von :after die height des übergeordneten .pie-Elements erhöht hat, positionieren wir es absolut. Und da wir möchten, dass unser .pie-Element eher wie eine echte Torte aussieht, machen wir es mit border-radius: 50% rund.

Wir wollen auch den Prozentwert in der Mitte des dunklen Tortensegments anzeigen.
Um dies zu tun, positionieren wir es zuerst genau in der Mitte des .pie-Elements. Standardmäßig wird das :after-Pseudo-Element nach dem Inhalt seines übergeordneten Elements angezeigt. Da .pie in diesem Fall keinen Inhalt hat, befindet sich die obere linke Ecke des :after-Pseudo-Elements in der oberen linken Ecke des content-box des übergeordneten Elements. Hier ist die content-box eine 0x0-Box in der Mitte der padding-box. Denken Sie daran, dass wir das padding von .pie auf 50% gesetzt haben – ein Wert, der sich sowohl in horizontaler als auch in vertikaler Richtung auf die Breite des übergeordneten Elements bezieht!
Das bedeutet, dass sich die obere linke Ecke von :after in der Mitte seines übergeordneten Elements befindet. Ein translate(-50%, -50%) verschiebt es also nach links um die Hälfte seiner eigenen width und nach oben um die Hälfte seiner eigenen height, sodass sich sein Mittelpunkt mit dem von .pie überschneidet.
Denken Sie daran, dass %-basierte Übersetzungen relativ zu den Dimensionen *des Elements sind, auf das sie angewendet werden*, entlang der entsprechenden Achse. Mit anderen Worten, eine %-basierte Übersetzung entlang der x-Achse bezieht sich auf die width des Elements, eine %-basierte Übersetzung entlang der y-Achse bezieht sich auf seine height und eine %-basierte Übersetzung entlang der z-Achse bezieht sich auf seine Tiefe, die immer 0 ist, da alle Elemente flache zweidimensionale Boxen mit 0 Tiefe entlang der dritten Achse sind.

Als Nächstes rotieren wir den Wert so, dass die positive Hälfte seiner x-Achse das dunkle Segment in zwei gleiche Hälften teilt, und übersetzen ihn dann um den halben Radius des Tortendiagramms entlang dieser nun rotierten x-Achse.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Wir müssen herausfinden, wie viel wir das :after-Pseudo-Element drehen müssen, damit seine x-Achse das dunkle Segment in zwei gleiche Hälften teilt. Lassen Sie uns das aufschlüsseln!
Anfänglich ist die x-Achse horizontal und zeigt nach rechts. Um sie in die gewünschte Richtung zu bringen, müssen wir sie zunächst drehen, damit sie nach oben zeigt und entlang des Startrandes des Segments verläuft. Dann muss sie im Uhrzeigersinn um die Hälfte eines Segments gedreht werden.
Um die Achse nach oben zeigen zu lassen, müssen wir sie um -90deg drehen. Das Minuszeichen ist darauf zurückzuführen, dass positive Werte eine Uhrzeigerichtung haben und wir uns in die andere Richtung bewegen.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Als Nächstes müssen wir sie um die Hälfte eines Segments drehen.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Aber wie viel ist die Hälfte eines Segments?
Nun, wir wissen bereits, welchen Prozentsatz des Tortendiagramms dieses Segment darstellt: das ist unsere benutzerdefinierte Eigenschaft --p. Wenn wir diesen Wert durch 100 teilen und dann mit 360deg (oder 1turn, es spielt keine Rolle, welche Einheit verwendet wird) multiplizieren, erhalten wir den Zentralwinkel unseres dunklen Segments.
Nach der -90deg-Drehung müssen wir :after im Uhrzeigersinn (positiv) um die Hälfte dieses Zentralwinkels drehen.
Das bedeutet, wir wenden die folgende transform-Kette an:
translate(-50%, -50%) rotate(calc(.5*var(--p)/100*1turn - 90deg)) translate(.25*$d);
Die letzte Übersetzung ist um ein Viertel von $d, was die width des Wrappers ist und uns auch den Durchmesser von .pie gibt. (Da die content-box von .pie eine 0x0-Box ist, hat sie keine border und ihr linker und rechter padding betragen jeweils 50% der Breite des übergeordneten Wrappers.) Der Radius von .pie ist die Hälfte seines Durchmessers, was bedeutet, dass die Hälfte des Radius ein Viertel des Durchmessers ($d) ist.
Jetzt ist das Wertlabel dort positioniert, wo wir es haben wollen.

Es gibt jedoch immer noch ein Problem: Wir möchten nicht, dass es gedreht ist, da dies bei bestimmten Winkeln sehr seltsam und nackenbrecherisch aussehen kann. Um dies zu beheben, setzen wir die Drehung am Ende zurück. Um es uns leichter zu machen, speichern wir den Drehwinkel in einer CSS-Variable namens --a.
--a: calc(.5*var(--p)/100*1turn - 90deg);
transform:
translate(-50%, -50%)
rotate(var(--a))
translate(.25*$d)
rotate(calc(-1*var(--a)));
Viel besser!

Layout
Wir wollen die gesamte Anordnung in der Mitte des Bildschirms haben, also lösen wir das mit einem kleinen, sauberen Grid-Trick.
body {
display: grid;
place-items: center center;
margin: 0;
min-height: 100vh
}
Okay, das platziert das gesamte .wrap-Element in der Mitte.

Der nächste Schritt ist, das Tortendiagramm über den Radio-Buttons zu platzieren. Das machen wir mit einem Flexbox-Layout auf dem .wrap-Element.
.wrap {
display: flex;
flex-wrap: wrap-reverse;
justify-content: center;
width: $d;
}

Styling der Radio-Buttons
…oder genauer gesagt, wir stylen die Labels der Radio-Buttons, denn das Erste, was wir tun, ist, die Radio-Inputs zu verstecken.
[type='radio'] {
position: absolute;
left: -100vw;
}

Da uns dies sehr hässliche und schwer zu unterscheidende Labels hinterlässt, geben wir jedem etwas margin und padding, damit sie nicht so gequetscht aussehen, sowie Hintergründe, damit ihre klickbaren Bereiche klar hervorgehoben sind. Wir können sogar Box- und Textschatten für einige 3D-Effekte hinzufügen. Und natürlich können wir einen separaten Fall erstellen, wenn ihre entsprechenden Eingaben :checked sind.
$c: #ecf081 #b3cc57;
[type='radio'] {
/* same as before */
+ label {
margin: 3em .5em .5em;
padding: .25em .5em;
border-radius: 5px;
box-shadow: 1px 1px nth($c, 2);
background: nth($c, 1);
font-size: 1.25em;
text-shadow: 1px 1px #fff;
cursor: pointer;
}
&:checked {
+ label {
box-shadow: inset -1px -1px nth($c, 1);
background: nth($c, 2);
color: #fff;
text-shadow: 1px 1px #000;
}
}
}
Wir haben auch die font-size etwas erhöht und ein border-radius gesetzt, um die Ecken abzurunden.

Abschließende Schickmacher-Details
Wir setzen einen background für das body, passen die font des gesamten Elements an und fügen eine transition für die Radio-Labels hinzu.

Graceful Degradation
Während unsere Demo in Blink-Browsern jetzt gut aussieht, sieht sie in allen anderen Browsern schrecklich aus – und das sind die meisten Browser!
Zuerst packen wir unsere Arbeit in einen @supports-Block, der auf native conic-gradient()-Unterstützung prüft, damit Browser, die diese unterstützen, das Tortendiagramm rendern. Dies beinhaltet unser conic-gradient(), das padding, das dem Tortendiagramm gleiche horizontale und vertikale Dimensionen verleiht, das border-radius, das das Tortendiagramm kreisförmig macht, und die transform-Kette, die das Wertlabel in der Mitte des Tortensegments positioniert.
.pie {
@supports (background: conic-gradient(tan, red)) {
padding: 50%;
border-radius: 50%;
background: conic-gradient(var(--stop-list));
--a: calc(.5*var(--p)/100*1turn - 90deg);
--pos: rotate(var(--a))
translate(#{.25*$d})
rotate(calc(-1*var(--a)));
}
}
}
Nun konstruieren wir ein Balkendiagramm als Fallback für alle anderen Browser mit linear-gradient(). Wir wollen, dass sich unser Balken über das .wrap-Element erstreckt, sodass der horizontale padding weiterhin 50% beträgt, aber vertikal als schmaler Balken. Wir wollen immer noch, dass das Diagramm hoch genug ist, um das Wertlabel aufzunehmen. Das bedeutet, wir wählen einen kleineren vertikalen padding. Wir verringern auch den border-radius, da 50% uns eine Ellipse ergeben würden und wir ein Rechteck mit leicht abgerundeten Ecken benötigen.
Der Fallback ersetzt auch conic-gradient() durch einen von links nach rechts verlaufenden linear-gradient(). Da sowohl der linear-gradient(), der das Fallback-Balkendiagramm erstellt, als auch der conic-gradient(), der das Tortendiagramm erstellt, die gleiche Stoppliste verwenden, können wir sie in einer CSS-Variable (--stop-list) speichern, damit sie nicht einmal im kompilierten CSS wiederholt werden muss.
Schließlich wollen wir im Fallback das Wertlabel in der Mitte des Balkens haben, da wir keine Tortensegmente mehr haben. Das bedeutet, wir speichern alle Positionierungen nach der Zentrierung in einer CSS-Variable (--pos), deren Wert im Fall ohne conic-gradient()-Unterstützung nichts ist und andernfalls die vorherige transform-Kette.
.pie {
padding: 1.5em 50%;
border-radius: 5px;
--stop-list: #ab3e5b calc(var(--p)*1%), #ef746f 0%;
background: linear-gradient(90deg, var(--stop-list));
/* same as before */
&:after {
transform: translate(-50%, -50%) var(--pos, #{unquote(' ')});
/* same as before */
}
@supports (background: conic-gradient(tan, red)) {
padding: 50%;
border-radius: 50%;
background: conic-gradient(var(--stop-list));
--a: calc(.5*var(--p)/100*1turn - 90deg);
--pos: rotate(var(--a))
translate(#{.25*$d})
rotate(calc(-1*var(--a)));
}
}
}
Wir wechseln auch zu einem Flexbox-Layout auf dem body (da das Grid-Layout, so clever es auch sein mag, in Edge fehlerhaft ist).
body {
display: flex;
align-items: center;
justify-content: center;
/* same as before */
}
Dies ergibt einen Fallback als Balkendiagramm für Browser, die conic-gradient() nicht unterstützen.

Alles responsiv machen
Das einzige Problem, das wir noch haben, ist, dass es nicht mehr gut aussieht, wenn die Viewport-Breite kleiner ist als der Durchmesser des Tortendiagramms.
CSS-Variablen und Media Queries zur Rettung!
Wir setzen den Durchmesser auf eine CSS-Variable (--d), die verwendet wird, um die Dimensionen des Tortendiagramms und die Position des Wertlabels in der Mitte unseres Segments festzulegen.
.wrap {
--d: #{$d};
width: var(--d);
/* same as before */
@media (max-width: $d) { --d: 95vw }
}
Unterhalb bestimmter Viewport-Breiten verringern wir auch die font-size, die margin für unsere <label>-Elemente, und wir positionieren das Wertlabel nicht mehr in der Mitte des dunklen Tortensegments, sondern in der Mitte des Tortendiagramms selbst.
.wrap {
/* same as before */
@media (max-width: 265px) { font-size: .75em; }
}
[type='radio'] {
/* same as before */
+ label {
/* same as before */
@media (max-width: 195px) { margin-top: .25em; }
}
}
.pie{
/* same as before */
@media (max-width: 160px) { --pos: #{unquote(' ')} }
}
Dies ergibt unser Endergebnis: ein responsives Tortendiagramm in Browsern, die conic-gradient() nativ unterstützen. Und obwohl das leider derzeit nur Blink-Browser sind, haben wir einen soliden Fallback, der ein responsives Balkendiagramm für alle anderen Browser rendert. Wir animieren auch zwischen Werten – dies ist derzeit noch stärker eingeschränkt, nur auf Blink-Browser mit aktiviertem Flag für experimentelle Webplattformfunktionen.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Bonus: radialer Fortschritt!
Wir können dieses Konzept auch anwenden, um einen radialen Fortschrittsanzeiger wie den unten stehenden zu erstellen (inspiriert von diesem Pen).

Die Technik ist so gut wie gleich, abgesehen davon, dass wir das Wertlabel genau in der Mitte lassen und den conic-gradient() auf das :before-Pseudo-Element setzen. Das liegt daran, dass wir eine mask verwenden, um alles außer einem dünnen äußeren Ring zu entfernen. Wenn wir den conic-gradient() und die mask auf das Element selbst setzen würden, würde die mask auch das Wertlabel im Inneren ausblenden, und wir möchten, dass dieses sichtbar ist.
Beim Klicken auf den <button> wird ein neuer Wert für unser einheitenloses Prozentzeichen (--p) zufällig generiert, und wir wechseln sanft zwischen den Werten. Wenn wir eine feste transition-duration einstellen, würde dies zu einem wirklich langsamen transition zwischen zwei nahen Werten (z.B. 47% auf 49%) und einem wirklich schnellen transition beim Wechsel zwischen Werten mit einer größeren Lücke dazwischen (z.B. 3% auf 98%) führen. Wir umgehen dies, indem wir die transition-duration vom absoluten Wert der Differenz zwischen dem vorherigen Wert von --p und seinem neu generierten Wert abhängig machen.
[id='out'] { /* radial progress element */
transition: --p calc(var(--dp, 1)*1s) ease-out;
}
const _GEN = document.getElementById('gen'),
_OUT = document.getElementById('out');
_GEN.addEventListener('click', e => {
let old_perc = ~~_OUT.style.getPropertyValue('--p'),
new_perc = Math.round(100*Math.random());
_OUT.style.setProperty('--p', new_perc);
_OUT.style.setProperty('--dp', .01*Math.abs(old_perc - new_perc));
_OUT.setAttribute('aria-label', `Graphical representation of generated percentage: ${new_perc}% of 100%.`)
}, false);
Dies ergibt uns eine schöne animierte radiale Fortschrittsanzeige für Browser, die alle neuen und glänzenden Funktionen unterstützen. Wir haben einen linearen Fallback für Browser, die conic-gradient() nicht nativ unterstützen, und keine transition im Falle fehlender Houdini-Unterstützung.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Tolle Informationen. Seit letzter Woche sammle ich Details über CSS-Variablen.
Es gibt einige erstaunliche Details auf Ihrem Blog, die ich nicht kannte. Danke.
Super Tutorial! Sehr hilfreich.
Großartiger Beitrag! Ich experimentiere ebenfalls mit konischen Gradienten und Tortendiagrammen, seit Chrome die Option hinter dem Flag aktiviert hat, aber Houdini macht es so viel besser, indem es die Animation ermöglicht.
Nur eine alberne Frage: Warum die Radio-Inputs außerhalb des Viewports verstecken? Würde
display: nonenicht auch funktionieren?Weil meiner Kenntnis nach nicht alle Screenreader Elemente lesen, die mit
display: noneversteckt sind. Ich habe das nicht getestet. Ich habe versucht, einen Screenreader zu benutzen, fand es aber ziemlich unmöglich zu verstehen, wie er funktioniert. Also, um auf Nummer sicher zu gehen, setze ichdisplay: nonenicht auf Dinge, die von Screenreadern gelesen werden sollen.Toller Artikel, aber muss man Pug verwenden? Das fügt nur eine zusätzliche Komplexitätsebene für diejenigen hinzu (wie mich), die die Kernkonzepte lernen wollen, die Sie vermitteln wollen. Nur konstruktive Kritik :)
Wenn Sie auf „Kompiliert anzeigen“ klicken (erscheint beim Überfahren in der unteren rechten Ecke), können Sie das gute alte HTML lesen.
Ich hatte das gleiche Problem mit dem Artikel. Die Option „Kompiliert anzeigen“ scheint auf Mobilgeräten nicht verfügbar zu sein (vielleicht ist sie aus dem Bildschirm ausgeblendet?).
Was finden Sie am Pug schwer zu verstehen? Ernsthafte Frage. Denn ich benutze Pug anstelle von einfachem HTML, gerade um eine zusätzliche Komplexitätsebene zu entfernen. Ich weiß, dass ich beim ersten Anblick solcher Dinge Schwierigkeiten hätte, das HTML zu verstehen. Das Pug ist nur eine kompakte und besser lesbare Version, deshalb verwende ich es, um es einfacher verständlich zu machen, weil das HTML manchmal aussehen kann, als hätte jemand Zeichen auf den Bildschirm erbrochen, es gibt viel Wiederholung und Redundanz, die es schwer macht, damit anzufangen.
Auf jeden Fall füge ich die kompilierte Version zum Artikel hinzu.