diff --git a/docs/morphisms/index.html b/docs/morphisms/index.html
new file mode 100644
index 0000000..ff87ecb
--- /dev/null
+++ b/docs/morphisms/index.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/morphisms/js/d3_visualisation.js b/docs/morphisms/js/d3_visualisation.js
new file mode 100644
index 0000000..5543cc5
--- /dev/null
+++ b/docs/morphisms/js/d3_visualisation.js
@@ -0,0 +1,221 @@
+function drag(simulation) {
+
+ function dragstarted(event, d) {
+ if (!event.active) simulation.alphaTarget(0.3).restart();
+ d.fx = d.x;
+ d.fy = d.y;
+ }
+
+ function dragged(event, d) {
+ d.fx = event.x;
+ d.fy = event.y;
+ }
+
+ function dragended(event, d) {
+ if (!event.active) simulation.alphaTarget(0);
+ d.fx = null;
+ d.fy = null;
+ }
+
+ return d3.drag()
+ .on("start", dragstarted)
+ .on("drag", dragged)
+ .on("end", dragended);
+}
+
+function linkArc(d) {
+ const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y);
+ return `
+ M${d.source.x},${d.source.y}
+ A${r},${r} 0 0,1 ${d.target.x},${d.target.y}
+ `;
+}
+
+function highlight(node) {
+ return node.transition()
+ .duration('400')
+ .attr('opacity', 1)
+ .attr('filter', 'sepia(0.0)')
+ ;
+}
+
+function dehighlight(node) {
+ return node.transition()
+ .duration('400')
+ .attr('opacity', 0.3)
+ .attr('filter', 'sepia(0.8)')
+ ;
+}
+
+function _union(...arr) {
+ return arr.reduce((first, second) => [...new Set(first.concat(second))]);
+}
+
+async function create_svg(
+ data,
+ width,
+ height,
+ ) {
+ let links = data.links;
+ let nodes = data.nodes;
+ let types = Array.from(new Set(nodes.map(d => d.level)));
+ let color = d3.scaleOrdinal(types, d3.schemeSet1);
+
+ d3.select("datalist")
+ .selectAll("option")
+ .data(nodes)
+ .join("option")
+ .attr("value", n=>n.id)
+ .text(n=>n.id)
+
+
+
+ const simulation = d3.forceSimulation(nodes)
+ .force("link", d3.forceLink(links).id(d => d.id))
+ .force("charge", d3.forceManyBody().strength(-400))
+ .force("center", d3.forceCenter(width/2, height/2))
+ .force("x", d3.forceX())
+ .force("y", d3.forceY());
+
+ const svg = d3.create("svg")
+ .attr("preserveAspectRatio", "xMinYMin meet")
+ .attr("viewBox", [0, 0, width, height])
+ .classed("svg-content", true)
+ .style("font", "12px sans-serif");
+
+ // Per-type markers, as they don't inherit styles.
+ svg.append("defs").selectAll("marker")
+ .data(types)
+ .join("marker")
+ .attr("id", d => `arrow-${d}`)
+ .attr("viewBox", "0 -5 10 10")
+ .attr("refX", 15)
+ .attr("refY", 0)
+ .attr("markerWidth", 6)
+ .attr("markerHeight", 6)
+ .attr("orient", "auto")
+ .append("path")
+ .attr("fill", d=>color(d))
+ .attr("d", "M0,-5 L10,0 L0,5 z")
+ ;
+
+ const svg_content = svg.append("g")
+
+ const link = svg_content.append("g")
+ .attr("fill", "none")
+ .attr("stroke-width", 1.5)
+ .selectAll("path")
+ .data(links)
+ .join("path")
+ .attr("stroke", d => color(d.source.level))
+ .attr("marker-end", d => `url(${new URL(`#arrow-${d.source.level}`, location)})`)
+ .attr("opacity", 0.3)
+ .attr("filter", "sepia(0.8)")
+ ;
+
+ function find_descendants(node) {
+ let desc = links.filter(l => l.source.id == node.id );
+ if (desc.length == 0) {
+ return []
+ } else {
+ let all_desc = desc
+ .map(d => find_descendants(d.target))
+ .reduce(
+ (total, item) => Array.from(new Set(total.concat(item))),
+ desc.map(l=>l.target)
+ )
+ ;
+ return all_desc;
+ // return _union(desc, _union(...desc.map(l=>find_descendants(l.target))));
+ }
+ };
+
+ const descendants = {};
+
+ nodes.forEach((node) => {
+ let desc = find_descendants(node);
+ desc.push(node)
+ descendants[node.id] = desc;
+ });
+
+ console.log(descendants)
+
+ function foreground_descendants(id) {
+ node.classed("foreground", (n) => {
+ return (descendants[id].find(v => v.id == n.id)) ? true : false;
+ });
+
+ link.classed("foreground", (n) => {
+ let verts = descendants[id]
+ return (verts.includes(n.source) && verts.includes(n.target)) ? true : false;
+ });
+ }
+
+ d3.select("input").on("input", function () {
+ let n = nodes.find(n => n.id == this.value)
+ if (n) {
+ foreground_descendants(n.id)
+ // let transform = {k:1, x:n.x, y:n.y}
+ console.log(n)
+
+ d3.zoom().translateTo(svg_content,n.x, n.y)
+ // console.log(transform)
+ // svg_content.attr("transform", `translate(${n.x/2},${n.y/2})`)
+ }
+ });
+
+ const node = svg_content.append("g")
+ .attr("stroke-linecap", "round")
+ .attr("stroke-linejoin", "round")
+ .selectAll("g")
+ .data(nodes)
+ .join("g")
+ .attr("class", d=>d.id)
+ .attr("opacity", 0.3)
+ .attr("filter", "sepia(0.8)")
+ .on("mouseover", function (d, i) {
+ highlight(d3.select(this));
+ })
+ .on("mouseout", function (d, i) {
+ dehighlight(d3.select(this))
+ })
+ .on("click", function (d, i) {
+ console.log(this)
+ let id = this.classList[0];
+ foreground_descendants(id);
+ })
+ .call(drag(simulation));
+
+ // circles for nodes:
+ node.append("circle")
+ .attr("stroke", "white")
+ .attr("stroke-width", 1.5)
+ .attr("r", 5)
+ .attr("fill", d => color(d.level));
+
+ node.append("foreignObject")
+ .attr("x", 10)
+ .attr("y", "0.31em")
+ .clone(true).lower()
+ .attr("fill", "none")
+ .attr("stroke", "white")
+ .attr("stroke-width", 5)
+ .append(d => createMathSpan(d.id));
+
+ const zoom = d3.zoom()
+ .scaleExtent([0.2, 5])
+ // .translateExtent([[0, 0], [width, height]])
+ .on("zoom", (e) => {
+ console.log(e.transform)
+ svg_content.attr("transform", e.transform)
+ });
+
+ svg.call(zoom)
+
+ simulation.on("tick", () => {
+ link.attr("d", linkArc);
+ node.attr("transform", d => `translate(${d.x},${d.y})`);
+ });
+
+ return svg;
+}
diff --git a/docs/morphisms/js/morphisms.js b/docs/morphisms/js/morphisms.js
new file mode 100644
index 0000000..d8807e7
--- /dev/null
+++ b/docs/morphisms/js/morphisms.js
@@ -0,0 +1,83 @@
+const morphisms_url = new URL("https://raw.githubusercontent.com/kalmarek/SmallHyperbolic/mk/morphisms/data/triangle_groups_morphisms.json")
+
+async function fetch_json(url) {
+ try {
+ let response = await fetch(url);
+ let json = await response.json();
+ return json;
+ } catch (err) {
+ console.log("Error while fetching json:" + err);
+ }
+}
+
+
+ ;
+
+async function place_svg(svg) {
+ d3.select("div.canvas")
+ .append("div")
+ .attr("class", "container-fluid")
+ .attr("class", "svg-container")
+ .node()
+ .appendChild(svg.node());
+};
+
+async function add_search() {
+ let input_grp = d3.select("div.canvas")
+ .append("div")
+ .classed("search-field", true)
+ .append("div")
+ .classed("container", true)
+ // .append("div")
+ // .classed("input-group", true)
+ ;
+ // let floating = input_grp.insert("div")
+ // .attr("class", "form-floating")
+
+ let input = input_grp.insert("input")
+ .attr("class", "form-control")
+ .attr("list", "datalistOptions")
+ .attr("id", "groupSearch")
+ .attr("placeholder", "Type to search...");
+
+ // input_grp.insert("label")
+ // .attr("for", "groupSearch")
+ // .text("Type to search...")
+
+ input_grp.insert("datalist")
+ .attr("id", "datalistOptions")
+
+ // input_grp.append("button")
+ // .classed("btn btn-primary", true)
+ // .attr("type", "button")
+ // .attr("id", "searchBtn")
+ // .append("i")
+ // .classed("bi-search", true)
+ // ;
+}
+
+async function show_katex() {
+ let math_objects = document.getElementsByClassName("math");
+ let toggle = true;
+ for (let elt of math_objects) {
+ toggleKaTeX(elt, toggle);
+ let fObj = elt.parentElement;
+ let rect = elt.getElementsByClassName("math-tex")[0].getBoundingClientRect();
+ fObj.setAttribute("width", rect.width+4);
+ fObj.setAttribute("height", rect.height+4);
+ }
+};
+
+add_search()
+
+fetch_json(morphisms_url)
+ // .then(async (data) => { console.log(data); return data;})
+ .then(async (data) => {
+ return create_svg(data, window.innerWidth, window.innerHeight);
+ })
+ // .then(async (data) => { console.log(data); return data; })
+ .then(place_svg)
+ .then(show_katex)
+ // .then(add_search)
+;
+