Linien-, Balken- und Kreisdiagramme sind das tägliche Brot von Dashboards und die Grundkomponenten jedes Werkzeugkastens für Datenvisualisierung. Sicher, Sie können SVG oder eine JavaScript-Diagrammbibliothek wie Chart.js oder ein komplexes Werkzeug wie D3 verwenden, um diese Diagramme zu erstellen, aber was ist, wenn Sie keine weitere Bibliothek auf Ihre bereits leistungsschwache Website laden möchten?
Es gibt viele Artikel, die sich mit der Erstellung von rein CSS-basierten Balkendiagrammen, Säulendiagrammen und Kreisdiagrammen befassen, aber wenn Sie nur ein einfaches Liniendiagramm wünschen, haben Sie Pech. Während CSS Linien mit Rahmen und Ähnlichem "zeichnen" kann, gibt es keine klare Methode, um eine Linie von einem Punkt zu einem anderen auf einer X- und Y-Koordinatenebene zu zeichnen.
Nun, es gibt einen Weg! Wenn Sie nur ein einfaches Liniendiagramm benötigen, müssen Sie keine riesige JavaScript-Bibliothek laden oder sogar zu SVG greifen. Sie können alles Notwendige mit nur CSS und ein paar benutzerdefinierten Eigenschaften in Ihrem HTML erstellen. Ein Wort der Warnung jedoch. Es beinhaltet ein wenig Trigonometrie. Wenn Sie das nicht abgeschreckt hat, krempeln Sie die Ärmel hoch und legen wir los!
Hier ist ein kleiner Vorgeschmack darauf, wohin wir gehen
Beginnen wir mit der Basislinie
Wenn Sie ein Liniendiagramm von Hand erstellen (also buchstäblich Linien auf ein kariertes Blatt Papier zeichnen), würden Sie mit der Erstellung der Punkte beginnen und dann diese Punkte verbinden, um die Linien zu bilden. Wenn Sie den Prozess so zerlegen, können Sie jedes grundlegende Liniendiagramm in CSS nachbilden.
Nehmen wir an, wir haben ein Array von Daten, um Punkte in einem X- und Y-Koordinatensystem anzuzeigen, wobei die Wochentage entlang der X-Achse verlaufen und die numerischen Werte Punkte auf der Y-Achse darstellen.
[
{ value: 25, dimension: "Monday" },
{ value: 60, dimension: "Tuesday" },
{ value: 45, dimension: "Wednesday" },
{ value: 50, dimension: "Thursday" },
{ value: 40, dimension: "Friday" }
]
Lassen Sie uns eine unsortierte Liste erstellen, um unsere Datenpunkte zu halten, und ihr einige Stile zuweisen. Hier ist unser HTML
<figure class="css-chart" style="--widget-size: 200px;">
<ul class="line-chart">
<li>
<div class="data-point" data-value="25"></div>
</li>
<li>
<div class="data-point" data-value="60"></div>
</li>
<li>
<div class="data-point" data-value="45"></div>
</li>
<li>
<div class="data-point" data-value="50"></div>
</li>
<li>
<div class="data-point" data-value="40"></div>
</li>
</ul>
</figure>
Ein paar Anmerkungen, die Sie hier entnehmen können. Erstens umschließen wir alles in einem <figure>-Element, was eine schöne semantische HTML-Methode ist, um zu sagen, dass dies in sich geschlossener Inhalt ist, der uns auch den optionalen Vorteil der Verwendung eines <figcaption> bietet, falls wir ihn benötigen. Zweitens beachten Sie, dass wir die Werte in einem Datenattribut speichern, das wir data-value nennen und das sich in einem eigenen Div innerhalb eines Listenelements in der unsortierten Liste befindet. Warum verwenden wir ein separates Div, anstatt die Klasse und das Attribut direkt auf den Listenelementen zu platzieren? Es wird uns später helfen, wenn wir mit dem Zeichnen von Linien beginnen.
Zuletzt beachten Sie, dass wir eine Inline- benutzerdefinierte Eigenschaft auf dem übergeordneten <figure>-Element haben, die wir --widget-size nennen. Wir werden dies in dem CSS verwenden, das wie folgt aussehen wird
/* The parent element */
.css-chart {
/* The chart borders */
border-bottom: 1px solid;
border-left: 1px solid;
/* The height, which is initially defined in the HTML */
height: var(--widget-size);
/* A little breathing room should there be others items around the chart */
margin: 1em;
/* Remove any padding so we have as much space to work with inside the element */
padding: 0;
position: relative;
/* The chart width, as defined in the HTML */
width: var(--widget-size);
}
/* The unordered list holding the data points, no list styling and no spacing */
.line-chart {
list-style: none;
margin: 0;
padding: 0;
}
/* Each point on the chart, each a 12px circle with a light border */
.data-point {
background-color: white;
border: 2px solid lightblue;
border-radius: 50%;
height: 12px;
position: absolute;
width: 12px;
}
Das obige HTML und CSS ergeben diesen nicht so aufregenden Ausgangspunkt
Darstellung von Datenpunkten
Das sieht noch nicht nach viel aus. Wir brauchen eine Möglichkeit, jeden Datenpunkt an seiner jeweiligen X- und Y-Koordinate auf unserem baldigen Diagramm zu zeichnen. In unserem CSS haben wir die Klasse .data-point so eingestellt, dass sie absolut positioniert wird, und wir haben eine feste Breite und Höhe für unseren übergeordneten .css-chart-Container mit einer benutzerdefinierten Eigenschaft festgelegt. Wir können dies verwenden, um unsere X- und Y-Positionen zu berechnen.
Unsere benutzerdefinierte Eigenschaft legt die Diagrammhöhe auf 200 Pixel fest, und in unserem Werte-Array ist der höchste Wert 60. Wenn wir diesen Datenpunkt als höchsten Punkt auf der Y-Achse des Diagramms bei 200 Pixel festlegen, können wir das Verhältnis jedes Wertes in unserem Datensatz zu 60 verwenden und dies mit 200 multiplizieren, um die Y-Koordinate all unserer Punkte zu erhalten. Unser höchster Wert von 60 hat also einen Y-Wert, der wie folgt berechnet werden kann
(60 / 60) * 200 = 200px
Und unser kleinster Wert von 25 ergibt auf die gleiche Weise einen Y-Wert
(25 / 60) * 200 = 83.33333333333334px
Die Y-Koordinate für jeden Datenpunkt zu erhalten, ist einfacher. Wenn wir die Punkte gleichmäßig über das Diagramm verteilen, können wir die Breite des Diagramms (200 Pixel) durch die Anzahl der Werte in unserem Daten-Array (5) teilen, um 40 Pixel zu erhalten. Das bedeutet, der erste Wert hat eine X-Koordinate von 40 Pixel (um einen Rand für eine linke Achse zu lassen, falls gewünscht), und der letzte Wert hat eine X-Koordinate von 200 Pixel.
Du hast gerade Mathe gemacht! 🤓
Fürs Erste fügen wir Inline-Stile zu jedem der Divs in den Listenelementen hinzu. Unser neues HTML sieht dann so aus, wobei die Inline-Stile die berechnete Positionierung für jeden Punkt enthalten.
<figure class="css-chart">
<ul class="line-chart">
<li>
<div class="data-point" data-value="25" style="bottom: 83.33333333333334px; left: 40px;"></div>
</li>
<li>
<div class="data-point" data-value="60" style="bottom: 200px; left: 80px;"></div>
</li>
<li>
<div class="data-point" data-value="45" style="bottom: 150px; left: 120px;"></div>
</li>
<li>
<div class="data-point" data-value="50" style="bottom: 166.66666666666669pxpx; left: 160px;"></div>
</li>
<li>
<div class="data-point" data-value="40" style="bottom: 133.33333333333331px; left: 200px;"></div>
</li>
</ul>
</figure>
Hey, das sieht schon viel besser aus! Aber auch wenn Sie sehen können, wohin die Reise geht, können Sie es immer noch nicht wirklich ein Liniendiagramm nennen. Kein Problem. Wir müssen nur noch ein *kleines* bisschen Mathe verwenden, um unser Verbinde-die-Punkte-Spiel zu beenden. Schauen Sie sich noch einmal das Bild unserer gerenderten Datenpunkte an. Können Sie die Dreiecke sehen, die sie verbinden? Wenn nicht, hilft vielleicht dieses nächste Bild
Warum ist das wichtig? Psst, die Antwort kommt als Nächstes.
Darstellung von Liniensegmenten
Sehen Sie jetzt die Dreiecke? Und es sind nicht irgendwelche alten Dreiecke. Es sind die besten Arten von Dreiecken (zumindest für unsere Zwecke), denn sie sind **rechtwinklig**! Als wir die Y-Koordinaten unserer Datenpunkte berechneten, berechneten wir auch die Länge eines Schenkels unseres rechtwinkligen Dreiecks (d.h. der "Vorlauf", wenn man es wie eine Treppenstufe betrachtet). Wenn wir die Differenz der X-Koordinate von einem Punkt zum nächsten berechnen, erfahren wir die Länge einer anderen Seite unseres rechtwinkligen Dreiecks (d.h. der "Aufstieg" einer Treppenstufe). Und mit diesen beiden Informationen können wir die Länge der magischen Hypotenuse berechnen, die sich herausstellt, genau das ist, was wir brauchen, um auf den Bildschirm zu zeichnen, um unsere Punkte zu verbinden und ein echtes Liniendiagramm zu erstellen.
Nehmen wir zum Beispiel die zweiten und dritten Punkte im Diagramm.
<!-- ... -->
<li>
<div class="data-point" data-value="60" style="bottom: 200px; left: 80px;"></div>
</li>
<li>
<div class="data-point" data-value="45" style="bottom: 150px; left: 120px;"></div>
</li>
<!-- ... -->
Der zweite Datenpunkt hat einen Y-Wert von 200 und der dritte Datenpunkt einen Y-Wert von 150, also hat die Gegenkathete des Dreiecks, die sie verbindet, eine Länge von 200 minus 150, oder 50. Sie hat eine Ankathete, die 40 Pixel lang ist (der Abstand, den wir zwischen jedem unserer Punkte gesetzt haben).
Das bedeutet, die Länge der Hypotenuse ist die Quadratwurzel aus 50 Quadrat plus 40 Quadrat, oder 64,03124237432849.
Lassen Sie uns ein weiteres Div innerhalb jedes Listenelements im Diagramm erstellen, das als Hypotenuse eines Dreiecks dient, das von diesem Punkt aus gezeichnet wird. Dann setzen wir eine Inline-benutzerdefinierte Eigenschaft auf unserem neuen Div, die die Länge dieser Hypotenuse enthält.
<!-- ... -->
<li>
<div class="data-point" data-value="60"></div>
<div class="line-segment" style="--hypotenuse: 64.03124237432849;"></div>
</li>
<!-- ... -->
Während wir schon dabei sind, müssen unsere Liniensegmente ihre richtigen X- und Y-Koordinaten kennen. Lassen Sie uns also die Inline-Stile von unseren .data-point-Elementen entfernen und stattdessen CSS-benutzerdefinierte Eigenschaften zu ihren übergeordneten Elementen (dem <li>-Element) hinzufügen. Nennen wir diese Eigenschaften, kreativ, --x und --y. Unsere Datenpunkte müssen nichts über die Hypotenuse (die Länge unseres Liniensegments) wissen, daher können wir eine CSS-benutzerdefinierte Eigenschaft für die Länge der Hypotenuse direkt zu unserem .line-segment hinzufügen. Unser HTML sieht dann so aus
<!-- ... -->
<li style="--y: 200px; --x: 80px">
<div class="data-point" data-value="60"></div>
<div class="line-segment" style="--hypotenuse: 64.03124237432849;"></div>
</li>
<!-- ... -->
Wir müssen unser CSS aktualisieren, um die Datenpunkte mit diesen neuen benutzerdefinierten Eigenschaften zu positionieren und das neue .line-segment-Div, das wir dem Markup hinzugefügt haben, zu gestalten
.data-point {
/* Same as before */
bottom: var(--y);
left: var(--x);
}
.line-segment {
background-color: blue;
bottom: var(--y);
height: 3px;
left: var(--x);
position: absolute;
width: calc(var(--hypotenuse) * 1px);
}
Nun, wir haben Liniensegmente, aber das ist überhaupt nicht das, was wir wollen. Um ein funktionsfähiges Liniendiagramm zu erhalten, müssen wir eine Transformation anwenden. Aber zuerst beheben wir ein paar Dinge.
Erstens verlaufen unsere Liniensegmente am unteren Rand unserer Datenpunkte, aber wir möchten, dass der Ursprung der Liniensegmente die Mitte der Datenpunktkreise ist. Das können wir mit einer schnellen CSS-Änderung an unseren .data-point-Stilen beheben. Wir müssen ihre X- und Y-Position anpassen, um sowohl die Größe des Datenpunkts und seines Rahmens als auch die Breite des Liniensegments zu berücksichtigen.
.data-point {
/* ... */
/* The data points have a radius of 8px and the line segment has a width of 3px,
so we split the difference to center the data points on the line segment origins */
bottom: calc(var(--y) - 6.5px);
left: calc(var(--x) - 9.5px);
}
Zweitens werden unsere Liniensegmente über den Datenpunkten gerendert, anstatt dahinter. Das können wir beheben, indem wir das Liniensegment zuerst in unserem HTML platzieren
<!-- ... -->
<li style="--y: 200px; --x: 80px">
<div class="line-segment" style="--hypotenuse: 64.03124237432849;"></div>
<div class="data-point" data-value="60"></div>
</li>
<!-- ... -->
Transformationen anwenden, FTW
Wir sind jetzt fast fertig. Wir müssen nur noch eine letzte Berechnung durchführen. Genauer gesagt müssen wir den Winkel ermitteln, der der Gegenkathete unseres rechtwinkligen Dreiecks gegenüberliegt, und dann unser Liniensegment um diese Anzahl von Grad drehen.
Wie machen wir das? Trigonometrie! Sie erinnern sich vielleicht an den kleinen Merkhilfe, um sich zu merken, wie Sinus, Kosinus und Tangens berechnet werden
- SOH (Sinus = Gegenkathete über Hypotenuse
- CAH (Kosinus = Ankathete über Hypotenuse)
- TOA (Tangens = Gegenkathete über Ankathete)
Sie können jeden von ihnen verwenden, da wir die Länge aller drei Seiten unseres rechtwinkligen Dreiecks kennen. Ich habe Sinus gewählt, also bleibt uns diese Gleichung
sin(x) = Opposite / Hypotenuse
Die Antwort auf diese Gleichung sagt uns, wie wir jedes Liniensegment drehen müssen, um es mit dem nächsten Datenpunkt zu verbinden. Das können wir schnell in JavaScript mit Math.asin(Gegenkathete / Hypotenuse) tun. Es gibt uns die Antwort in Radiant, also müssen wir das Ergebnis mit (180 / Math.PI) multiplizieren.
Am Beispiel unseres zweiten Datenpunkts von vorhin haben wir bereits berechnet, dass die Gegenkathete eine Länge von 50 und die Hypotenuse eine Länge von 64,03124237432849 hat, also können wir unsere Gleichung so umschreiben
sin(x) = 50 / 64.03124237432849 = 51.34019174590991
Das ist der Winkel, den wir suchen! Wir müssen diese Gleichung für jeden unserer Datenpunkte lösen und dann den Wert als CSS-benutzerdefinierte Eigenschaft für unsere .line-segment-Elemente übergeben. Das ergibt ein HTML, das so aussieht
<!-- ... -->
<li style="--y: 200px; --x: 80px">
<div class="data-point" data-value="60"></div>
<div class="line-segment" style="--hypotenuse: 64.03124237432849; --angle: 51.34019174590991;"></div>
</li>
<!-- ... -->
Und hier können wir diese Eigenschaften im CSS anwenden
.line-segment {
/* ... */
transform: rotate(calc(var(--angle) * 1deg));
width: calc(var(--hypotenuse) * 1px);
}
Jetzt, wenn wir das rendern, haben wir unsere Liniensegmente!
Warte, was? Unsere Liniensegmente sind überall. Was nun? Oh, richtig. Standardmäßig dreht transform: rotate() um die Mitte des transformierten Elements. Wir möchten, dass die Drehung von der unteren linken Ecke ausgeht, um sich vom aktuellen Datenpunkt zum nächsten zu neigen. Das bedeutet, wir müssen eine weitere CSS-Eigenschaft für unsere .line-segment-Klasse festlegen.
.line-segment {
/* ... */
transform: rotate(calc(var(--angle) * 1deg));
transform-origin: left bottom;
width: calc(var(--hypotenuse) * 1px);
}
Und nun, wenn wir es rendern, erhalten wir endlich das rein CSS-basierte Liniendiagramm, auf das wir gewartet haben.
Wichtiger Hinweis: Wenn Sie den Wert der Gegenkathete (den "Aufstieg") berechnen, stellen Sie sicher, dass er als "Y-Position des aktuellen Datenpunkts" minus "Y-Position des nächsten Datenpunkts" berechnet wird. Dies ergibt einen negativen Wert, wenn der nächste Datenpunkt größer ist (höher im Diagramm) als der aktuelle Datenpunkt, was zu einer negativen Drehung führt. So stellen wir sicher, dass die Linie nach oben verläuft.
Wann Sie diese Art von Diagramm verwenden sollten
Dieser Ansatz eignet sich hervorragend für eine einfache statische Website oder für eine dynamische Website, die serverseitig generierte Inhalte verwendet. Natürlich kann er auch auf einer Website mit clientseitig dynamisch generierten Inhalten verwendet werden, aber dann sind Sie wieder bei der Ausführung von JavaScript auf dem Client. Das CodePen am Anfang dieses Beitrags zeigt ein Beispiel für die clientseitige dynamische Generierung dieses Liniendiagramms.
Die CSS-Funktion calc() ist sehr nützlich, aber sie kann Sinus, Kosinus und Tangens nicht für uns berechnen. Das bedeutet, Sie müssten entweder Ihre Werte von Hand berechnen oder eine schnelle Funktion (client- oder serverseitig) schreiben, um die benötigten Werte (X, Y, Hypotenuse und Winkel) für unsere CSS-benutzerdefinierten Eigenschaften zu generieren.
Ich weiß, dass einige von Ihnen dies durchgearbeitet haben und das Gefühl haben werden, dass es kein reines CSS ist, wenn ein Skript zur Berechnung der Werte benötigt wird – und das ist fair. Der Punkt ist, dass die gesamte Diagrammdarstellung in CSS erfolgt. Die Datenpunkte und die Linien, die sie verbinden, werden alle mit HTML-Elementen und CSS erstellt, das auch in einer statisch gerenderten Umgebung ohne aktiviertes JavaScript wunderbar funktioniert. Und vielleicht noch wichtiger, es ist nicht nötig, eine weitere aufgeblähte Bibliothek herunterzuladen, nur um ein einfaches Liniendiagramm auf Ihrer Seite darzustellen.
Potenzielle Verbesserungen
Wie bei allem gibt es immer etwas, das wir tun können, um die Dinge auf die nächste Stufe zu heben. In diesem Fall denke ich, dass es drei Bereiche gibt, in denen dieser Ansatz verbessert werden könnte.
Responsivität
Der von mir beschriebene Ansatz verwendet eine feste Größe für die Diagrammdimensionen, was genau das ist, was wir in einem responsiven Design nicht wollen. Wir können diese Einschränkung umgehen, wenn wir JavaScript auf dem Client ausführen können. Anstatt unsere Diagrammgröße fest zu codieren, können wir eine CSS-benutzerdefinierte Eigenschaft festlegen (erinnern Sie sich an unsere Eigenschaft --widget-size?), alle Berechnungen darauf basieren und diese Eigenschaft aktualisieren, wenn der Container oder das Fenster entweder initial angezeigt wird oder sich mit einer Form von Containerabfrage oder einem Listener für die Fenstergröße ändert.
Tooltips
Wir könnten ein ::before-Pseudo-Element zu .data-point hinzufügen, um die data-value-Information, die es enthält, in einem Tooltip anzuzeigen, wenn über den Datenpunkt gehovert wird. Dies ist eine nette Zusatzfunktion, die hilft, unser einfaches Diagramm in ein fertiges Produkt zu verwandeln.
Achsenlinien
Beachten Sie, dass die Diagrammachsen unbeschriftet sind? Wir könnten Beschriftungen verteilen, die den höchsten Wert, Null und eine beliebige Anzahl von Punkten dazwischen auf der Achse darstellen.
Ränder
Ich habe versucht, die Zahlen für diesen Artikel so einfach wie möglich zu halten, aber in der realen Welt möchten Sie wahrscheinlich einige Ränder in das Diagramm einbeziehen, damit die Datenpunkte nicht die äußersten Ränder ihres Containers überlappen. Das könnte so einfach sein, wie die Breite eines Datenpunkts vom Bereich Ihrer Y-Koordinaten abzuziehen. Für die X-Koordinaten könnten Sie ähnlich die Breite eines Datenpunkts von der Gesamtbreite des Diagramms abziehen, bevor Sie sie in gleiche Regionen aufteilen.
Und da haben Sie es! Wir haben uns gerade einen Ansatz zur Diagrammerstellung in CSS angesehen, und wir brauchten nicht einmal eine Bibliothek oder eine andere Drittanbieter-Abhängigkeit, um es zum Laufen zu bringen. 💥
Das gefällt mir, ich habe nur wirklich Schwierigkeiten mit den Winkeln.
Ich bin viel zu dumm, um diese Mathe herauszufinden.
Hallo Jonathan! Danke, dass du dir das angesehen hast. Fühl dich nicht dumm! Ich habe versucht zu erklären, wie man die Mathe dafür macht, ohne zu tief ins Detail zu gehen, aber das könnte es schwer verständlich gemacht haben. Hier ist die Grundidee. Die Länge der Basis Ihres Dreiecks ist der horizontale Abstand zwischen den Punkten. Die Länge der gegenüberliegenden Seite des Dreiecks ist die Differenz zwischen zwei benachbarten Punkten (also wenn ein Punkt bei 150px und der nächste bei 100px liegt, hat diese Seite des Dreiecks eine Länge von 50). Die Länge der Linie, die Sie zwischen den Punkten ziehen möchten, ist die Hypotenuse dieses Dreiecks, also die Quadratwurzel aus der Summe der Quadrate der anderen beiden Seiten. Sobald Sie die Länge dieser Hypotenuse haben, müssen Sie nur noch herausfinden, in welchem Winkel Sie sie zeichnen müssen. Dafür können Sie JavaScript in der Konsole Ihres Browsers verwenden. Die Formel lautet Math.asin(Gegenkathete / Hypotenuse) * (180 / Math.PI). Das gibt Ihnen die Anzahl der Grad, um die Sie Ihr Liniensegment drehen müssen. Ich hoffe, das hilft!
Das war erstaunlich! Ich liebe das "Hier sind die Werkzeuge, jetzt drück sie an die Grenzen"-Erlebnis, das die Webentwicklung ausmacht. Tolle Beschreibung.
Hallo Marshall, danke für deinen Beitrag.
Ich habe die Tooltips hinzugefügt, es ist noch einfacher als ein ::before-Pseudoelement, Sie können den data-point-Divs einfach einen Titel geben.
Ich habe auch den "Schlüssel" zum values-Array hinzugefügt, der einfach die Beschriftung auf der x-Achse ist, in meinem Fall ein Monat, zu dem der Wert gehört.
Hier sind meine Änderungen an Ihrem Code
1) Füllen Sie das Werte-Array mit Wert und Schlüssel
const chartValues = [{value: 25, key: “2020-03”},{value: 60, key: “2020-04”},{value: 45, key: “2020-05”},{value: 50, key: “2020-06”},{value: 40, key: “2020-07”}]
2) Kopieren Sie in formatLineChartData den Schlüssel aus dem ursprünglichen Werte-Array nach cssValues, so wie Sie es mit den Werten tun
currentValue.value = values[i].value;
currentValue.key = values[i].key;
3) Geben Sie in createListItem den Schlüssel und den Wert als Titel für data-point aus
Ich hoffe, das hilft jemandem ;)
Beste Grüße
Sascha
Das ist ein großartiger Beitrag, genau das, was ich brauche.
Können Sie etwas Licht darauf werfen, wie man die <div>-Eigenschaft bei Fenstergrößenänderung ändert und das Zeichnen der Linien & Kreise aktualisiert? Ich habe versucht, render() beim resize-Ereignis aufzurufen, was die neuen Linien/Kreise korrekt zeichnet, aber die alten Linien/Kreise sind immer noch auf dem Bildschirm.