Blog der Heimetli Software AG

Mit Python ein Sunburst-Diagramm erzeugen

Sunburst-Diagramme eignen sich gut für eine Uebersicht von hierarchischen Daten, beispielsweise für eine Darstellung des belegten Diskplatzes.

Das nachfolgend beschriebene Script liest die Ausgabe von du und erzeugt daraus ein Diagramm in SVG. du ist eine Linux-Utility die bestimmt wieviel Platz die Directories belegen.

Das Root-Directory von Knoppix 8.6.1 sieht zum Beispiel so aus:

UNIONFS usr KNOPPIX media mnt-system KNOPPIX1 ramdisk home KNOPPIX2 usr usr lib sdb1 KNOPPIX share usr bin var var lib lib opt opt var knoppix home usr opt lib KNOPPIX share lib share i386-linux-gnu lib bin share bin lib locale games python3 texlive lib modules modules libreoffice doc bin eclipse eclipse wine chromium jvm cache firefox knoppix lib python2.7 cache fonts lib firmware firmware src src icons docker docker i386-linux-gnu i386-linux-gnu i386-linux-gnu locale locale games games texlive texlive python3 dist-packages doc texmf-dist apt apt doc libreoffice libreoffice chromium chromium program wine wine plugins plugins jvm java-8-openjdk-i386 firefox firefox python2.7 5.2.5-64 5.2.5-64 apt apt dist-packages python3 fonts fonts jvm python3 HTML icons neverball icons 5.2.5 5.2.5 Static Static UNIONFS usr KNOPPIX media mnt-system usr usr lib sdb1 KNOPPIX share lib KNOPPIX share lib share i386-linux-gnu

Für Linux-Kenner sieht das Bild recht seltsam aus, aber Knoppix ist ein Life-System, was bedeutet dass mehrere Directories öfter vorkommen.

Ein Nachteil der Sunburst-Darstellung: es ist schwierig die Segmente anzuschreiben. Deshalb haben alle Segmente ein title-Element mit dem Namen des Directories und diese Titel werden beim Mouseover angezeigt.

Einen Graph erzeugen

du nimmt zwar einen Pfad als Argument an, aber die Ausgabe sah nicht so aus wie ich es erwartete. Die besten Resultate bekam ich mit folgender Kommandozeile:

> (cd /; du) | python3 sunburst.py > sunburst.svg

segment.py

Segment Ist eine Klasse die Daten und Methoden für ein Segment bereithält. Der komplizierteste Teil ist die Methode write_path. Um sie etwas übersichtlicher zu halten sind manche Variablen extra kurz benannt. p1, p2, p3 und p4 sind die Eckpunkte des Segments. Sie wurden als lokale Variablen definiert um das print-Statement nicht allzu lang werden zu lassen.

import math

class Segment:
    """Class for a segment in the diagram

    Contains the data for one segment and
    the methods to generate the SVG for
    the segment"""
    
    def __init__( self, lst ):
        """Constructs a segment from a list of strings"""
        self.offset = 0.0
        self.size   = lst[0]
        self.name   = lst[-1]
        self.parent = "/".join( lst[1:-1] )
        self.angle  = 0
        self.cx     = 0
        self.cy     = 0
        self.color  = None

    def set_color( self, color ):
        self.color = color

    def write_path( self, parents, center, radius, ringwidth ):
        """Writes the path element for the segment to STDOUT"""
        if self.color == None:
            self.color = parents[self.parent].color

        start = parents[self.parent].offset
        end   = start + self.angle
        parents[self.parent].offset = end
        self.offset = start
        parents[f"{self.parent}/{self.name}"] = self

        flag = 1 if (end - start) > math.pi else 0

        # Shorten the names for better readability of the path string
        rw = ringwidth
        r  = radius
    
        p1 = self.point( center, r + rw, start )
        p2 = self.point( center, r + rw, end   )
        p3 = self.point( center, r,      end   )
        p4 = self.point( center, r,      start )

        self.cx = center + (r+rw/2) * math.sin(start+self.angle/2)
        self.cy = center - (r+rw/2) * math.cos(start+self.angle/2)

        print( f" <path d=\"M {p1} A {r+rw} {r+rw} 0 {flag} 1 {p2} L {p3} A {r} {r} 0 {flag} 0 {p4} Z\" fill=\"{self.color}\" stroke=\"white\">" )
        print( f" <title>{self.name}</title>" )
        print( " </path>" )

    def write_text( self ):
        if self.angle > 0.35:
            print( f"<text x=\"{self.cx}\" y=\"{self.cy}\">{self.name}</text>" )

    def point( self, center, radius, angle ):
        return f"{center+radius*math.sin(angle)} {center-radius*math.cos(angle)}"

sunburst.py

Dieses Script liest die Ausgabe von du vom Standard-Input und zerlegt die Zeilen in Listen variabler Länge. Diese Listen werden in der Liste data abgelegt, und zwar geordnet nach der Länge. In data[0] befindet sich das Root-Element, in data[1] alle Directories direkt darunter, in der nächsten Liste alle SubSub-Directories.

Die andere wichtige Struktur ist parents. Das ist ein Dictionary mit dem die Segmente ihre Parents finden können.

from segment import Segment
import math
import sys

RINGWIDTH = 60

def read_data():
    """Read the output of du"""

    # One list for each path length
    data = [ [], [], [], [], [] ]

    for line in sys.stdin:
        if line.count("/") < len(data):
            line = line.strip()
            line = line.replace("\t","/")
            lst = line.split("/")

            data[len(lst)-2].append( Segment(lst) )

    return data

def init( data ):
    """Initialize factor and parents"""
    root = data[0][0]
    return { root.name : root }, (2 * math.pi) / float(root.size)


def prepare_lists( data, factor, limit ):
    """Sorts the ring segments, computes the angles and eliminates tiny segments"""
    for lst in data[1:]:
        lst.sort( key = lambda s: int(s.size), reverse = True )

        for i in range( len(lst)-1, -1, -1 ):
            angle = float(lst[i].size) * factor
            if  angle >= limit:
                lst[i].angle = angle
            else:
                del lst[i]

def assign_colors( data ):
    """Assigns the colors to the segments in the first ring"""
    ring   = data[1]
    length = len(ring)
    for i in range(length):
        ring[i].set_color( f"hsl({int(360.0/length*(length-i))},70%,50%)" )

def main():
    data            = read_data()
    parents, factor = init( data )

    prepare_lists( data, factor, 0.017453293 )
    assign_colors( data )

    print( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" )
    print( "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 560 560\">" )

    radius = 25
    for ring in data[1:]:
        for segment in ring:
            segment.write_path( parents, 280, radius, RINGWIDTH )
        radius = radius + RINGWIDTH

    print( " <g pointer-events=\"none\" text-anchor=\"middle\" font-size=\"10\" font-family=\"sans-serif\" font-weight=\"bold\">" )

    for ring in data[1:]:
        for segment in ring:
            segment.write_text()

    print( " </g>" )
    print( "</svg>" )

if( __name__ == "__main__" ):
    main()

Selber probieren?

Kein Problem: sunburst.zip enthält die Sourcefiles.