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/sparkline_widget.ml.html
Source file sparkline_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(*****************************************************************************) (* *) (* SPDX-License-Identifier: MIT *) (* Copyright (c) 2025 Nomadic Labs <contact@nomadic-labs.com> *) (* Copyright (c) 2026 Mathias Bourgoin <mathias.bourgoin@atacama.tech> *) (* *) (*****************************************************************************) module W = Widgets (* Unicode block characters for sparkline rendering *) let blocks = [|" "; " "; "▂"; "▃"; "▄"; "▅"; "▆"; "▇"; "█"|] type threshold = {value : float; color : string} type render_mode = ASCII | Braille | Octant type t = { width : int; max_points : int; min_value : float option; max_value : float option; data : float Queue.t; } let create ~width ~max_points ?min_value ?max_value () = {width; max_points; min_value; max_value; data = Queue.create ()} let push t value = Queue.push value t.data ; (* Maintain circular buffer: drop oldest if exceeds max_points *) while Queue.length t.data > t.max_points do ignore (Queue.pop t.data) done let stats t = if Queue.is_empty t.data then (0., 0., 0.) else let values = Queue.to_seq t.data |> List.of_seq in let min_val = match t.min_value with | Some v -> v | None -> List.fold_left min Float.infinity values in let max_val = match t.max_value with | Some v -> v | None -> List.fold_left max Float.neg_infinity values in let current = List.hd (List.rev values) in (min_val, max_val, current) let normalize value min_val max_val = let range = max_val -. min_val in if range = 0. then 4 (* Middle block for flat line *) else let ratio = (value -. min_val) /. range in (* Map to blocks[1..8], not blocks[0..8] since blocks[0] is space *) let block_idx = 1 + int_of_float (ratio *. 7.) in min 8 (max 1 block_idx) let get_color ~thresholds ~(default_color : string option) value = let sorted_thresholds = List.sort (fun a b -> Float.compare b.value a.value) thresholds in match List.find_opt (fun t -> value > t.value) sorted_thresholds with | Some t -> Some t.color | None -> default_color let render_octant t ~focus ~show_value ?color ?(thresholds = []) () = let values = Queue.to_seq t.data |> Array.of_seq in let min_val, max_val, current = stats t in let canvas = Octant_canvas.create ~width:t.width ~height:1 in let dot_width, dot_height = Octant_canvas.get_dot_dimensions canvas in let range = max_val -. min_val in let inv_range = if range = 0. then 0. else 1. /. range in let point_count = Array.length values in let samples = if point_count > dot_width then let step = float_of_int point_count /. float_of_int dot_width in Array.init dot_width (fun i -> let start_f = float_of_int i *. step in let stop_f = float_of_int (i + 1) *. step in let start_i = int_of_float start_f in let stop_i = min point_count (int_of_float stop_f) in let sum = ref 0. in let count = ref 0 in for idx = start_i to stop_i - 1 do sum := !sum +. values.(idx) ; incr count done ; if !count = 0 then min_val else !sum /. float_of_int !count) else if point_count = dot_width then Array.copy values else let pad_left = (dot_width - point_count) / 2 in let arr = Array.make dot_width min_val in Array.blit values 0 arr pad_left point_count ; arr in Array.iteri (fun x value -> let y = if range = 0. then dot_height / 2 else let ratio = (value -. min_val) *. inv_range in int_of_float (ratio *. float_of_int (dot_height - 1)) in let y = dot_height - 1 - y in let color_str = get_color ~thresholds ~default_color:color value in Octant_canvas.set_dot canvas ~x ~y ~color:color_str) samples ; let sparkline = Octant_canvas.render canvas in let sparkline = if focus then W.themed_emphasis sparkline else sparkline in if show_value then Printf.sprintf "%s %.1f" sparkline current else sparkline let render t ~focus ~show_value ?color ?(thresholds = []) ?(mode = ASCII) () = if Queue.is_empty t.data then let empty = String.make t.width ' ' in if show_value then empty ^ " 0.0" else empty else match mode with | Octant -> render_octant t ~focus ~show_value ?color ~thresholds () | ASCII -> let values = Queue.to_seq t.data |> List.of_seq in let min_val, max_val, current = stats t in let buf = Buffer.create (t.width * 10) in (* Each Unicode char can be multiple bytes, plus ANSI codes *) let render_value value = let block_idx = normalize value min_val max_val in let block = blocks.(block_idx) in let c = get_color ~thresholds ~default_color:color value in match c with Some color -> W.ansi color block | None -> block in (* Render sparkline blocks *) let point_count = List.length values in (if point_count > t.width then (* Sample data to fit width *) let step = float_of_int point_count /. float_of_int t.width in for i = 0 to t.width - 1 do let idx = int_of_float (float_of_int i *. step) in let value = List.nth values idx in Buffer.add_string buf (render_value value) done else if point_count = t.width then (* Perfect fit: render all points *) List.iter (fun value -> Buffer.add_string buf (render_value value)) values else (* Pad with spaces if fewer points than width *) let pad_left = (t.width - point_count) / 2 in Buffer.add_string buf (String.make pad_left ' ') ; List.iter (fun value -> Buffer.add_string buf (render_value value)) values ; let pad_right = t.width - point_count - pad_left in Buffer.add_string buf (String.make pad_right ' ')) ; let sparkline = Buffer.contents buf in (* Use themed emphasis for focus instead of raw bold *) let sparkline = if focus then W.themed_emphasis sparkline else sparkline in if show_value then Printf.sprintf "%s %.1f" sparkline current else sparkline | Braille -> let values = Queue.to_seq t.data |> Array.of_seq in let min_val, max_val, current = stats t in (* Create braille canvas: 1 cell height (4 dots), t.width cells wide *) let canvas = Braille_canvas.create ~width:t.width ~height:1 in let width_cells, height_cells = Braille_canvas.get_dimensions canvas in let needs_color = thresholds <> [] || color <> None in let styles = if needs_color then Some (Array.make_matrix height_cells width_cells None) else None in let dot_width, dot_height = Braille_canvas.get_dot_dimensions canvas in let range = max_val -. min_val in let inv_range = if range = 0. then 0. else 1. /. range in (* Sample or interpolate points to match dot_width *) let point_count = Array.length values in let samples = if point_count > dot_width then (* Downsample by averaging chunks *) let step = float_of_int point_count /. float_of_int dot_width in Array.init dot_width (fun i -> let start_f = float_of_int i *. step in let stop_f = float_of_int (i + 1) *. step in let start_i = int_of_float start_f in let stop_i = min point_count (int_of_float stop_f) in let sum = ref 0. in let count = ref 0 in for idx = start_i to stop_i - 1 do sum := !sum +. values.(idx) ; incr count done ; if !count = 0 then min_val else !sum /. float_of_int !count) else if point_count = dot_width then Array.copy values else (* Interpolate/pad to fill width *) let pad_left = (dot_width - point_count) / 2 in let arr = Array.make dot_width min_val in Array.blit values 0 arr pad_left point_count ; arr in (* Plot each value as a vertical line at its height *) Array.iteri (fun x value -> let y = if range = 0. then dot_height / 2 else let ratio = (value -. min_val) *. inv_range in (* Invert Y because we want higher values at the top *) int_of_float (ratio *. float_of_int (dot_height - 1)) in let y = dot_height - 1 - y in (* Set dot at position *) (match styles with | Some s -> let color = get_color ~thresholds ~default_color:color value in let cell_x = x / 2 in let cell_y = y / 4 in if cell_y < height_cells && cell_x < width_cells then s.(cell_y).(cell_x) <- color | None -> ()) ; Braille_canvas.set_dot canvas ~x ~y) samples ; let sparkline = match styles with | Some s -> Chart_utils.render_braille_with_colors canvas s | None -> Braille_canvas.render canvas in (* Use themed emphasis for focus instead of raw bold *) let sparkline = if focus then W.themed_emphasis sparkline else sparkline in if show_value then Printf.sprintf "%s %.1f" sparkline current else sparkline let render_with_label t ~label ~focus ?color ?(thresholds = []) ?(mode = ASCII) () = let spark = render t ~focus ~show_value:false ?color ~thresholds ~mode () in let _, _, current = stats t in Printf.sprintf "%s: [%s] %.1f" label spark current let clear t = Queue.clear t.data (* Accessor functions for SDL rendering *) let get_data t = Queue.to_seq t.data |> List.of_seq let get_bounds t = if Queue.is_empty t.data then (0., 1., 0.) else let values = get_data t in let min_val = match t.min_value with | Some v -> v | None -> List.fold_left min Float.infinity values in let max_val = match t.max_value with | Some v -> v | None -> List.fold_left max Float.neg_infinity values in let current = List.hd (List.rev values) in (min_val, max_val, current) let is_empty t = Queue.is_empty t.data let () = Miaou_registry.register ~name:"sparkline" ~mli:[%blob "sparkline_widget.mli"] ()
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>