package matrix

  1. Overview
  2. Docs

Source file sparkline.ml

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
module Style = Ansi.Style
module Color = Ansi.Color

let utf8_of_uchar (u : Uchar.t) : string =
  let b = Buffer.create 4 in
  Buffer.add_utf_8_uchar b u;
  Buffer.contents b

let braille_glyph_of_bits bits =
  let code = 0x2800 + bits in
  let u =
    match Uchar.of_int code with
    | exception Invalid_argument _ -> Uchar.of_int 0x2800
    | x -> x
  in
  utf8_of_uchar u

type kind = [ `Bars | `Braille ]

type t = {
  auto_max : bool;
  style : Style.t;
  mutable max_value : float;
  buf : float array;
  mutable head : int;
  mutable len : int;
}

let capacity t = Array.length t.buf
let clamp_nonneg v = if v < 0. then 0. else v
let set_max t m = t.max_value <- (if m > 0. then m else 1.)

let create ?(style = Style.default) ?(auto_max = true) ?max_value ~capacity () =
  let cap = max 1 capacity in
  let maxv = match max_value with Some m when m > 0. -> m | _ -> 1. in
  {
    auto_max;
    style;
    max_value = maxv;
    buf = Array.make cap 0.;
    head = 0;
    len = 0;
  }

let clear t =
  t.head <- 0;
  t.len <- 0

let push t v =
  let v = clamp_nonneg v in
  if t.auto_max && v > t.max_value then set_max t v;
  let cap = capacity t in
  Array.unsafe_set t.buf t.head v;
  t.head <- (t.head + 1) mod cap;
  if t.len < cap then t.len <- t.len + 1

let push_all t vs = List.iter (push t) vs

let iter_visible ?max_points t f =
  let cap = capacity t in
  let keep =
    match max_points with
    | None -> min t.len cap
    | Some m -> min t.len (min cap m)
  in
  if keep = 0 then 0
  else
    let start = (t.head - keep + cap) mod cap in
    for i = 0 to keep - 1 do
      let idx = (start + i) mod cap in
      f i (Array.unsafe_get t.buf idx)
    done;
    keep

let fill_background canvas ~x ~y ~width ~height style =
  match style.Style.bg with
  | None -> ()
  | Some bg -> Grid.fill_rect canvas ~x ~y ~width ~height ~color:bg

let draw_bars t canvas ~x:ox ~y:oy ~width ~height ~columns_only =
  if not columns_only then
    fill_background canvas ~x:ox ~y:oy ~width ~height t.style;
  let keep = iter_visible ~max_points:width t (fun _ _ -> ()) in
  if keep = 0 then ()
  else
    let start_x = width - keep in
    let bottom = height - 1 in
    let scale = if t.max_value <= 0. then 1. else float height /. t.max_value in
    let lower_block_char f =
      if f >= 1. then "█"
      else if f <= 0. then ""
      else
        let e = int_of_float (f /. 0.125) in
        let n = f -. (float e *. 0.125) in
        let e = if n >= 0.0625 then e + 1 else e in
        match e with
        | 0 -> ""
        | 1 -> "▁"
        | 2 -> "▂"
        | 3 -> "▃"
        | 4 -> "▄"
        | 5 -> "▅"
        | 6 -> "▆"
        | 7 -> "▇"
        | _ -> "█"
    in
    ignore
      (iter_visible ~max_points:width t (fun i v ->
           let x = ox + start_x + i in
           let sv = v *. scale in
           if sv > 0. then (
             let n = Float.floor sv in
             let full = int_of_float n in
             let frac = sv -. n in
             for j = 0 to full - 1 do
               let y = oy + bottom - j in
               if y >= oy && y < oy + height then
                 Grid.draw_text ~style:t.style canvas ~x ~y ~text:"█"
             done;
             let top = lower_block_char frac in
             if top <> "" then
               let y = oy + bottom - full in
               if y >= oy && y < oy + height then
                 Grid.draw_text ~style:t.style canvas ~x ~y ~text:top)))

