summaryrefslogtreecommitdiff
path: root/src/chart.ml
blob: 0aee05d1d2c8e0882d169a4a2cff5871ef44f27d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
exception Empty_list
let fold fn = function
    | [] -> raise Empty_list
    | head :: tail -> List.fold_left fn head tail

let list_min ls = fold Stdlib.min ls
let list_max ls = fold Stdlib.max ls

let rec repeat n a =
    if n <= 0
    then []
    else a :: repeat (n - 1) a

let empty ls = (ls = [])

let rec zip l1 l2 =
    match (l1, l2) with
    | ([], _) -> []
    | (_, []) -> []
    | (h1 :: t1, h2 :: t2) -> (h1, h2) :: zip t1 t2

let rec transpose = function
    | [] :: _ -> []
    | ls -> List.(map hd ls) :: transpose List.(map tl ls)

let flip f a b = f b a

let rot_right ls = List.(rev ls |> transpose)
let rot_left ls  = List.(transpose ls |> rev)

let to_string c = String.make 1 c
let space = to_string ' '

let saturating_sub b a =
    if a < b then 0
    else a - b

module type Marker = sig
    type t
    val of_string : string -> t
    val to_string : t -> string
end

module Marker = struct
    type t = Point 
           | Pixel 
           | Circle 
           | TriangleDown 
           | TriangleUp
           | TriangleLeft
           | TriangleRight
           | Custom of string

    exception Invalid_marker_length
    let of_string s = 
        if String.length s <> 1
        then raise Invalid_marker_length
        else Custom(s)

    let to_string = function
        | Point -> "."
        | Pixel -> ","
        | Circle  -> "o"
        | TriangleDown  -> "v"
        | TriangleUp -> "^"
        | TriangleLeft -> ">"
        | TriangleRight -> "<"
        | Custom(s) -> s
end

module type ChartOptions = sig
    type t
    val default : t
    val with_height  : int -> t -> t
    val with_marker  : string -> t -> t
end

module ChartOptions = struct
    type t = { height: int; ascii: bool; marker: Marker.t}
    let default =
        { height = 14
        ; ascii = false
        ; marker = Marker.Point
        }
    let with_height v c = {c with height = v; }
    let with_marker v c = {c with marker = v; }
end

module Chart = struct
    let draw vals (opts: ChartOptions.t) =
        let height = opts.height in
        (* let width = List.length vals in *)
        let min_val = list_min vals in
        let max_val = list_max vals in
        (*let range = max_val - min_val in*)

        (* rescale from min-max to 0-height *)
        let scale v = ((v - min_val) * height) / max_val in
        let new_vals = List.map scale vals in
        let rec pad left right s =
            if left = 0
            then s :: repeat right space
            else space :: pad (left - 1) right s
        in
        let new_vals' = zip new_vals (List.tl new_vals) in
        let make_connector s e =
            match Stdlib.compare s e with
            | 0 -> pad s (height - s - 1) "-"
            | x ->
                let (ltr_chr, rtl_chr) =
                    match x with
                    | 1 -> ("└", "┐")
                    | _ -> ("┘", "┌")
                in
                let (st, en) = Stdlib.((min s e, max s e)) in
                let l = en - st |> saturating_sub 1 in
                let (left, right) = (repeat st space, repeat (height - l - st - 1) space)
                in
                List.flatten [left; [ltr_chr]; repeat l "│"; [rtl_chr]; right]
            in
        let m = Marker.to_string opts.marker in
        let rec make_graph = function
            | [] -> []
            | (s, e) :: [] ->
                    pad s (height - s - 1) m
                    :: make_connector s e
                    :: pad e (height - e - 1) m
                    :: []
            | (s, e) :: tl ->
                    pad s (height - s - 1) m
                    :: make_connector s e 
                    :: make_graph tl
        in
        make_graph new_vals' |> rot_left
end