package miaou-core
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
Miaou core/widgets (no drivers, no SDL)
Install
dune-project
Dependency
Authors
Maintainers
Sources
v0.5.2.tar.gz
md5=60a3b9f181f24572a06a9492532bfdda
sha512=fcc35a275066be2900e6201782faf47503076fa4640f08cf78067835a6f447b74613009e55b2ac799adb7ca46f1bffa261fc5971753f2cc3c6bef327511c7ef6
doc/src/miaou_widgets_display/bar_chart_widget.ml.html
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"] ()
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>