let draw_braille t canvas ~x:ox ~y:oy ~width ~height ~columns_only =
  if not columns_only then
    fill_background canvas ~x:ox ~y:oy ~width ~height t.style;
  let keep = iter_visible ~max_points:width t (fun _ _ -> ()) in
  if keep = 0 then ()
  else
    let start_x = width - keep in
    let scale = if t.max_value <= 0. then 1. else float height /. t.max_value in
    let grid_w = width * 2 in
    let grid_h = height * 4 in
    let grid_wm1 = grid_w - 1 in
    let grid_hm1 = grid_h - 1 in
    let dots : (int * int, int) Hashtbl.t = Hashtbl.create 64 in
    let set_dot x y =
      if x >= 0 && y >= 0 && x < grid_w && y < grid_h then
        let cell_x = x / 2 in
        let cell_y = y / 4 in
        if cell_x >= 0 && cell_x < width && cell_y >= 0 && cell_y < height then
          let bit_x = x land 1 in
          let bit_y = y mod 4 in
          let bit_pos =
            match (bit_x, bit_y) with
            | 0, 0 -> 0
            | 0, 1 -> 1
            | 0, 2 -> 2
            | 0, 3 -> 6
            | 1, 0 -> 3
            | 1, 1 -> 4
            | 1, 2 -> 5
            | 1, 3 -> 7
            | _ -> 0
          in
          let key = (cell_x, cell_y) in
          let cur = Option.value (Hashtbl.find_opt dots key) ~default:0 in
          Hashtbl.replace dots key (cur lor (1 lsl bit_pos))
    in
    let to_px i v =
      let x_logical = float (start_x + i) in
      let x_px =
        int_of_float
          (Float.round (x_logical *. float grid_wm1 /. float (max 1 width)))
      in
      let y_val = v *. scale in
      let y_px =
        grid_hm1
        - int_of_float
            (Float.round (y_val *. float grid_hm1 /. float (max 1 height)))
      in
      (x_px, y_px)
    in
    let abs_int x = if x < 0 then -x else x in
    let draw_segment (x1, y1) (x2, y2) =
      let low (x1, y1) (x2, y2) =
        let dx = x2 - x1 in
        let dy = y2 - y1 in
        let yi = ref 1 in
        let dy' = ref dy in
        if !dy' < 0 then (
          yi := -1;
          dy' := - !dy');
        let d = ref ((2 * !dy') - dx) in
        let y = ref y1 in
        let start = ref x1 and stop = ref x2 in
        if !start > !stop then (
          start := x2;
          stop := x1);
        for x = !start to !stop do
          set_dot x !y;
          if !d > 0 then (
            y := !y + !yi;
            d := !d + (2 * (!dy' - dx)))
          else d := !d + (2 * !dy')
        done
      in
      let high (x1, y1) (x2, y2) =
        let dx = x2 - x1 in
        let dy = y2 - y1 in
        let xi = ref 1 in
        let dx' = ref dx in
        if !dx' < 0 then (
          xi := -1;
          dx' := - !dx');
        let d = ref ((2 * !dx') - dy) in
        let x = ref x1 in
        let start = ref y1 and stop = ref y2 in
        if !start > !stop then (
          start := y2;
          stop := y1);
        for y = !start to !stop do
          set_dot !x y;
          if !d > 0 then (
            x := !x + !xi;
            d := !d + (2 * (!dx' - dy)))
          else d := !d + (2 * !dx')
        done
      in
      if abs_int (y2 - y1) < abs_int (x2 - x1) then
        if x1 > x2 then low (x2, y2) (x1, y1) else low (x1, y1) (x2, y2)
      else if y1 > y2 then high (x2, y2) (x1, y1)
      else high (x1, y1) (x2, y2)
    in
    let prev = ref None in
    ignore
      (iter_visible ~max_points:width t (fun i v ->
           let pt = to_px i v in
           match !prev with
           | None ->
               let x, y = pt in
               set_dot x y;
               prev := Some pt
           | Some p ->
               draw_segment p pt;
               prev := Some pt));
    Hashtbl.iter
      (fun (cx, cy) bits ->
        Grid.draw_text ~style:t.style canvas ~x:(ox + cx) ~y:(oy + cy)
          ~text:(braille_glyph_of_bits bits))
      dots

let draw t ~kind ?(columns_only = false) ?(x = 0) ?(y = 0) canvas ~width ~height
    =
  let width = max 1 width and height = max 1 height in
  match kind with
  | `Bars -> draw_bars t canvas ~x ~y ~width ~height ~columns_only
  | `Braille -> draw_braille t canvas ~x ~y ~width ~height ~columns_only

let draw_values ?(style = Style.default) ~kind ?(x = 0) ?(y = 0) values canvas
    ~width ~height =
  let t = create ~style ~capacity:(max 1 width) () in
  push_all t values;
  draw t ~kind ~x ~y canvas ~width ~height