package miaou-core

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file bar_chart_widget.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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
(*****************************************************************************)
(*                                                                           *)
(* SPDX-License-Identifier: MIT                                              *)
(* Copyright (c) 2025 Nomadic Labs <contact@nomadic-labs.com>                *)
(* Copyright (c) 2026 Mathias Bourgoin <mathias.bourgoin@atacama.tech>       *)
(*                                                                           *)
(*****************************************************************************)

module Helpers = Miaou_helpers.Helpers
module W = Widgets

type bar = string * float * string option

type threshold = {value : float; color : string}

type render_mode = ASCII | Braille | Octant

type t = {
  width : int;
  height : int;
  data : bar list;
  title : string option;
  color : string option;
  min_value : float option;
  max_value : float option;
}

let create ~width ~height ~data ?title ?color ?min_value ?max_value () =
  {
    width;
    height;
    data;
    title;
    color = (match color with Some c -> Some c | None -> None);
    min_value;
    max_value;
  }

let update_data t ~data = {t with data}

let set_default_color t ~color = {t with color = Some color}

(* Calculate value bounds *)
let calculate_bounds t =
  let values = List.map (fun (_, v, _) -> v) t.data in
  let data_min, data_max = Chart_utils.bounds values in
  let min_val =
    match t.min_value with Some v -> v | None -> min 0.0 data_min
  in
  let max_val =
    match t.max_value with Some v -> v | None -> max 1.0 data_max
  in
  (min_val, max_val)

let get_color ~thresholds ~default_color (value, bar_color) =
  let sorted_thresholds =
    List.sort (fun a b -> Float.compare b.value a.value) thresholds
  in
  match bar_color with
  | Some c -> Some c
  | None -> (
      match List.find_opt (fun t -> value > t.value) sorted_thresholds with
      | Some t -> Some t.color
      | None -> default_color)

let render_octant t ~show_values ~thresholds =
  (* Octant bars: same 2×4 resolution as Braille but with per-bar fg color *)
  let min_val, max_val = calculate_bounds t in
  let range = max_val -. min_val in
  let inv_range = if range = 0. then 0. else 1. /. range in
  let num_bars = List.length t.data in
  let bar_width_cells = max 1 (t.width / num_bars) in
  let canvas = Octant_canvas.create ~width:t.width ~height:t.height in
  let dot_width, dot_height = Octant_canvas.get_dot_dimensions canvas in
  let bar_width_dots = max 1 (dot_width / num_bars) in

  List.iteri
    (fun idx (_label, value, bar_color) ->
      let color =
        get_color ~thresholds ~default_color:t.color (value, bar_color)
      in
      let bar_height_dots =
        if range = 0. then dot_height
        else
          int_of_float
            ((value -. min_val) *. inv_range *. float_of_int dot_height)
      in
      let x_start = idx * bar_width_dots in
      let x_end = min dot_width ((idx + 1) * bar_width_dots) in
      for x = x_start to x_end - 1 do
        for dot_from_top = 0 to bar_height_dots - 1 do
          let y = dot_height - 1 - dot_from_top in
          Octant_canvas.set_dot canvas ~x ~y ~color
        done
      done)
    t.data ;

  let lines = ref [] in
  (match t.title with
  | Some title -> lines := W.themed_emphasis title :: !lines
  | None -> ()) ;
  lines := Octant_canvas.render canvas :: !lines ;
  let labels_line = Buffer.create t.width in
  List.iter
    (fun (label, _, _) ->
      let truncated =
        if String.length label > bar_width_cells then
          String.sub label 0 bar_width_cells
        else label
      in
      let padding = max 0 (bar_width_cells - String.length truncated) / 2 in
      Buffer.add_string labels_line (String.make padding ' ') ;
      Buffer.add_string labels_line truncated ;
      Buffer.add_string
        labels_line
        (String.make (bar_width_cells - String.length truncated - padding) ' '))
    t.data ;
  lines := Buffer.contents labels_line :: !lines ;
  if show_values then (
    let values_line = Buffer.create t.width in
    List.iter
      (fun (_, value, _) ->
        let s = Printf.sprintf "%.1f" value in
        let s =
          if String.length s > bar_width_cells then
            String.sub s 0 bar_width_cells
          else s
        in
        let padding = max 0 (bar_width_cells - String.length s) / 2 in
        Buffer.add_string values_line (String.make padding ' ') ;
        Buffer.add_string values_line s ;
        Buffer.add_string
          values_line
          (String.make (bar_width_cells - String.length s - padding) ' '))
      t.data ;
    lines := Buffer.contents values_line :: !lines) ;
  Miaou_helpers.Helpers.concat_lines !lines

