HLab

Julien Hautefeuille

Générer un histogramme svg avec Golang

Il est parfois utile de créer des visuels afin d’appuyer certaines données statistiques, mathématiques ou scientifiques.

Le programme que vous retrouverez ci-dessous génère un graphique sous la forme d’un histogramme à partir de données.

Le choix du format de fichier svg permet d’éditer l’histogramme généré dans un logiciel d’édition d’image vectorielle du type Inkscape.

Le programme fait appel à la notion d’interface, construction idiomatique du langage Golang. L’utilisation d’interfaces permet de se familiariser avec la programmation-objet par composition. Appliquée à ce programme, la création d’une interface permet une meilleure modularité du programme et son extension pour la construction de widgets futurs.

Le programme est encore largement perfectible surtout autour de la fonction calcorigin() qui est appelée 2 fois et qui pourrait être substituée par l’utilisation d’une structure de stockage. Je laisse le soin au lecteur de faire appel à sa créativité.

out.svg

Listing du programme :

package main

import (
    "os"
    "fmt"
    "github.com/ajstarks/svgo"
)

var (
    canvas = svg.New(cfile("out.svg"))
    canvas_w = 1024
    canvas_h = 768
)

func cfile(n string) (out *os.File) {
    out, err := os.Create(n)
    if err != nil {
        panic(err)
    }
    return out
}

type bar struct {
    x           int
    y           int
    w           int
    h           int
    offset      int
    l           string
    lx          string
    ly          string
    d           map[string]int
    c           *svg.SVG
    ga          bool
    gas         int
    gw          bool
    gws         int
    wb          int
    s_z         string
    s_t         string
    s_x         string
    s_y         string
    s_lv        string
    s_lt        string
    s_ga        string
    s_gw        string
    s_bar       string
    s_bart      string
    s_bartt     string
}

type widget interface {
    //minmax() (min int, max int)
    drawgrid()
    drawzone()
    drawtitle()
    drawaxes()
    drawbar()
}

func (b bar) minmax() (min int, max int) {
    values := []int{}
    for _,v := range b.d {
        values = append(values, v)
    }
    max, min = values[0], values[0]
    for _, e := range values {
        if e > max {
            max = e
        }
        if e < min {
            min = e
        }
    }
    return min, max
}

func (b bar) calcorigin() (ax_o_x int, 
                            ax_o_y int, 
                            ax_e_x int, 
                            ax_e_y int, 
                            ay_o_x int, 
                            ay_o_y int, 
                            ay_e_x int, 
                            ay_e_y int, 
                            l_ax int, 
                            l_ay int) {

    // axe x, origin and end point
    ax_o_x = b.x + b.offset
    ax_o_y = b.h + b.y - b.offset
    ax_e_x = b.w + b.x - b.offset
    ax_e_y = b.h + b.y - b.offset
    l_ax = ax_e_x - ax_o_x

    // axe y, origin and end point
    ay_o_x = b.x + b.offset
    ay_o_y = b.y + b.h - b.offset
    ay_e_x = b.x + b.offset
    ay_e_y = b.y + b.offset
    l_ay = ay_o_y - ay_e_y

    return ax_o_x, ax_o_y, ax_e_x, ax_e_y, ay_o_x, ay_o_y, ay_e_x, ay_e_y, l_ax, l_ay
}

func (b bar) drawgrid() {
    // canvas grid
    if b.ga {
        canvas.Grid(0, 0, canvas_w, canvas_h, b.gas, b.s_ga)
    }
    // widget grid
    if b.gw {
        canvas.Grid(b.x, b.y, b.w, b.h, b.gws, b.s_gw)
    }
}

func (b bar) drawzone() {
    b.c.Rect(b.x, b.y, b.w, b.h, b.s_z)
}

func (b bar) drawtitle() {
    offs := 20
    b.c.Text(b.w + b.x - offs, b.y + offs, b.l, b.s_t)
}

func (b bar) drawbar() {
    ax_o_x, ax_o_y, _, _, _, _, _, _, l_ax, l_ay := b.calcorigin()
    nb_e := len(b.d)
    grad := (l_ax - (2 * b.offset)) / nb_e

    cut := 0
    for k, v := range b.d {
        b.c.Rect(ax_o_x + b.offset + cut, ax_o_y - (v * l_ay / 100), b.wb, (v * l_ay / 100), b.s_bar)
        b.c.Text(ax_o_x + b.offset + cut + (b.wb/2), ax_o_y - (v * l_ay / 100) - 10, k, b.s_bart)
        b.c.Text(ax_o_x + b.offset + cut + (b.wb/2), ax_o_y - (v * l_ay / 100) + 50, fmt.Sprintf("%d", v), b.s_bartt)
        cut = cut + grad
    }
}

