Spinnennetze für Fahrräder

Je nach dem, welche Radfahrer*innen man fragt, gilt die N-plus-eins-Regel. Die besagt, dass wenn man aktuell N Fahrräder hat, dann sollte man demnächst N + 1 Fahrräder besitzen. Das ist also eine Pauschalerlaubnis, sich immer noch ein weiteres Fahrrad zu kaufen. Ich selbst habe ein Trekkingrad, und damit bin ich sehr glücklich. Ich kann damit alle Touren machen.

Aber wenn man mal in so ein Bedürfnis des Haben-Wollens reinhört, kann man schon ein paar Nischen finden, in denen noch etwas ging. Zum Beispiel wäre so ein Mountainbike (MTB) für Fahrten auf Waldwegen ein bisschen besser. Und ein Rennrad wäre für den Weg ins Büro schon sehr cool. Aber für lange Touren ist das nichts, weil ich da nicht hinreichend Gepäck draufpacken kann. Da wäre so ein Crossrad cool, aber das ist nicht ganz so ideal auf Asphaltstrecken, wie ein extrem leichtes Rennrad. Es ist also kompliziert.

Aber es geht gar nicht so sehr um die Fahrräder. Vielmehr geht um es das Diagram, das ich als Bewertungshilfe gemacht habe. Dies ist ein Spinnennetzdiagram, und es zeigt die vier Fahrräder. Je größer die Fläche, desto besser das Fahrrad insgesamt. Man sieht aber auch gut, wie Rennrad und MTB eine Nische abdecken, während Trekking- und Crossrad viel breiter aufgestellt sind.

Dieses Diagramm habe ich mit Matplotlib aus der folgenden Tabelle mit Daten erzeugt.

Matplotlib

Kriterium Trekkingrad Rennrad MTB Crossrad
0 Gewicht 3 5 3 4
1 Geschwindigkeit 3 5 2 4
2 Regen 5 1 3 5
3 Langstrecke 4 3 1 5
4 Gepäck 5 1 1 3
5 Anstiege 4 3 5 3
6 Preis 5 3 3 3
7 Unbefestigt 3 1 5 3
8 Beleuchtung 5 1 1 5

Dann habe ich ein bisschen Python-Code, der daraus das Diagramm macht:

import pandas as pd
import matplotlib.pyplot as pl
import numpy as np

data = pd.read_csv('bewertung.csv')
long = data.melt('Kriterium', var_name="Fahrrad", value_name="Wert")

fig, axs = pl.subplots(2, 2, subplot_kw={'projection': 'polar'}, sharex=True, sharey=True)
fig.set_size_inches((10, 10))
fig.patch.set_facecolor('white')

for (fahrrad, group), ax in zip(long.groupby('Fahrrad'), axs.flatten()):
    d = pd.concat([group, group.iloc[0:1]], ignore_index=True)
    theta = np.linspace(0, 2 * np.pi, len(group) + 1)
    ax.fill_between(theta, 0, d.Wert, label=fahrrad)
    ax.set_rticks(np.arange(1, 6))
    ax.set_thetagrids(np.degrees(theta[:-1]), group.Kriterium)
    ax.grid(True)
    ax.set_title(fahrrad)
fig.tight_layout()
fig.savefig("spider-matplotlib.svg")

Soweit auch alles noch in meiner Komfortzone bezüglich Programmierung. Das ganze erzeugt eine SVG-Bilddatei, die man sich jetzt anschauen kann. Und das war es dann aber auch schon.

Richtung D3.js

Mit Bibliotheken wie Altair (Python) kann ich ich Spezifikationen für Vega-Lite (JavaScript) erzeugen, und damit interaktive Grafiken für meine Webseite bauen. Da gibt es auch diverse Artikel, die Diagramme enthalten. Eine Bibliothek, die ich aber schon lange einmal ausprobieren wollte, ist D3.js, mit der man interaktive und animierte Visualisierungen erzeugen kann. Nachdem ich neulich meinen Einstieg in JavaScript hatte, ist die Sprache nicht mehr eine Barriere zwischen mir und der Bibliothek. Ich kann anfangen die Dokumentation zu lesen und herumzuspielen. Mein erstes Projekt sollte die Erzeugung von diesem Spinnennetzdiagram sein, aber mit Interaktivität und Animation. Ich möchte vier Schaltflächen für die Fahrräder machen, und dann soll das Spinnennetz auf das entsprechende Fahrrad umschalten.

Um in D3 reinzukommen, habe ich ein bisschen nach Dokumentation geschaut. Das Wiki von D3 verlinkt auch zu einer Einleitung, die jedoch von Observable geschrieben wurde. Das Problem dabei ist, dass sie D3 im Kontext ihrer Entwicklungsumgebung erklären, und nicht das »nackte« D3. Also habe ich nach Büchern geschaut. Die zwei aktuellsten Bücher, die auch D3 in Version 5 vorstellen, sind einmal das kürzere Buch von Huntington2 und das längere von de Rocha1. Ich habe mit dem kürzeren von Huntington angefangen, weil es mit 180 Seiten noch hinreichend Detail versprach, das von de Rocha mit über 600 Seiten jedoch wohl mehr ins Detail geht, als ich das für den Einstieg brauchen werde.

Reines SVG

Jedoch hatte ich das Buch in wenigen Stunden durch, und gefühlt noch nicht so wirklich viel gelernt. Ich habe etwas über SVG gelernt, das war schon hilfreich. Und auch etwas über D3, wie dort die Daten und die Knoten im DOM verknüpft werden. Aber da waren diverse schmutzige Hacks, die nicht so wirklich stimmig wirkten. Ich habe dann allerdings erstmal noch ein Buch nur zu SVG und JavaScript von Larsen3 genommen, damit ich das in einer ersten Version ohne D3 machen kann.

