diff --git a/makesvg.py b/makesvg.py
new file mode 100644
index 00000000..ed72392e
--- /dev/null
+++ b/makesvg.py
@@ -0,0 +1,136 @@
+from __future__ import annotations
+import dis
+
+import apsw, pprint, sys, apsw.ext, math, apsw.shell
+
+
+con = apsw.Connection(sys.argv[1])
+
+
+root, group, each = (apsw.ext.analyze_pages(con, n) for n in range(3))
+
+if False:
+ pprint.pprint(root)
+ pprint.pprint(group)
+
+# angles are 0.0 through 1.0
+# distance (radius) is 0 through 1.0
+
+# use this as radius for output coordinates as int
+RADIUS = 1000
+
+
+def colour_for_angle(angle: float) -> str:
+ # these are zero to one and cover how dark to light the colours
+ # become. base is added to each rgb, while span is how much
+ # the values range above that.
+ base = 0
+ span = 1
+ assert base + span <= 1
+
+ radians = angle * math.pi
+ third = 1 / 3 * math.pi
+
+ red = int(255 * (base + span * abs(math.cos(radians))))
+ green = int(255 * (base + span * abs(math.cos(third + radians))))
+ blue = int(255 * (base + span * abs(math.cos(third + third + radians))))
+
+ return f"#{red:02x}{green:02x}{blue:02x}"
+
+
+def pos_for_angle(angle: float, distance: float) -> tuple[float, float]:
+ "give x,y for distance from centre"
+
+ # the minus bit is because trig has east as 0 but we want north as
+ # zero
+ radians = angle * 2 * math.pi - (1 / 4 * 2 * math.pi)
+
+ return distance * math.cos(radians), distance * math.sin(radians)
+
+
+def c(v: float | list[float]) -> str:
+ # outputs a coordinate scaling by RADIUS
+ if isinstance(v, float):
+ return str(round(v * RADIUS))
+ return " ".join(str(round(x * RADIUS)) for x in v)
+
+
+def p(angle, distance):
+ return c(pos_for_angle(angle, distance))
+
+
+def slice(id: str, start_angle: float, end_angle: float, start_distance: float, end_distance: float):
+ assert 0 <= start_angle <= 1.0
+ assert 0 <= end_angle <= 1.0
+ assert end_angle > start_angle
+ assert 0 <= start_distance <= 1.0
+ assert 0 <= end_distance <= 1.0
+ assert end_distance > start_distance
+
+ l = 1 if (end_angle - start_angle) > 1 / 2 else 0
+
+ d = []
+ d.append(f"M {p(start_angle, start_distance)}")
+ d.append(f"L {p(start_angle, end_distance)}")
+ d.append(f"A {c(end_distance)} {c(end_distance)} 0 {l} 1 {p(end_angle, end_distance)}")
+ d.append(f"L {p(end_angle, start_distance)}")
+ d.append(f"A {c(start_distance)} {c(start_distance)} 0 {l} 0 {p(start_angle, start_distance)}")
+
+ ds = " ".join(d)
+
+ fill = colour_for_angle((start_angle + end_angle) / 2)
+ return f""""""
+
+def text(id: str, usage)->str:
+ return f"""{usage}"""
+
+
+# controls how much whitespace is around the edges
+OVERSCAN = 1.2
+header = f""""
+
+# maps which element hovering over causes a response on
+hover_response :dict[str,str]= {}
+
+id_counter = 0
+def next_id():
+ global id_counter
+ id_counter+=1
+ return f"id{id_counter}"
+
+out = [header]
+
+# inner summary circle
+id = next_id()
+out.append(f"""""")
+
+resp=next_id()
+out.append(text(resp, root))
+hover_response[id]=resp
+
+
+
+ring1_start = 0
+for name, usage in group.items():
+ ring1_proportion = usage.pages_used / root.pages_total
+ id=next_id()
+ out.append(slice(id, ring1_start, ring1_start + ring1_proportion, 1 / 3, 0.6))
+ ring2_start = ring1_start
+ ring1_start += ring1_proportion
+
+ for child in sorted(usage.tables + usage.indices):
+ usage2 = each[child]
+ ring2_proportion = usage2.pages_used / root.pages_total
+ id=next_id()
+ out.append(slice(id, ring2_start, ring2_start + ring2_proportion, 2 / 3, 1.0))
+ ring2_start += ring2_proportion
+
+out.append("")
+
+with open(sys.argv[2], "wt") as f:
+ f.write("\n".join(out))