func (b bar) drawaxes() {
    // axe x
    ax_o_x, ax_o_y, ax_e_x, ax_e_y, ay_o_x, ay_o_y, ay_e_x, ay_e_y, l_ax, l_ay := b.calcorigin()
    b.c.Line(ax_o_x, ax_o_y, ax_e_x, ax_e_y, b.s_x)

    // axe y
    b.c.Line(ay_o_x, ay_o_y, ay_e_x, ay_e_y, b.s_y)

    //fmt.Printf("longueur axe x: %d, longueur axe y: %d\n", l_ax, l_ay)

    // y graduation and legend 
    goffs := 20 // graduation offset
    voffs := 10 // title value offset 
    toffs := 20 // title legend y offset
    stroke := 5 // graduation width
    for y := ay_o_y; y >= ay_e_y; y = y - goffs {
        if y == ay_o_y {
            b.c.Text(ay_o_x - voffs, y, "0", b.s_lv)
        } 
        if y == ay_e_y {
            b.c.Text(ay_o_x - voffs, y, "100", b.s_lv)
        }
        b.c.Line(ay_o_x, y, ay_o_x - stroke, y, b.s_y)
    }
    // y axe legend title
    b.c.TranslateRotate(ay_o_x - toffs, l_ay / 2 + b.y + b.offset, -90)
    b.c.Text(0, 0, b.ly, b.s_lt)
    b.c.Gend()

    // x axe legend title
    b.c.Text(l_ax / 2 + b.x + b.offset, ax_o_y + toffs, b.lx, b.s_lt)
}

func widgetbar(w widget) {
    w.drawgrid()
    w.drawzone()    // widget zone
    w.drawtitle()   // widget title
    w.drawbar()     // draw bar
    w.drawaxes()    // axes widget

    //min, max := w.minmax()
    //fmt.Printf("min: %d, max: %d\n", min, max)
    //fmt.Println(w)
}

func main() {
    canvas.Start(canvas_w, canvas_h)
    canvas.Title("Widget Bar")

    b := bar {
        x:      120,
        y:      60,
        w:      640,
        h:      480,
        offset: 40,
        // legend
        l:      "Skills languages",
        lx:     "Languages",
        ly:     "Percent",
        // data
                d: map[string]int{  "Python":       95,
                                    "SQL":          85,
                                    "Golang":       70, 
                                    "Php":          50,
                                    "Javascript":   70, 
                                    "Shell":        60,
                                    "CSS":          70, 
                                    "HTML":         95},
        c:      canvas,
        ga:     false,  // canvas grid
        gas:    20,     // canvas grid size
        gw:     false,  // widget grid
        gws:    20,     // widget grid size
        wb:     50,     // bar width
        // Styles
        // Widget zone
        s_z:    "stroke:black;stroke-width:1;fill-opacity:0",
        // Widget title
        s_t:    "font-size:18px;font-family:Ubuntu;text-anchor:end;no-stroke;fill:black",
        // Axe style
        s_x:    "stroke:black",
        s_y:    "stroke:black",
        // Axe title legend style
        s_lv:   "font-size:14px;font-family:Ubuntu;text-anchor:end;no-stroke;fill:black",
        s_lt:   "font-size:14px;font-family:Ubuntu;text-anchor:middle;no-stroke;fill:black",
        // Grid style
        s_ga:   "stroke:lightgray",
        s_gw:   "stroke:red",
        // Bar style
        s_bar:  "fill:lightgrey;no-stroke",
        s_bart: "font-size:12px;font-family:Ubuntu;text-anchor:middle;no-stroke;fill:black",
        s_bartt:"font-size:16px;font-family:Ubuntu;text-anchor:middle;no-stroke;fill:black",
    }

    widgetbar(b)
    canvas.End()
    fmt.Printf("=> Drawing success\n")
}

http://www.svgopen.org/2011/papers/34-SVGo_a_Go_Library_for_SVG_generation/ https://speakerdeck.com/ajstarks/svgo-workshop https://speakerdeck.com/ajstarks/programming-pictures-with-svgo https://www.flickr.com/photos/ajstarks/albums/72157623441699483/page2