Das Spinnennetz selbst wird ein Polygon mit 9 Ecken. Das ist in SVG relativ einfach gemacht, allerdings muss ich die Koordinaten ausrechnen. Das geht mit Cosinus und Sinus, und so habe ich für das Trekkingrad Pixelwerte mit Python erzeugt:

xs = np.cos(data.index / len(data.index) * 2 * np.pi) * 50 * data.Trekkingrad + 300
ys = np.sin(data.index / len(data.index) * 2 * np.pi) * 50 * data.Trekkingrad + 300
' '.join([f"{int(x)},{int(y)}" for x, y in zip(xs, ys)])

Das bekomme ich heraus:

450,300 414,396 343,546 200,473 65,385 112,231 174,83 326,152 491,139

Daraus kann man dann ein SVG mit Pfad machen. Das geht so:

<svg width=600 height=600>
    <polygon points="450,300 414,396 343,546 200,473 65,385 112,231 174,83 326,152 491,139" fill="#1f77b4" />
</svg>

Und so sieht das aus:

Im Vergleich zum vorherigen Bild ist das verdreht, weil der Start der Winkel anders gezählt wird. Aber ist eh willkürlich.

Dann braucht es noch die Texte, damit man weiß, was welche Richtung eigentlich aussagt. Das geht mit Tags wie <text x=600 y=300 fill=gray>Gewicht</text>. Dann haben wir dieses Ergebnis hier:

GewichtGeschwindigkeitRegenLangstreckeGepäckAnstiegePreisUnbefestigtBeleuchtung

Ich habe noch mit <circle cx=300 cy=300 r=250 fill=none stroke-width=5 stroke=gray /> einen Kreis hinzugefügt, damit man den Bereich besser sehen kann.

JavaScript dazu

Nun kann ich JavaScript hinzufügen. Letztlich will ich nur das points Attribut des polygon Elementes verändern. Dazu habe ich diese Werte hier:

Fahrrad Eckpunkte
Trekkingrad 450,300 414,396 343,546 200,473 65,385 112,231 174,83 326,152 491,139
Rennrad 550,300 491,460 308,349 225,429 253,317 159,248 224,170 308,250 338,267
MTB 450,300 376,364 326,447 275,343 253,317 65,214 224,170 343,53 338,267
Crossrad 500,300 453,428 343,546 175,516 159,351 159,248 224,170 326,152 491,139

Das ganze kann man dann mit ein bisschen JavaScript kombinieren, siehe spider1.js für den Quelltext. Dazu kommen dann noch ein paar button Tags, und schon kann man damit spielen:

GewichtGeschwindigkeitRegenLangstreckeGepäckAnstiegePreisUnbefestigtBeleuchtung

Das ist soweit auch ganz nett, jedoch ist da noch keine Animation bei.

Mit Animation

Einem Tutorial zu Shape Morphing folgend, kann man es noch besser machen. Man fügt dem polygon Tag noch mehrere animation Tags hinzu. Diese geben dann die Endpunkte der Animation an.

<polygon points="450,300 414,396 343,546 200,473 65,385 112,231 174,83 326,152 491,139" fill="#1f77b4">
    <animate attributeName="points" dur="1s" fill="freeze" id="animation-to-Trekkingrad" begin="indefinite" to="450,300 414,396 343,546 200,473 65,385 112,231 174,83 326,152 491,139"/>
    </polygon>

Durch begin="indefinite" beginnt die Animation nicht direkt automatisch. Mit ein bisschen JavaScript kann man dann die Animation starten lassen:

const animation = document.getElementById(`animation-to-${bikeName}`)
animation.beginElement()

Setzt man das mit den Buttons zusammen, so erhält man dieses schönere Ergebnis:

GewichtGeschwindigkeitRegenLangstreckeGepäckAnstiegePreisUnbefestigtBeleuchtung

Man könnte jetzt noch die insgesamte Darstellung verbessern, den Text etwas besser ausrichten und derartige Dinge tun. Die Animation ist aber so ziemlich das, was ich mir vorgestellt hatte. Die Animation hat allerdings keine physikalische Kinematik dabei, sie sieht noch ziemlich künstlich aus. Es wäre lustiger, wenn sie vielleicht sogar auch über das Ziel hinausschießen und sich dann erst einpendeln würde.

Davon ab war die Entwicklung jetzt sehr ad-hoc. Ich habe das SVG von Hand geschrieben. Dann habe ich mit einem Python-Notebook die Punkte für das Polygon ausgerechnet. Möchte man die Größe von dem Bild jetzt ändern, muss man neue Eckpunkte ausrechnen. Das ist nicht so wirklich cool. Veränderungen in den Daten kann ich ebenfalls nicht sinnvoll weiterreichen. Das ist für einmalige Dinge ganz nett, aber so richtig flexibel fühlt sich das nicht an.


  1. da Rocha, H. Learn D3.js: Create interactive data-driven visualizations for the web with the D3.js library. (Packt Publishing, 2019). 

  2. Huntington, M. D3.js Quick Start Guide: Create Amazing, Interactive Visualizations in the Browser With JavaScript. (Packt Publishing, 2018). 

  3. Larsen, R. Mastering SVG Web Animations, Visualizations and Vector Graphics With HTML, CSS and JavaScript. (2018).