├── .gitignore ├── LICENSE ├── README.md ├── img ├── circle.svg ├── ellipse.svg └── rectangle.svg └── src └── svg-viewer-in-svg /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | \#*\# 3 | .\#* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, Stevan Andjelkovic 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the 14 | distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SVG viewer written in SVG 2 | 3 | This post is about how to write an [SVG](https://en.wikipedia.org/wiki/SVG) 4 | viewer / browser / "slideshow" which is itself a self-contained SVG. 5 | 6 | ## Motivation 7 | 8 | I've been working on a parallel processing pipeline. Each stage of the pipeline 9 | is running on a separate thread, and it takes some work items from a queue in 10 | front of it, processes them and then puts them in the queue in front of the next 11 | stage in the pipeline. 12 | 13 | In order to better understand what exactly is going on I thought I'd visualise 14 | the pipeline including the length and contents of all queues as well as the 15 | position each worker/stage is at in the queue it's processing. 16 | 17 | For now lets just imagine that an SVG image is created every time interval. So 18 | after a run of the pipeline we'll have a bunch of SVGs showing us how it evolved 19 | over time. 20 | 21 | Initially I was using the [`feh`](https://feh.finalrewind.org/) image viewer, 22 | which if you pass it several images lets you navigate through them using the 23 | arrow keys. 24 | 25 | But then I wondered: how can I show these SVGs to somebody else over the web? 26 | 27 | ## Demo 28 | 29 | Before I show you how I did it, let's have a look at the resulting pipeline 30 | visualisation (you need to click on the image): 31 | 32 | [![Demo](https://stevana.github.io/svg-viewer-in-svg/wordcount-pipeline.svg)](https://stevana.github.io/svg-viewer-in-svg/wordcount-pipeline.svg) 33 | 34 | The arrows in the top left corner are clickable and will take you to the first, 35 | next, previous and last SVG respectively. 36 | 37 | What you are seeing is a run of a parallel word count pipeline, where lines are 38 | coming in from stdin and the counts are being written to stdout at the end. 39 | 40 | ## The code 41 | 42 | Let's start by having a look at the SVG itself. 43 | 44 | ```xml 45 | 46 | 47 | // The navigation menu for going to the first, previous, next and last 48 | // slide/image. There's also a progress bar here which shows which slide 49 | // we are currently on and how many there are in total. 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | // Placeholder for the image. 59 | 60 | 61 | // The index of the currently viewed image. 62 | 63 | 64 | // The fact that we can embedd JavaScript into SVGs is what makes this 65 | // whole thing work. 66 | 74 | 75 | ``` 76 | 77 | The following goes into the script tag above: 78 | 79 | ```javascript 80 | // Array holding the SVG images. 81 | const imgs = new Array( 82 | "...", 83 | "...", 84 | "...", 85 | ); 86 | 87 | // Helper for registering onclick handlers. 88 | function registerClick(selector, f) { 89 | document.querySelector(selector).addEventListener("click", (e) => { 90 | f(e); 91 | }); 92 | } 93 | 94 | // Set and return the value of our counter, this is abusing the id 95 | // of the desc tag... 96 | function setCounter(f) { 97 | const counter = document.querySelector("desc"); 98 | counter.id = f(parseInt(counter.id)); 99 | return counter.id; 100 | } 101 | 102 | // Updates our image placeholder by injecting the SVG into the 103 | // image tag. Also updates the progress bar. 104 | function setImage(i) { 105 | const img = document.querySelector("#image"); 106 | img.setAttribute("href", imgs[i]); 107 | updateProgress(); 108 | } 109 | 110 | // Update the progress bar in the menu. 111 | function updateProgress() { 112 | document.querySelector("#progress").innerHTML = 113 | document.querySelector("desc").id + "/" + (imgs.length - 1); 114 | } 115 | 116 | // We can now define our navigation functions in terms of setting 117 | // the counter and the image. 118 | 119 | function first() { 120 | setImage(setCounter((_) => 0)); 121 | } 122 | 123 | function previous() { 124 | setImage(setCounter((i) => i <= 0 ? 0 : --i)); 125 | } 126 | 127 | function next() { 128 | setImage(setCounter((i) => i >= imgs.length - 1 ? imgs.length - 1 : ++i)); 129 | } 130 | 131 | function last() { 132 | setImage(setCounter((_) => imgs.length - 1)); 133 | } 134 | 135 | // Finally, to kick things off: register onclick handlers for the 136 | // navigation buttons and set the image to the first image in the array. 137 | registerClick("#first", (_) => first()); 138 | registerClick("#next", (_) => next()); 139 | registerClick("#previous", (_) => previous()); 140 | registerClick("#last", (_) => last()); 141 | setImage(0); 142 | 143 | // We could even add keyboard support... 144 | window.addEventListener("keydown", (e) => { 145 | // Left arrow or k. 146 | if (e.keyCode === 37 || e.keyCode === 75) { 147 | previous(); 148 | } 149 | // Right arrow or j. 150 | else if (e.keyCode === 39 || e.keyCode === 74) { 151 | next(); 152 | } 153 | }); 154 | ``` 155 | 156 | Another thing worth mentioning is that in my application the thread that 157 | collects the metrics runs about 1000 times per second. If there's no change in 158 | the metrics then we probably don't want to display an image that's the same as 159 | the previous one. So I keep a CRC32 checksum of the metrics that the last image 160 | is generated from and if the next metrics data has the same checksum, I skip 161 | generating that image (as it will be the same as the previous one). 162 | 163 | The (inner) SVGs themselves are generated with graphviz via the 164 | [dot](https://graphviz.org/doc/info/lang.html) language, the 165 | [record-based](https://graphviz.org/doc/info/shapes.html#record) node shapes 166 | turned out to be useful for visualing data structures. 167 | 168 | It's quite annoying to populate the `imgs` array by hand, so I wrote a small 169 | bash [script](https://github.com/stevana/svg-viewer-in-svg/tree/main/src) which 170 | takes a bunch of SVGs and outputs a single SVG which can be used to view the 171 | original images. 172 | 173 | ## Usage 174 | 175 | The easiest way to get started is probably to clone the repository. 176 | 177 | ```bash 178 | git clone https://github.com/stevana/svg-viewer-in-svg 179 | cd svg-viewer-in-svg 180 | ``` 181 | 182 | In the `img/` directory there are three simple SVGs: 183 | 184 | ```bash 185 | ls img/ 186 | circle.svg ellipse.svg rectangle.svg 187 | ``` 188 | 189 | We can combine them all into one a single SVG that is a "slideshow" of the 190 | shapes as follows: 191 | 192 | ```bash 193 | ./src/svg-viewer-in-svg img/*.svg > /tmp/combined-shapes.svg 194 | firefox /tmp/combined-shapes.svg 195 | ``` 196 | 197 | If you want to "install" the script, simply copy it to any directory that is in 198 | your `$PATH`. 199 | 200 | One last thing worth noting is that hosting these combined SVGs on GitHub is a 201 | bit of a pain. Merely checking them into a repository and trying to include them 202 | in markdown won't work, because GitHub appears to be doing some SVG script tag 203 | sanitising for security reasons. Uploading them to gh-pages and linking to those 204 | seems to work though[^1]. 205 | 206 | ## Contributing 207 | 208 | I hope I've managed to inspire you to think about how to visualise the execution 209 | of your own programs! Feel free to copy and adapt the code as you see fit. If 210 | you come up with some interesting modifications or better ways of doing things, 211 | then please do share! 212 | 213 | ## See also 214 | 215 | Brendan Gregg's [flamegraphs](https://www.brendangregg.com/flamegraphs.html) 216 | also generates a clickable SVG. I got the idea of adding keyboard support from 217 | looking at his SVG, there's probably more interesting stuff to steal there. 218 | 219 | 220 | [^1]: The following [gist](https://gist.github.com/ramnathv/2227408) shows how 221 | to create gh-pages branch that doesn't have any history. Also see the GitHub 222 | pages documentation for how to enable gh-pages for a respository. 223 | -------------------------------------------------------------------------------- /img/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/svg-viewer-in-svg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | name=$(basename "$0") 4 | 5 | for file in "$@"; do 6 | if [ "${file: -4}" != ".svg" ]; then 7 | echo "$name: $file doesn't have .svg suffix" 8 | exit 1 9 | fi 10 | done 11 | 12 | cat < 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Counter used to keep track of the index. 26 | 27 | 107 | 108 | EOF 109 | --------------------------------------------------------------------------------