Skip to content

Commit

Permalink
Make mermaid diagrams downloadable captionable figures (#477)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonatan Kłosko <[email protected]>
  • Loading branch information
christhekeele and jonatanklosko authored Oct 15, 2024
1 parent f538e63 commit 743739e
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 33 deletions.
36 changes: 36 additions & 0 deletions assets/packs/mermaid/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.container {
display: flex;
align-items: flex-start;
gap: 8px;
width: max-content;
}

.container .download-btn {
cursor: pointer;
padding: 0.25rem;
color: #61758A;
background: none;
border: none;
}

.container .download-btn:hover {
color: #0D1829;
}

.container:not(:hover) .download-btn {
opacity: 0;
}

.figure {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
width: max-content;
}

.caption {
margin-top: 8px;
font-size: 0.875rem;
color: #61758a;
}
51 changes: 47 additions & 4 deletions assets/packs/mermaid/main.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,61 @@
import mermaid from "mermaid";
import "./main.css";

mermaid.initialize({ startOnLoad: false });

export function init(ctx, content) {
export function init(ctx, { diagram, caption, download }) {
ctx.importCSS("main.css");

function render() {
mermaid.render("graph1", content).then(({ svg, bindFunctions }) => {
ctx.root.innerHTML = svg;
mermaid.render("diagram", diagram).then(({ svg, bindFunctions }) => {
// Fix for: https://github.com/mermaid-js/mermaid/issues/1766
svg = svg.replace(/<br>/gi, "<br/>");

let container = document.createElement("div");
container.classList.add("container");
ctx.root.appendChild(container);

const figure = document.createElement("figure");
figure.classList.add("figure");
figure.innerHTML = svg;
container.appendChild(figure);

if (caption) {
const figcaption = document.createElement("figcaption");
figcaption.classList.add("caption");
figcaption.textContent = caption;
figure.appendChild(figcaption);
}

if (download) {
const downloadBtn = document.createElement("button");
downloadBtn.classList.add("download-btn");
downloadBtn.title = "Download";
downloadBtn.innerHTML = `<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 10H18L12 16L6 10H11V3H13V10ZM4 19H20V12H22V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V12H4V19Z"></path></svg>`;
container.appendChild(downloadBtn);

downloadBtn.addEventListener("click", (event) => {
const blobURL = URL.createObjectURL(
new Blob([svg], { type: "image/svg+xml" }),
);

const a = document.createElement("a");
a.style.display = "none";
a.href = blobURL;
a.download = "diagram.svg";

container.appendChild(a);
a.click();
container.removeChild(a);
});
}

if (bindFunctions) {
bindFunctions(ctx.root);
}

// A workaround for https://github.com/mermaid-js/mermaid/issues/1758
const svgEl = ctx.root.querySelector("svg");
const svgEl = figure.querySelector("svg");
svgEl.removeAttribute("height");
});
}
Expand Down
1 change: 1 addition & 0 deletions lib/assets/mermaid/build/main.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/assets/mermaid/build/main.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 20 additions & 6 deletions lib/kino/mermaid.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
defmodule Kino.Mermaid do
@moduledoc ~S'''
A kino for rendering Mermaid graphs.
A kino for rendering Mermaid diagrams.
> #### Relation to Kino.Markdown {: .info}
>
> Mermaid graphs can also be generated dynamically with `Kino.Markdown`,
> Mermaid diagrams can also be generated dynamically with `Kino.Markdown`,
> however the output of `Kino.Markdown` is never persisted in the
> notebook source. `Kino.Mermaid` doesn't have this limitation.
Expand All @@ -25,10 +25,24 @@ defmodule Kino.Mermaid do
@type t :: Kino.JS.t()

@doc """
Creates a new kino displaying the given Mermaid graph.
Creates a new kino displaying the given Mermaid diagram.
## Options
* `:caption` - an optional caption for the rendered diagram.
* `:download` - whether or not to show a button for downloading
the diagram as a SVG. Defaults to `true`.
"""
@spec new(binary()) :: t()
def new(content) do
Kino.JS.new(__MODULE__, content, export: fn content -> {"mermaid", content} end)
@spec new(binary(), keyword()) :: t()
def new(diagram, opts \\ []) do
opts = Keyword.validate!(opts, caption: nil, download: true)

Kino.JS.new(
__MODULE__,
%{diagram: diagram, caption: opts[:caption], download: opts[:download]},
export: fn diagram -> {"mermaid", diagram} end
)
end
end
60 changes: 42 additions & 18 deletions lib/kino/process.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ defmodule Kino.Process do
* `:render_ets_tables` - determines whether ETS tables associated with the
supervision tree are rendered. Defaults to `false`.
* `:caption` - an optional caption for the diagram. Either a custom
caption as string, or `nil` to disable the default caption.
## Examples
To view the applications running in your instance run:
Expand Down Expand Up @@ -86,13 +89,18 @@ defmodule Kino.Process do
{:dictionary, dictionary} = process_info(root_supervisor, :dictionary)
[ancestor] = dictionary[:"$ancestors"]

Mermaid.new("""
graph #{direction};
application_master(#{inspect(master)}):::supervisor ---> supervisor_ancestor;
supervisor_ancestor(#{inspect(ancestor)}):::supervisor ---> 0;
#{edges}
#{@mermaid_classdefs}
""")
caption = Keyword.get(opts, :caption, "Application tree for #{inspect(application)}")

Mermaid.new(
"""
graph #{direction};
application_master(#{inspect(master)}):::supervisor ---> supervisor_ancestor;
supervisor_ancestor(#{inspect(ancestor)}):::supervisor ---> 0;
#{edges}
#{@mermaid_classdefs}
""",
caption: caption
)
end

@doc """
Expand All @@ -108,6 +116,9 @@ defmodule Kino.Process do
* `:direction` - defines the direction of the graph visual. The
value can either be `:top_down` or `:left_right`. Defaults to `:top_down`.
* `:caption` - an optional caption for the diagram. Either a custom
caption as string, or `nil` to disable the default caption.
## Examples
With a supervisor definition like so:
Expand Down Expand Up @@ -162,11 +173,16 @@ defmodule Kino.Process do

edges = traverse_supervisor(supervisor_pid, opts)

Mermaid.new("""
graph #{direction};
#{edges}
#{@mermaid_classdefs}
""")
caption = Keyword.get(opts, :caption, "Supervisor tree for #{inspect(supervisor)}")

Mermaid.new(
"""
graph #{direction};
#{edges}
#{@mermaid_classdefs}
""",
caption: caption
)
end

@doc """
Expand Down Expand Up @@ -236,6 +252,9 @@ defmodule Kino.Process do
is used. However, if the function returns a `String.t()`, then
that will be used for the label.
* `:caption` - an optional caption for the diagram. Either a custom
caption as string, or `nil` to disable the default caption.
## Examples
To generate a trace of all the messages occurring during the execution of the
Expand Down Expand Up @@ -412,13 +431,18 @@ defmodule Kino.Process do
|> Enum.reverse()
|> Enum.join("\n")

caption = Keyword.get(opts, :caption, "Messages traced from #{inspect(trace_pids)}")

sequence_diagram =
Mermaid.new("""
%%{init: {'themeCSS': '.actor:last-of-type:not(:only-of-type) {dominant-baseline: hanging;}'} }%%
sequenceDiagram
#{participants}
#{messages}
""")
Mermaid.new(
"""
%%{init: {'themeCSS': '.actor:last-of-type:not(:only-of-type) {dominant-baseline: hanging;}'} }%%
sequenceDiagram
#{participants}
#{messages}
""",
caption: caption
)

{func_result, sequence_diagram}
end
Expand Down
6 changes: 3 additions & 3 deletions lib/kino/shorts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ defmodule Kino.Shorts do
def text(text), do: Kino.Text.new(text)

@doc ~S'''
Renders Mermaid graphs.
Renders Mermaid diagrams.
It is a wrapper around `Kino.Mermaid.new/1`.
Expand All @@ -178,8 +178,8 @@ defmodule Kino.Shorts do
C-->D;
""")
'''
@spec mermaid(String.t()) :: Kino.Mermaid.t()
def mermaid(mermaid), do: Kino.Mermaid.new(mermaid)
@spec mermaid(String.t(), keyword()) :: Kino.Mermaid.t()
def mermaid(diagram, opts \\ []), do: Kino.Mermaid.new(diagram, opts)

@doc """
A placeholder for static outputs that can be dynamically updated.
Expand Down
2 changes: 1 addition & 1 deletion test/kino/process_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ defmodule Kino.ProcessTest do
send(Kino.JS.DataStore, {:connect, self(), %{origin: "client:#{inspect(self())}", ref: ref}})
assert_receive {:connect_reply, data, %{ref: ^ref}}

data
data.diagram
end

defp supervision_tree_with_ets_table do
Expand Down

0 comments on commit 743739e

Please sign in to comment.