Diagramme mit Bézier-Kurven glätten
Üblicherweise werden Liniendiagramme erstellt, indem die einzelnen Punkte des Diagramms durch gerade Strecken verbunden werden. Daraus ergibt sich ein in der Regel gezackter Streckenzug, der unter Umständen optisch nicht besonders ansprechend ist, da er suggeriert, dass die (hypothetischen) Werte zwischen den Punkten linear ab- bzw. zunehmen.
Mit Hilfe von Bézier-Kurven kann ein Diagramm eine natürlichere Darstellung (Glättung) erhalten, da die einzelnen Punkte durch diese harmonischen Kurven verbunden werden.
Hier wird demonstriert, wie man am Beispiel von mit PHP erstellten SVGs bzw. mit einer in Python geschriebenen grafischen GTK+-Oberfläche unter Verwendung von Cairo Diagramme auf diese Weise darstellen kann.
Hier zunächst der PHP-Code für die klassische Darstellung:
<?php
header("Content-Type: image/svg+xml; charset=utf-8");
$width = 800;
$height = 400;
$values = array(-2, 4, 8, -6, 0, 1, 3, -2, -1, 0);
$maxval = 10;
$margin = 20;
$xgap = ($width - 2 * $margin) / (count($values) - 1);
$ygap = ($height - 2 * $margin) / ($maxval * 2);
echo "<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' baseProfile='full' width='".$width."px' height='".$height."px'>\r\n";
echo " <style type='text/css'>
rect { fill:#001e33; }
line { stroke-width:1; }
.grid { stroke:#005800; }
.zero { stroke:#3fe53f; }
.diagram { stroke:#ffbb00; fill:none; }
</style>\r\n";
echo " <title>Diagramm mit Bézier-Kurven</title>\r\n";
# background
echo " <rect x='0' y='0' width='".$width."' height='".$height."' />\r\n";
# vertical grid lines
for ($i = 0; $i < count($values); $i++) {
$x = $margin + $i * $xgap;
echo " <line class='grid' x1='".$x."' y1='".$margin."' x2='".$x."' y2='".($height - $margin)."' />\r\n";
}
# horizontal grid lines
for ($i = $maxval; $i >= -$maxval; $i--) {
if ($i == 0) $class = "zero"; else $class = "grid";
$y = $margin + ($i - $maxval) * -$ygap;
echo " <line class='".$class."' x1='".($margin)."' y1='".$y."' x2='".($width - $margin)."' y2='".$y."' />\r\n";
}
# diagram lines
echo " <path class='diagram' d='";
for ($i = 0; $i < count($values); $i++) {
$x = $margin + $i * $xgap;
$y = $margin + ($values[$i] - $maxval) * -$ygap;
if ($i == 0) echo "M "; else echo "L ";
echo $x." ".$y;
if ($i != count($values) - 1) echo " ";
}
echo "' />\r\n";
echo "</svg>";
?>
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from gi.repository import Gtk
class MainWindow(Gtk.Window):
def __init__(self):
self.width = 800
self.height = 400
Gtk.Window.__init__(self, title="Diagramm mit Bézier-Kurven")
self.set_position(Gtk.WindowPosition.CENTER)
self.set_default_size(self.width, self.height)
self.drawingarea01 = Gtk.DrawingArea()
self.drawingarea01.connect('draw', self.display_diagram)
self.add(self.drawingarea01)
self.show_all()
def display_diagram(self, widget, cr):
values = [-2, 4, 8, -6, 0, 1, 3, -2, -1, 0]
maxval = 10
margin = 20
xgap = (self.width - 2 * margin) / (len(values) - 1)
ygap = (self.height - 2 * margin) / (maxval * 2)
# background
color = self.hex2rgb("001e33")
cr.set_source_rgb(color[0], color[1], color[2])
cr.rectangle(0, 0, self.width, self.height)
cr.fill()
cr.set_line_width(1)
# vertical grid lines
color = self.hex2rgb("005800")
cr.set_source_rgb(color[0], color[1], color[2])
for i in range(0, len(values)):
x = margin + i * xgap;
cr.move_to(x, margin)
cr.line_to(x, self.height - margin)
cr.stroke()
# horizontal grid lines
for i in range(maxval, -maxval - 1, -1):
if i == 0:
color = self.hex2rgb("3fe53f")
else:
color = self.hex2rgb("005800")
cr.set_source_rgb(color[0], color[1], color[2])
y = margin + (i - maxval) * -ygap
cr.move_to(margin, y)
cr.line_to(self.width - margin, y)
cr.stroke()
# diagram lines
color = self.hex2rgb("ffbb00")
cr.set_source_rgb(color[0], color[1], color[2])
for i in range(0, len(values)):
x = margin + i * xgap;
y = margin + (values[i] - maxval) * -ygap
if i == 0:
cr.move_to(x, y)
else:
cr.line_to(x, y)
cr.stroke()
def hex2rgb(self, hexcol):
r = int(hexcol[0:2], 16) / 256
g = int(hexcol[2:4], 16) / 256
b = int(hexcol[4:6], 16) / 256
return(r, g, b)
win = MainWindow()
win.connect("delete-event", Gtk.main_quit)
Gtk.main()
Daraus ergibt sich folgende Darstellung:
Jetzt werden im Code die Linien des Diagramms zunächst durch einfache kubische Bézier-Kurven ersetzt. Hier die Änderung im PHP-Quelltext:
# diagram lines
$clen = $xgap / 3;
echo " <path class='diagram' d='";
for ($i = 0; $i < count($values); $i++) {
$x = $margin + $i * $xgap;
$y = $margin + ($values[$i] - $maxval) * -$ygap;
if ($i == 0) echo "M ".$x." ".$y;
else {
$cx1 = $lastx + $clen;
$cy1 = $lasty;
$cx2 = $x - $clen;
$cy2 = $y;
echo "C ".$cx1.",".$cy1." ".$cx2.",".$cy2." ".$x.",".$y;
}
$lastx = $x;
$lasty = $y;
if ($i != count($values) - 1) echo " ";
}
echo "' />\r\n";
Und die Änderungen im Python-Code:
# diagram lines
clen = xgap / 3
color = self.hex2rgb("ffbb00")
cr.set_source_rgb(color[0], color[1], color[2])
for i in range(0, len(values)):
x = margin + i * xgap;
y = margin + (values[i] - maxval) * -ygap
if i == 0:
cr.move_to(x, y)
else:
cx1 = lastx + clen;
cy1 = lasty;
cx2 = x - clen;
cy2 = y;
cr.curve_to(cx1, cy1, cx2, cy2, x, y)
lastx = x;
lasty = y;
cr.stroke()
Daraus ergibt sich nun folgende Darstellung:
Das ist schon besser. Allerdings ist der Graph noch etwas ›verbeult‹, da die Tangenten an den Enden der einzelnen Kurvenabschnitte immer horizontal, also parallel zur x-Achse, verlaufen. Um einen geschmeidigeren Kurvenverlauf zu erzeugen, müssen diese Tangenten aber parallel zu der gedachten Verbindungslinie zwischen dem vorigen und dem nächsten Punkt des Diagramms verlaufen.
Im PHP-Quelltext wird das folgendermaßen realisiert:
# diagram lines
$clen = $xgap / 3;
echo " <path class='diagram' d='";
for ($i = 0; $i < count($values); $i++) {
$x = $margin + $i * $xgap;
$y = $margin + ($values[$i] - $maxval) * -$ygap;
if ($i == 0) {
echo "M ".$x." ".$y;
$cy1diff = 0;
} else {
if ($i != count($values) - 1) $nextvalue = $values[$i + 1];
else $nextvalue = $values[$i];
$nexty = $margin + ($nextvalue - $maxval) * -$ygap;
$cy2diff = ($nexty - $lasty) / 6;
$cx1 = $lastx + $clen;
$cy1 = $lasty + $cy1diff;
$cx2 = $x - $clen;
$cy2 = $y - $cy2diff;
echo "C ".$cx1.",".$cy1." ".$cx2.",".$cy2." ".$x.",".$y;
$cy1diff = $cy2diff;
}
$lastx = $x;
$lasty = $y;
if ($i != count($values) - 1) echo " ";
}
echo "' />\r\n";
Und die Änderungen im Python-Code:
# diagram lines
clen = xgap / 3
color = self.hex2rgb("ffbb00")
cr.set_source_rgb(color[0], color[1], color[2])
for i in range(0, len(values)):
x = margin + i * xgap;
y = margin + (values[i] - maxval) * -ygap
if i == 0:
cr.move_to(x, y)
cy1diff = 0
else:
if i != len(values) - 1:
nextvalue = values[i + 1]
else:
nextvalue = values[i]
nexty = margin + (nextvalue - maxval) * -ygap
cy2diff = (nexty - lasty) / 6
cx1 = lastx + clen;
cy1 = lasty + cy1diff;
cx2 = x - clen;
cy2 = y - cy2diff;
cr.curve_to(cx1, cy1, cx2, cy2, x, y)
cy1diff = cy2diff
lastx = x;
lasty = y;
cr.stroke()
Daraus ergibt sich schließlich folgende Darstellung im Vergleich zur klassischen: