Bitte warten...

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:

Code kopieren
<?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>";
?>

Und der Python-Code:

Code kopieren
#!/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:

Code kopieren
  # 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:

Code kopieren
        # 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:

Code kopieren
  # 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:

Code kopieren
        # 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: