├── .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 | [](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 |
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 |
4 |
--------------------------------------------------------------------------------
/img/ellipse.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/img/rectangle.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------