Wie man ein Liniendiagramm mit CSS erstellt

Avatar of Marshall Humphries
Marshall Humphries am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

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

Nun, es sieht irgendwie wie ein Diagramm aus.

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>
Wir haben ein Diagramm!

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.

Die Hypotenuse ist das, was wir brauchen, um unser Liniendiagramm zu zeichnen

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);
}
Wir haben jetzt alle Teile. Sie passen nur noch nicht richtig zusammen.

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!

Nun, die Liniensegmente sind definitiv gedreht. Wir brauchen noch einen Schritt.

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.

Endlich! Ein Liniendiagramm!

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. 💥