let render t ~show_values ?(thresholds = []) ?(mode = ASCII) () =
  if t.data = [] then ""
  else
    match mode with
    | Octant -> render_octant t ~show_values ~thresholds
    | ASCII ->
        let min_val, max_val = calculate_bounds t in
        let range = max_val -. min_val in
        let num_bars = List.length t.data in
        let bar_width = max 1 (t.width / num_bars) in

        let lines = ref [] in

        let repeat ch count =
          let buf = Buffer.create count in
          for _ = 1 to count do
            Buffer.add_string buf ch
          done ;
          Buffer.contents buf
        in

        (* Title *)
        (match t.title with
        | Some title -> lines := W.themed_emphasis title :: !lines
        | None -> ()) ;

        (* Chart area *)
        for row = t.height - 1 downto 0 do
          let line_buf = Buffer.create t.width in
          let y_val_at_row =
            min_val +. (range *. (float_of_int row /. float_of_int t.height))
          in
          List.iter
            (fun (_label, value, bar_color) ->
              let bar_height =
                if range = 0. then t.height
                else
                  int_of_float
                    ((value -. min_val) /. range *. float_of_int t.height)
              in
              let color =
                get_color ~thresholds ~default_color:t.color (value, bar_color)
              in
              let bar_char = if W.prefer_ascii () then "#" else "█" in
              let top_char = if W.prefer_ascii () then "-" else "▀" in

              let segment =
                if row < bar_height then repeat bar_char bar_width
                else if row = bar_height && value > y_val_at_row then
                  repeat top_char bar_width
                else String.make bar_width ' '
              in
              let styled_segment =
                if row <= bar_height && value > y_val_at_row then
                  match color with
                  | Some c -> W.ansi c segment
                  | None -> segment
                else segment
              in
              Buffer.add_string line_buf styled_segment)
            t.data ;
          lines := Buffer.contents line_buf :: !lines
        done ;

        (* X-axis labels *)
        let labels_line = Buffer.create t.width in
        List.iter
          (fun (label, _, _) ->
            let truncated =
              if String.length label > bar_width then
                String.sub label 0 bar_width
              else label
            in
            let padding = max 0 (bar_width - String.length truncated) / 2 in
            Buffer.add_string labels_line (String.make padding ' ') ;
            Buffer.add_string labels_line truncated ;
            Buffer.add_string
              labels_line
              (String.make (bar_width - String.length truncated - padding) ' '))
          t.data ;
        lines := Buffer.contents labels_line :: !lines ;

        (* Add values on top of bars *)
        if show_values then (
          let values_line = Buffer.create t.width in
          List.iter
            (fun (_, value, _) ->
              let s = Printf.sprintf "%.1f" value in
              let s =
                if String.length s > bar_width then String.sub s 0 bar_width
                else s
              in
              let padding = max 0 (bar_width - String.length s) / 2 in
              Buffer.add_string values_line (String.make padding ' ') ;
              Buffer.add_string values_line s ;
              Buffer.add_string
                values_line
                (String.make (bar_width - String.length s - padding) ' '))
            t.data ;
          lines := Buffer.contents values_line :: !lines) ;

        Helpers.concat_lines !lines
    | Braille ->
        (* Use braille canvas for higher resolution bars *)
        let min_val, max_val = calculate_bounds t in
        let range = max_val -. min_val in
        let inv_range = if range = 0. then 0. else 1. /. range in
        let num_bars = List.length t.data in
        let bar_width_cells = max 1 (t.width / num_bars) in

        let canvas = Braille_canvas.create ~width:t.width ~height:t.height in
        let width_cells, height_cells = Braille_canvas.get_dimensions canvas in
        let styles = Array.make_matrix height_cells width_cells None in
        let dot_width, dot_height = Braille_canvas.get_dot_dimensions canvas in
        let bar_width_dots = max 1 (dot_width / num_bars) in

        let mask_left = [|0; 0x40; 0x44; 0x46; 0x47|] in
        let mask_right = [|0; 0x80; 0xA0; 0xB0; 0xB8|] in

        (* Draw each bar *)
        List.iteri
          (fun idx (_label, value, bar_color) ->
            let color =
              get_color ~thresholds ~default_color:t.color (value, bar_color)
            in
            let bar_height_dots =
              if range = 0. then dot_height
              else
                int_of_float
                  ((value -. min_val) *. inv_range *. float_of_int dot_height)
            in
            let x_start = idx * bar_width_dots in
            let x_end = min dot_width ((idx + 1) * bar_width_dots) in
            let max_cell_y = height_cells - 1 in
            (* Fill bar column-by-column using precomputed masks *)
            for x = x_start to x_end - 1 do
              let col = x land 1 in
              let cell_x = x lsr 1 in
              let rec fill cell_y remaining =
                if remaining <= 0 || cell_y < 0 then ()
                else
                  let dots_in_cell = min 4 remaining in
                  let mask =
                    if col = 0 then mask_left.(dots_in_cell)
                    else mask_right.(dots_in_cell)
                  in
                  styles.(cell_y).(cell_x) <- color ;
                  Braille_canvas.add_cell_bits canvas ~cell_x ~cell_y mask ;
                  fill (cell_y - 1) (remaining - 4)
              in
              fill max_cell_y bar_height_dots
            done)
          t.data ;

        let lines = ref [] in

        (* Title *)
        (match t.title with
        | Some title -> lines := W.themed_emphasis title :: !lines
        | None -> ()) ;

        (* Chart *)
        lines := Chart_utils.render_braille_with_colors canvas styles :: !lines ;

        (* X-axis labels *)
        let labels_line = Buffer.create t.width in
        List.iter
          (fun (label, _, _) ->
            let truncated =
              if String.length label > bar_width_cells then
                String.sub label 0 bar_width_cells
              else label
            in
            let padding =
              max 0 (bar_width_cells - String.length truncated) / 2
            in
            Buffer.add_string labels_line (String.make padding ' ') ;
            Buffer.add_string labels_line truncated ;
            Buffer.add_string
              labels_line
              (String.make
                 (bar_width_cells - String.length truncated - padding)
                 ' '))
          t.data ;
        lines := Buffer.contents labels_line :: !lines ;

        (* Add values *)
        if show_values then (
          let values_line = Buffer.create t.width in
          List.iter
            (fun (_, value, _) ->
              let s = Printf.sprintf "%.1f" value in
              let s =
                if String.length s > bar_width_cells then
                  String.sub s 0 bar_width_cells
                else s
              in
              let padding = max 0 (bar_width_cells - String.length s) / 2 in
              Buffer.add_string values_line (String.make padding ' ') ;
              Buffer.add_string values_line s ;
              Buffer.add_string
                values_line
                (String.make (bar_width_cells - String.length s - padding) ' '))
            t.data ;
          lines := Buffer.contents values_line :: !lines) ;

        Helpers.concat_lines !lines

let () =
  Miaou_registry.register
    ~name:"bar_chart"
    ~mli:[%blob "bar_chart_widget.mli"]
    ()