recursion_visualize/
visualize.rs

1use std::fmt::Display;
2
3use recursion::{Collapsible, Expandable, MappableFrame};
4
5/// The ability to collapse a value into some output type, frame by frame
6pub trait CollapsibleVizExt: Collapsible
7where
8    Self: Sized + Display,
9    <Self::FrameToken as MappableFrame>::Frame<()>: Display,
10{
11    fn collapse_frames_v<Out>(
12        self,
13        collapse_frame: impl FnMut(<Self::FrameToken as MappableFrame>::Frame<Out>) -> Out,
14    ) -> (Out, Viz)
15    where
16        Out: Display;
17
18    fn try_collapse_frames_v<Out, E: Display>(
19        self,
20        collapse_frame: impl FnMut(<Self::FrameToken as MappableFrame>::Frame<Out>) -> Result<Out, E>,
21    ) -> (Result<Out, E>, Viz)
22    where
23        Out: Display;
24}
25
26impl<X: Collapsible> CollapsibleVizExt for X
27where
28    Self: Sized + Display,
29    <Self::FrameToken as MappableFrame>::Frame<()>: Display,
30{
31    fn collapse_frames_v<Out>(
32        self,
33        collapse_frame: impl FnMut(<Self::FrameToken as MappableFrame>::Frame<Out>) -> Out,
34    ) -> (Out, Viz)
35    where
36        Out: Display,
37    {
38        expand_and_collapse_v::<Self::FrameToken, Self, Out>(self, Self::into_frame, collapse_frame)
39    }
40
41    fn try_collapse_frames_v<Out, E: Display>(
42        self,
43        collapse_frame: impl FnMut(<Self::FrameToken as MappableFrame>::Frame<Out>) -> Result<Out, E>,
44    ) -> (Result<Out, E>, Viz)
45    where
46        Out: Display,
47    {
48        try_expand_and_collapse_v::<Self::FrameToken, Self, Out, E>(
49            self,
50            |x| Ok(Self::into_frame(x)),
51            collapse_frame,
52        )
53    }
54}
55
56pub trait ExpandableVizExt: Expandable
57where
58    Self: Sized + Display,
59    <Self::FrameToken as MappableFrame>::Frame<()>: Display,
60{
61    fn expand_frames_v<In>(
62        input: In,
63        expand_frame: impl FnMut(In) -> <Self::FrameToken as MappableFrame>::Frame<In>,
64    ) -> (Self, Viz)
65    where
66        In: Display;
67}
68
69impl<X: Expandable> ExpandableVizExt for X
70where
71    Self: Sized + Display,
72    <Self::FrameToken as MappableFrame>::Frame<()>: Display,
73{
74    fn expand_frames_v<In>(
75        input: In,
76        expand_frame: impl FnMut(In) -> <Self::FrameToken as MappableFrame>::Frame<In>,
77    ) -> (Self, Viz)
78    where
79        In: Display,
80    {
81        expand_and_collapse_v::<Self::FrameToken, In, Self>(input, expand_frame, Self::from_frame)
82    }
83}
84
85type VizNodeId = u32;
86
87#[derive(Clone)]
88pub enum VizAction {
89    // expand a seed to a node, with new child seeds if any
90    ExpandSeed {
91        target_id: VizNodeId,
92        txt: String,
93        seeds: Vec<(VizNodeId, String)>,
94    },
95    // collapse node to value, removing all child nodes
96    CollapseNode {
97        target_id: VizNodeId,
98        txt: String,
99    },
100    // info text display!
101    InfoCard {
102        info_header: String,
103        info_txt: String,
104    },
105}
106
107// impl VizAction {
108//     pub fn target_id(&self) -> VizNodeId {
109//         match self {
110//             VizAction::ExpandSeed { target_id, .. } => *target_id,
111//             VizAction::CollapseNode { target_id, ..} => *target_id,
112//         }
113//     }
114
115//     pub fn increment_id(&mut self, x: u32) {
116//         match self {
117//             VizAction::ExpandSeed { target_id, .. } => *target_id += x,
118//             VizAction::CollapseNode { target_id, ..} => *target_id += x,
119//         }
120//     }
121// }
122
123#[derive(Clone)]
124pub struct Viz {
125    seed_txt: String,
126    root_id: VizNodeId,
127    actions: Vec<VizAction>,
128}
129
130impl Viz {
131    pub fn label(mut self, info_header: String, info_txt: String) -> Self {
132        let mut actions = vec![VizAction::InfoCard {
133            info_header,
134            info_txt,
135        }];
136        actions.extend(self.actions.into_iter());
137        self.actions = actions;
138
139        self
140    }
141
142    pub fn fuse(self, next: Self, info_header: String, info_txt: String) -> Self {
143        let mut actions = self.actions;
144        actions.push(VizAction::InfoCard {
145            info_txt,
146            info_header,
147        });
148        actions.extend(next.actions.into_iter());
149
150        Self {
151            seed_txt: self.seed_txt,
152            root_id: self.root_id,
153            actions,
154        }
155    }
156
157    pub fn write(self, path: String) {
158        let to_write = serialize_html(self).unwrap();
159
160        println!("write to: {:?}", path);
161
162        std::fs::write(path, to_write).unwrap();
163    }
164}
165
166// this is hilariously jamky and I can do better, but this is an experimental feature so I will not prioritize doing so.
167pub fn serialize_html(v: Viz) -> serde_json::Result<String> {
168    let mut out = String::new();
169    out.push_str(TEMPLATE_BEFORE);
170    out.push_str(&serialize_json(v)?);
171    out.push_str(TEMPLATE_AFTER);
172
173    Ok(out)
174}
175
176pub fn serialize_json(v: Viz) -> serde_json::Result<String> {
177    use serde_json::value::Value;
178    let actions: Vec<Value> = v
179        .actions
180        .into_iter()
181        .map(|elem| match elem {
182            VizAction::ExpandSeed {
183                target_id,
184                txt,
185                seeds,
186            } => {
187                let mut h = serde_json::Map::new();
188                h.insert(
189                    "target_id".to_string(),
190                    Value::String(target_id.to_string()),
191                );
192                h.insert("txt".to_string(), Value::String(txt));
193                let mut json_seeds = Vec::new();
194                for (node_id, txt) in seeds.into_iter() {
195                    let mut h = serde_json::Map::new();
196                    h.insert("node_id".to_string(), Value::String(node_id.to_string()));
197                    h.insert("txt".to_string(), Value::String(txt));
198                    json_seeds.push(Value::Object(h));
199                }
200                h.insert("seeds".to_string(), Value::Array(json_seeds));
201                Value::Object(h)
202            }
203            VizAction::CollapseNode { target_id, txt } => {
204                let mut h = serde_json::Map::new();
205                h.insert(
206                    "target_id".to_string(),
207                    Value::String(target_id.to_string()),
208                );
209                h.insert("txt".to_string(), Value::String(txt));
210                Value::Object(h)
211            }
212            VizAction::InfoCard {
213                info_txt,
214                info_header,
215            } => {
216                let mut h = serde_json::Map::new();
217                h.insert("info_txt".to_string(), Value::String(info_txt.to_string()));
218                h.insert(
219                    "info_header".to_string(),
220                    Value::String(info_header.to_string()),
221                );
222                h.insert("typ".to_string(), Value::String("info_card".to_string()));
223                Value::Object(h)
224            }
225        })
226        .collect();
227
228    let viz_root = {
229        let mut h = serde_json::Map::new();
230        h.insert("node_id".to_string(), Value::String(v.root_id.to_string()));
231        h.insert("txt".to_string(), Value::String(v.seed_txt));
232        h.insert("typ".to_string(), Value::String("seed".to_string()));
233        Value::Object(h)
234    };
235
236    let viz_js = {
237        let mut h = serde_json::Map::new();
238        h.insert("root".to_string(), viz_root);
239        h.insert("actions".to_string(), Value::Array(actions));
240        Value::Object(h)
241    };
242
243    serde_json::to_string(&viz_js)
244}
245
246pub fn try_expand_and_collapse_v<F, Seed, Out, E>(
247    seed: Seed,
248    mut coalg: impl FnMut(Seed) -> Result<F::Frame<Seed>, E>,
249    mut alg: impl FnMut(F::Frame<Out>) -> Result<Out, E>,
250) -> (Result<Out, E>, Viz)
251where
252    F: MappableFrame,
253    E: Display,
254    F::Frame<()>: Display,
255    Seed: Display,
256    Out: Display,
257{
258    enum State<Pre, Post> {
259        PreVisit(Pre),
260        PostVisit(Post),
261    }
262
263    let mut keygen = 1; // 0 is used for root node
264    let mut v = Vec::new();
265    let root_seed_txt = format!("{}", seed);
266
267    let mut vals: Vec<Out> = vec![];
268    let mut todo: Vec<State<(VizNodeId, Seed), _>> = vec![State::PreVisit((0, seed))];
269
270    while let Some(item) = todo.pop() {
271        match item {
272            State::PreVisit((viz_node_id, seed)) => {
273                let mut seeds_v = Vec::new();
274
275                let node = match coalg(seed) {
276                    Ok(node) => node,
277                    Err(e) => {
278                        v.push(VizAction::InfoCard {
279                            info_header: "Error during expand!".to_string(),
280                            info_txt: format!("error: {}", e),
281                        });
282                        return (
283                            Err(e),
284                            Viz {
285                                seed_txt: root_seed_txt,
286                                root_id: 0,
287                                actions: v,
288                            },
289                        );
290                    }
291                };
292                let mut topush = Vec::new();
293                let node = F::map_frame(node, |seed| {
294                    let k = keygen;
295                    keygen += 1;
296                    seeds_v.push((k, format!("{}", seed)));
297
298                    topush.push(State::PreVisit((k, seed)))
299                });
300
301                v.push(VizAction::ExpandSeed {
302                    target_id: viz_node_id,
303                    txt: format!("{}", node),
304                    seeds: seeds_v,
305                });
306
307                todo.push(State::PostVisit((viz_node_id, node)));
308                todo.extend(topush.into_iter());
309            }
310            State::PostVisit((viz_node_id, node)) => {
311                let node = F::map_frame(node, |_: ()| vals.pop().unwrap());
312
313                let out = match alg(node) {
314                    Ok(out) => out,
315                    Err(e) => {
316                        v.push(VizAction::InfoCard {
317                            info_header: "Error during collapse!".to_string(),
318                            info_txt: format!("error: {}", e),
319                        });
320                        return (
321                            Err(e),
322                            Viz {
323                                seed_txt: root_seed_txt,
324                                root_id: 0,
325                                actions: v,
326                            },
327                        );
328                    }
329                };
330
331                v.push(VizAction::CollapseNode {
332                    target_id: viz_node_id,
333                    txt: format!("{}", out),
334                });
335
336                vals.push(out)
337            }
338        };
339    }
340
341    let out = vals.pop().unwrap();
342
343    v.push(VizAction::InfoCard {
344        info_header: "Completed".to_string(),
345        info_txt: format!("result: {}", out),
346    });
347
348    (
349        Ok(out),
350        Viz {
351            seed_txt: root_seed_txt,
352            root_id: 0,
353            actions: v,
354        },
355    )
356}
357
358// use std::fmt::Debug;
359// TODO: split out root seed case to separate field on return obj, not needed as part of enum!
360pub fn expand_and_collapse_v<F, Seed, Out>(
361    seed: Seed,
362    mut coalg: impl FnMut(Seed) -> F::Frame<Seed>,
363    mut alg: impl FnMut(F::Frame<Out>) -> Out,
364) -> (Out, Viz)
365where
366    F: MappableFrame,
367    // F::Frame<Seed>: Display,
368    F::Frame<()>: Display,
369    Seed: Display,
370    Out: Display,
371{
372    enum State<Pre, Post> {
373        PreVisit(Pre),
374        PostVisit(Post),
375    }
376
377    let mut keygen = 1; // 0 is used for root node
378    let mut v = Vec::new();
379    let root_seed_txt = format!("{}", seed);
380
381    let mut vals: Vec<Out> = vec![];
382    let mut todo: Vec<State<(VizNodeId, Seed), _>> = vec![State::PreVisit((0, seed))];
383
384    while let Some(item) = todo.pop() {
385        match item {
386            State::PreVisit((viz_node_id, seed)) => {
387                let mut seeds_v = Vec::new();
388
389                let node = coalg(seed);
390                let mut topush = Vec::new();
391                let node = F::map_frame(node, |seed| {
392                    let k = keygen;
393                    keygen += 1;
394                    seeds_v.push((k, format!("{}", seed)));
395
396                    topush.push(State::PreVisit((k, seed)))
397                });
398
399                v.push(VizAction::ExpandSeed {
400                    target_id: viz_node_id,
401                    txt: format!("{}", node),
402                    seeds: seeds_v,
403                });
404
405                todo.push(State::PostVisit((viz_node_id, node)));
406                todo.extend(topush.into_iter());
407            }
408            State::PostVisit((viz_node_id, node)) => {
409                let node = F::map_frame(node, |_: ()| vals.pop().unwrap());
410
411                let out = alg(node);
412
413                v.push(VizAction::CollapseNode {
414                    target_id: viz_node_id,
415                    txt: format!("{}", out),
416                });
417
418                vals.push(out)
419            }
420        };
421    }
422
423    let out = vals.pop().unwrap();
424
425    v.push(VizAction::InfoCard {
426        info_header: "Completed".to_string(),
427        info_txt: format!("result: {}", out),
428    });
429
430    (
431        out,
432        Viz {
433            seed_txt: root_seed_txt,
434            root_id: 0,
435            actions: v,
436        },
437    )
438}
439
440//TODO/FIXME: something better than this. that said, this is in experimental so :shrug_emoji:
441static TEMPLATE_BEFORE: &'static str = r###"
442<!DOCTYPE html>
443<meta charset="UTF-8">
444<style>
445
446.node rect {
447  fill: #fff;
448  stroke-width: 4px;
449  rx: 4px;
450  rY: 4px;
451 }
452
453 .node text {
454  font: 16px verdana;
455}
456
457body {
458  background-color: lightcyan;
459} 
460
461.infocard {
462  background-color: white;
463  border-style: solid;
464  width: 500px;
465  padding: 10px;
466  border-radius: 10px;
467} 
468
469.infocard .cardheader {
470  font-size: 25px;
471  padding-top: 5px;
472  padding-bottom: 5px;
473  border-bottom: solid;
474  border-width: 5px;
475} 
476
477.infocard .cardbody {
478  font-size: 15px;
479  padding: 10px;
480  font-family: "Lucida Console", "Courier New", monospace;
481  background-color: steelblue;
482  color: white;
483}
484
485.link {
486  fill: none;
487  stroke-width: 4px;
488}
489
490</style>
491
492<body>
493
494<div opacity="0" id="titlecard" class="infocard">
495  <div class="cardheader">header</div>
496  <div class="cardbody">body</div>
497</div>
498
499<!-- load the d3.js library -->	
500<script src="https://d3js.org/d3.v7.js"></script>
501<script>
502
503
504 // colors for use in nodes, links, etc
505 const collapse_stroke = "mediumVioletRed";
506 const expand_stroke = "steelBlue";
507 const structure_stroke = "black";
508
509
510const data = "###;
511
512static TEMPLATE_AFTER: &'static str = r###"
513
514 var treeData = data.root;
515
516 var actions = data.actions;
517
518
519// Set the dimensions and margins of the diagram
520var margin = {top: 0, right: 10, bottom: 30, left: 30},
521    width = 900 - margin.left - margin.right,
522    height = 410 - margin.top - margin.bottom;
523
524// append the svg object to the body of the page
525// appends a 'group' element to 'svg'
526// moves the 'group' element to the top left margin
527var svg = d3.select("body").append("svg")
528    .attr("width", width + margin.right + margin.left)
529    .attr("height", height + margin.top + margin.bottom)
530    .append("g")
531    .attr("transform", "translate("
532          + margin.left + "," + margin.top + ")");
533
534var i = 0,
535    duration = 250,
536    root;
537
538// declares a tree layout and assigns the size
539var treemap = d3.tree().size([height/1.4, width]);
540
541// Assigns parent, children, height, depth
542 root = d3.hierarchy(treeData, function(d) { return d.children; });
543 root.x0 = height / 2;
544root.y0 = 0;
545
546
547update(root);
548
549var pause = 0;
550
551
552 let intervalId = setInterval(function () {
553    if (pause == 0) {
554     var next = actions.shift();
555     if (next) {
556    if (next.typ == "info_card") {
557         d3.select("#titlecard .cardheader").text(next.info_header);
558         d3.select("#titlecard .cardbody").text(next.info_txt);
559
560         d3.select("#titlecard")
561         .transition().duration(500)
562         .style("border-color", "mediumvioletred")
563         .style("color", "mediumvioletred")
564         .transition().duration(1000)
565         .style("border-color", "black")
566         .style("color", "black");
567
568    } else if (next.seeds) { // in this case, is expand (todo explicit typ field for this)
569
570        let target = root.find(x => x.data.node_id == next.target_id);
571
572        target.data.txt = next.txt;
573        target.data.typ = "structure";
574
575        if (next.seeds.length) {
576            target.children = [];
577            target.data.children = [];
578        } else {
579            delete target.children;
580            delete target.data.children;
581        }
582        next.seeds.forEach(function(seed) {
583            var newNode = d3.hierarchy(seed);
584            newNode.depth = target.depth + 1;
585            newNode.height = target.height - 1;
586            newNode.parent = target;
587
588            newNode.data.typ = "seed";
589
590            target.children.push(newNode);
591            target.data.children.push(newNode.data);
592        });
593
594        update(target);
595
596    } else { // in this case, is collapse
597        let target = root.find(x => x.data.node_id == next.target_id);
598
599        // remove child nodes from tree
600        delete target.children;
601        delete target.data.children;
602        target.data.txt = next.txt;
603        target.data.typ = "collapse";
604
605
606        update(target);
607
608     }
609     } else {
610         clearInterval(intervalId);
611     }} else { pause -= 1;}
612 }, 600);
613
614function update(source) {
615
616  // Assigns the x and y position for the nodes
617  var treeData = treemap(root);
618
619  // Compute the new tree layout.
620  var nodes = treeData.descendants(),
621      links = treeData.descendants().slice(1);
622
623  // Normalize for fixed-depth.
624  nodes.forEach(function(d){ d.y = d.depth * 110});
625
626  // ****************** Nodes section ***************************
627
628  // Update the nodes...
629  var node = svg.selectAll('g.node')
630      .data(nodes, function(d) {return d.id || (d.id = ++i); });
631
632  // Enter any new modes at the parent's previous position.
633  var nodeEnter = node.enter().append('g')
634      .attr('class', 'node')
635      .attr("transform", function(d) {
636        return "translate(" + source.y0 + "," + source.x0 + ")";
637    });
638
639  // Add rect for the nodes
640  nodeEnter.append('rect')
641      .attr('class', 'node')
642      .attr('width', 1e-6)
643      .attr('height', 1e-6)
644           .transition()
645           .duration(duration)
646
647           .transition()
648           .duration(duration)
649     ;
650
651  // Add labels for the nodes
652  nodeEnter.append('text')
653      .attr("dy", ".35em")
654      .attr("x", function(d) {
655          return d.children || d._children ? -13 : 13;
656      })
657      .attr("text-anchor", function(d) {
658          return d.children || d._children ? "end" : "start";
659      })
660           .text(function(d) { return (d.data.txt); });
661
662  // UPDATE
663  var nodeUpdate = nodeEnter.merge(node);
664
665  // Transition to the proper position for the node
666  nodeUpdate.transition()
667    .duration(duration)
668    .attr("transform", function(d) {
669        return "translate(" + d.y + "," + d.x + ")";
670     });
671
672  // Update the node attributes and style
673     nodeUpdate.select('rect.node')
674               .attr('stroke', function(d) {
675                   switch(d.data.typ) {
676                       case 'structure':
677                           return structure_stroke;
678                       case 'seed':
679                           return expand_stroke;
680                       case 'collapse':
681                           return collapse_stroke;
682                   }
683               })
684            .attr('width', function(d){ return textSize(d.data.txt).width})
685            .attr('height', textSize("x").height + 5 )
686               .attr("transform", function(d) {return "translate(0, -" + (textSize("x").height + 5) / 2 + ")"; })
687            .transition()
688     .duration(duration);
689
690     // update text
691     nodeUpdate.select("text")
692                            .text(function(d) { return (d.data.txt); });
693
694
695  // Remove any exiting nodes
696  var nodeExit = node.exit().transition()
697      .duration(duration)
698      .attr("transform", function(d) {
699          return "translate(" + source.y + "," + source.x + ")";
700      })
701      .remove();
702
703  // On exit reduce the node circles size to 0
704    nodeExit.select('rect')
705      .attr('width', 1e-6)
706      .attr('height', 1e-6);
707
708  // On exit reduce the opacity of text labels
709  nodeExit.select('text')
710    .style('fill-opacity', 1e-6);
711
712  // ****************** links section ***************************
713
714  // Update the links...
715  var link = svg.selectAll('path.link')
716      .data(links, function(d) { return d.id; });
717
718  // Enter any new links at the parent's previous position.
719  var linkEnter = link.enter().insert('path', "g")
720      .attr("class", "link")
721      .attr('d', function(d){
722        var o = {x: source.x0, y: source.y0}
723        return diagonal(o, o)
724      });
725
726  // UPDATE
727  var linkUpdate = linkEnter.merge(link);
728
729  // Transition back to the parent element position
730     linkUpdate.transition()
731               .attr('stroke', function(d) {
732                   switch(d.data.typ) {
733                       case 'structure':
734                           return structure_stroke;
735                       case 'seed':
736                           return expand_stroke;
737                       case 'collapse':
738                           return collapse_stroke;
739                   }})
740      .duration(duration)
741      .attr('d', function(d){ return diagonal(d, d.parent) });
742
743  // Remove any exiting links
744  var linkExit = link.exit().transition()
745      .duration(duration)
746      .attr('d', function(d) {
747        var o = {x: source.x, y: source.y}
748        return diagonal(o, o)
749      })
750      .remove();
751
752  // Store the old positions for transition.
753  nodes.forEach(function(d){
754    d.x0 = d.x;
755    d.y0 = d.y;
756  });
757
758  // Creates a curved (diagonal) path from parent to the child nodes
759  function diagonal(s, d) {
760
761    path = `M ${s.y} ${s.x}
762            C ${(s.y + d.y) / 2} ${s.x},
763              ${(s.y + d.y) / 2} ${d.x},
764              ${d.y} ${d.x}`
765
766    return path
767  }
768 }
769
770 function textSize(text) {
771     if (!d3) return;
772     var container = d3.select('body').append('svg');
773     container.append('text').attr("x", -99999).attr( "y", -99999 ).text(text);
774     var size = container.node().getBBox();
775     container.remove();
776     return { width: size.width + 30, height: size.height + 10 };
777 }
778
779
780</script>
781</body>
782
783"###;