Skip to content

Conversation

@mert-kurttutan
Copy link
Contributor

No description provided.

@mert-kurttutan
Copy link
Contributor Author

To support embedding image, I had to introduce new fn to RenderBackend trait, which is a breaking change. This is something to take note of.

@mert-kurttutan
Copy link
Contributor Author

All the features are done except for image embedding inside HTML.

For image, the only format supported is local png files.

@azriel91
Copy link
Contributor

azriel91 commented Jun 8, 2025

Heya, I finally got to trying this out, and it's really nice that I could swap layout in in place of the WASM graphviz call.

  1. I noticed Html is part of ShapeKind, which means a node can't have both a shape, and an HTML label.

    Example, a node with shape=rect in graphviz used to be able to be rendered, with the inner label being a HTML label:

    image

    When I use the equivalent box in layout, it renders as:

    image

    I haven't gone through the code to see what needs to be changed to be able to have both the shape and the HTML label, but from a brief look, ShapeKind's variants would take String|HtmlString enum for Box/Circle etc., and Html wouldn't be a shape variant itself.

  2. Graph labels don't support HTMLString yet:

    1. parser.rs:321: DotParser::parse_attribute_stmt needs self.lex(); to be self.lex_html(); if we know we're in an HTML supported attribute (like label).
    2. Maybe that means copying DotParser::parse_attribute_stmt, but with a version that could return String|HtmlString, and parser.rs:158: DotParser::parse_stmt calls that version of the function when the attribute identifier is label (or any other HTML supported attribute).
  3. I'll try out the images part soon.


Also, I'll try to chip in later this year (probably September+, if not before) -- my project relies on subgraphs / clusters.

@mert-kurttutan
Copy link
Contributor Author

@azriel91 For documentation purposes, could you share the dot files to reproduce images?

@mert-kurttutan
Copy link
Contributor Author

The reasoning behind was that html string was supposed to be generalization of record based shapes, see here. Since records dont render with other shapes (due to enum structure), I used the same enum structure.

With this observation, it is correct to rather say that they are generalization of label strings.

@mert-kurttutan
Copy link
Contributor Author

mert-kurttutan commented Jun 8, 2025

Now, it should handle the cases you mentioned above.

Also, this last change introduces some api breakage due to introduction of shapecontent

@mert-kurttutan
Copy link
Contributor Author

mert-kurttutan commented Jun 8, 2025

It turns out graph level labels are not processed at all even before this commit. The last commit processes htmlstring. But, since the labels are not rendered, it has no effect on the end image.

@azriel91
Copy link
Contributor

azriel91 commented Jun 9, 2025

heya, that's fast 🙇!

The dot source is "big", a sample is at https://azriel.im/dot_ix, click on the second tab on the top left (if the page loads weird, refresh -- there's sometimes an ordering issue on first load).

The site isn't using this branch yet, but if you do want to try, it's this:

https://github.com/azriel91/dot_ix/tree/feature/switch-graphviz-for-layout-rs

to run, it's cd playground && trunk serve

I'll try the updated branch tomorrow (am on my phone now).

@azriel91
Copy link
Contributor

azriel91 commented Jun 10, 2025

heya, here's an example dot file (live page):

wall of text

digraph G {
    compound  = true
    graph [
        margin    = 0.1
        nodesep   = 0.25
        ranksep   = 0.25
        bgcolor   = "transparent"
        fontname  = "helvetica"
        packmode  = "array_t"
        fontcolor = "#222222"
        fontsize  = 10
        rankdir   = TB
    ]
    node [
        fontcolor = "#111111"
        fontname  = "liberationmono"
        fontsize  = 10
        shape    = "circle"
                style     = "filled"
        width     = 0.3
        height    = 0.1
        margin    = "0.11,0.055"
    ]
    edge [
        constraint = true,
        dir        = forward,
        minlen     = 2,
        fontname   = "liberationmono"
        fontsize   = 10
        arrowsize  = 0.7
        color      = "#333333"
        fontcolor  = "#222222"
    ]
    subgraph cluster_tag_legend {
        margin = 10
        label = <<b>Legend</b>>
        style = rounded
        subgraph cluster_tag_one_and_two {
            label     = <   Both  >
            width     = 0.3
            height    = 0.1
            margin    = "0.030,0.020"
            fontname  = "liberationmono"
            fontsize  = 8
            class     = "outline-none [&>path]:fill-emerald-200 [&>path]:stroke-emerald-500 [&>path]:hover:fill-emerald-100 [&>path]:hover:stroke-emerald-400 [&>path]:focus:fill-lime-200 [&>path]:focus:outline-1 [&>path]:focus:outline-lime-600 [&>path]:focus:outline-dashed [&>path]:focus:rounded-xl cursor-pointer peer/tag_one_and_two"
            penwidth  = 1
            // invisible node for cluster to appear
            tag_one_and_two [
                fixedsize = true
                width     = 0.01
                height    = 0.01
                margin    = "0.0,0.0"
                shape     = point
                style     = invis
            ]
        }
        subgraph cluster_tag_two {
            label     = <Group Two>
            width     = 0.3
            height    = 0.1
            margin    = "0.030,0.020"
            fontname  = "liberationmono"
            fontsize  = 8
            class     = "outline-none [&>path]:fill-emerald-200 [&>path]:stroke-emerald-500 [&>path]:hover:fill-emerald-100 [&>path]:hover:stroke-emerald-400 [&>path]:focus:fill-lime-200 [&>path]:focus:outline-1 [&>path]:focus:outline-lime-600 [&>path]:focus:outline-dashed [&>path]:focus:rounded-xl cursor-pointer peer/tag_two"
            penwidth  = 1
            // invisible node for cluster to appear
            tag_two [
                fixedsize = true
                width     = 0.01
                height    = 0.01
                margin    = "0.0,0.0"
                shape     = point
                style     = invis
            ]
        }
        subgraph cluster_tag_one {
            label     = <Group One>
            width     = 0.3
            height    = 0.1
            margin    = "0.030,0.020"
            fontname  = "liberationmono"
            fontsize  = 8
            class     = "outline-none [&>path]:fill-emerald-200 [&>path]:stroke-emerald-500 [&>path]:hover:fill-emerald-100 [&>path]:hover:stroke-emerald-400 [&>path]:focus:fill-lime-200 [&>path]:focus:outline-1 [&>path]:focus:outline-lime-600 [&>path]:focus:outline-dashed [&>path]:focus:rounded-xl cursor-pointer peer/tag_one"
            penwidth  = 1
            // invisible node for cluster to appear
            tag_one [
                fixedsize = true
                width     = 0.01
                height    = 0.01
                margin    = "0.0,0.0"
                shape     = point
                style     = invis
            ]
        }
        tag_one -> tag_two [style = invis, minlen = 1]
        tag_two -> tag_one_and_two [style = invis, minlen = 1]
    }
    subgraph cluster_a_tasks {
        margin = "8"
        label = <<table
            border="0"
            cellborder="0"
            cellpadding="0"
            cellspacing="0"
        >
            <tr>
                <td align="left" balign="left"></td>
            </tr>
        </table>>
        style = "filled,rounded"
        class = "outline-none [&>path]:focus:outline-blue-500 [&>path]:focus:outline-2 [&>path]:focus:outline-dashed [&>path]:stroke-blue-400 [&>path]:stroke-1 [&>path]:[stroke-dasharray:2] [&>path]:focus:stroke-blue-500 [&>path]:focus:stroke-1 [&>path]:focus:[stroke-dasharray:2] [&>path]:focus:hover:stroke-blue-300 [&>path]:focus:hover:stroke-1 [&>path]:focus:hover:[stroke-dasharray:2] [&>path]:hover:stroke-blue-300 [&>path]:hover:stroke-1 [&>path]:hover:[stroke-dasharray:2] [&>path]:focus:active:stroke-blue-500 [&>path]:focus:active:stroke-1 [&>path]:focus:active:[stroke-dasharray:2] [&>path]:fill-blue-100 [&>path]:focus:fill-blue-100 [&>path]:focus:hover:fill-blue-50 [&>path]:hover:fill-blue-50 [&>path]:focus:active:fill-blue-100 px-1.5 py-1.5 cursor-pointer "
        subgraph cluster_a0 {
            label = <>
            margin = "0.11,0.055"
            class = "outline-none"
            a0 [
                label = ""
                class = "outline-none [&>ellipse]:focus:outline-blue-500 [&>ellipse]:focus:outline-2 [&>ellipse]:focus:outline-dashed [&>ellipse]:stroke-slate-600 [&>ellipse]:stroke-1 [&>ellipse]:focus:stroke-slate-500 [&>ellipse]:focus:stroke-1 [&>ellipse]:focus:hover:stroke-slate-400 [&>ellipse]:focus:hover:stroke-1 [&>ellipse]:hover:stroke-slate-400 [&>ellipse]:hover:stroke-1 [&>ellipse]:focus:active:stroke-slate-500 [&>ellipse]:focus:active:stroke-1 [&>ellipse]:fill-slate-300 [&>ellipse]:focus:fill-slate-200 [&>ellipse]:focus:hover:fill-slate-100 [&>ellipse]:hover:fill-slate-100 [&>ellipse]:focus:active:fill-slate-200 px-1.5 py-1.5 cursor-pointer peer-focus/tag_one:[&>ellipse]:stroke-yellow-500 peer-focus/tag_one:[&>ellipse]:stroke-2 peer-focus/tag_one:[&>ellipse]:fill-yellow-200 "
                margin = "0.13,0.055"
            ]
            a0_text [
                fillcolor="#00000000"
                shape="rectangle"
                margin = "0.13,0.055"
                label = <<table
                    border="0"
                    cellborder="0"
                    cellpadding="0"
                    cellspacing="0"
                >
                    <tr>
                        <td align="left" balign="left">a0</td>
                    </tr>
                </table>>
            ]
        }
        subgraph cluster_a1 {
            label = <>
            margin = "0.11,0.055"
            class = "outline-none"
            a1 [
                label = ""
                class = "outline-none [&>ellipse]:focus:outline-blue-500 [&>ellipse]:focus:outline-2 [&>ellipse]:focus:outline-dashed [&>ellipse]:stroke-slate-600 [&>ellipse]:stroke-1 [&>ellipse]:focus:stroke-slate-500 [&>ellipse]:focus:stroke-1 [&>ellipse]:focus:hover:stroke-slate-400 [&>ellipse]:focus:hover:stroke-1 [&>ellipse]:hover:stroke-slate-400 [&>ellipse]:hover:stroke-1 [&>ellipse]:focus:active:stroke-slate-500 [&>ellipse]:focus:active:stroke-1 [&>ellipse]:fill-slate-300 [&>ellipse]:focus:fill-slate-200 [&>ellipse]:focus:hover:fill-slate-100 [&>ellipse]:hover:fill-slate-100 [&>ellipse]:focus:active:fill-slate-200 px-1.5 py-1.5 cursor-pointer "
                margin = "0.13,0.055"
            ]
            a1_text [
                fillcolor="#00000000"
                shape="rectangle"
                margin = "0.13,0.055"
                label = <<table
                    border="0"
                    cellborder="0"
                    cellpadding="0"
                    cellspacing="0"
                >
                    <tr>
                        <td align="left" balign="left">a1</td>
                    </tr>
                </table>>
            ]
        }
    }
    subgraph cluster_bcd_tasks {
        margin = "8"
        label = <<table
            border="0"
            cellborder="0"
            cellpadding="0"
            cellspacing="0"
        >
            <tr>
                <td align="left" balign="left"></td>
            </tr>
        </table>>
        style = "filled,rounded"
        class = "outline-none [&>path]:focus:outline-blue-500 [&>path]:focus:outline-2 [&>path]:focus:outline-dashed [&>path]:stroke-violet-400 [&>path]:stroke-1 [&>path]:[stroke-dasharray:2] [&>path]:focus:stroke-violet-500 [&>path]:focus:stroke-1 [&>path]:focus:[stroke-dasharray:2] [&>path]:focus:hover:stroke-violet-300 [&>path]:focus:hover:stroke-1 [&>path]:focus:hover:[stroke-dasharray:2] [&>path]:hover:stroke-violet-300 [&>path]:hover:stroke-1 [&>path]:hover:[stroke-dasharray:2] [&>path]:focus:active:stroke-violet-500 [&>path]:focus:active:stroke-1 [&>path]:focus:active:[stroke-dasharray:2] [&>path]:fill-violet-100 [&>path]:focus:fill-violet-100 [&>path]:focus:hover:fill-violet-50 [&>path]:hover:fill-violet-50 [&>path]:focus:active:fill-violet-100 px-1.5 py-1.5 cursor-pointer "
        subgraph cluster_b {
            label = <>
            margin = "0.11,0.055"
            class = "outline-none"
            b [
                label = ""
                class = "outline-none [&>ellipse]:focus:outline-blue-500 [&>ellipse]:focus:outline-2 [&>ellipse]:focus:outline-dashed [&>ellipse]:stroke-slate-600 [&>ellipse]:stroke-1 [&>ellipse]:focus:stroke-slate-500 [&>ellipse]:focus:stroke-1 [&>ellipse]:focus:hover:stroke-slate-400 [&>ellipse]:focus:hover:stroke-1 [&>ellipse]:hover:stroke-slate-400 [&>ellipse]:hover:stroke-1 [&>ellipse]:focus:active:stroke-slate-500 [&>ellipse]:focus:active:stroke-1 [&>ellipse]:fill-slate-300 [&>ellipse]:focus:fill-slate-200 [&>ellipse]:focus:hover:fill-slate-100 [&>ellipse]:hover:fill-slate-100 [&>ellipse]:focus:active:fill-slate-200 px-1.5 py-1.5 cursor-pointer peer-focus/tag_one:[&>ellipse]:stroke-yellow-500 peer-focus/tag_one:[&>ellipse]:stroke-2 peer-focus/tag_one:[&>ellipse]:fill-yellow-200 peer-focus/tag_two:[&>ellipse]:stroke-red-500 peer-focus/tag_two:[&>ellipse]:stroke-2 peer-focus/tag_two:[&>ellipse]:fill-red-200 peer-focus/tag_one_and_two:[&>ellipse]:stroke-orange-500 peer-focus/tag_one_and_two:[&>ellipse]:stroke-2 peer-focus/tag_one_and_two:[&>ellipse]:fill-orange-200 "
                margin = "0.13,0.055"
            ]
            b_text [
                fillcolor="#00000000"
                shape="rectangle"
                margin = "0.13,0.055"
                label = <<table
                    border="0"
                    cellborder="0"
                    cellpadding="0"
                    cellspacing="0"
                >
                    <tr>
                        <td align="left" balign="left">b</td>
                    </tr>
                </table>>
            ]
        }
        subgraph cluster_c {
            label = <>
            margin = "0.11,0.055"
            class = "outline-none"
            c [
                label = ""
                class = "outline-none [&>ellipse]:focus:outline-blue-500 [&>ellipse]:focus:outline-2 [&>ellipse]:focus:outline-dashed [&>ellipse]:stroke-slate-600 [&>ellipse]:stroke-1 [&>ellipse]:focus:stroke-slate-500 [&>ellipse]:focus:stroke-1 [&>ellipse]:focus:hover:stroke-slate-400 [&>ellipse]:focus:hover:stroke-1 [&>ellipse]:hover:stroke-slate-400 [&>ellipse]:hover:stroke-1 [&>ellipse]:focus:active:stroke-slate-500 [&>ellipse]:focus:active:stroke-1 [&>ellipse]:fill-slate-300 [&>ellipse]:focus:fill-slate-200 [&>ellipse]:focus:hover:fill-slate-100 [&>ellipse]:hover:fill-slate-100 [&>ellipse]:focus:active:fill-slate-200 px-1.5 py-1.5 cursor-pointer peer-focus/tag_two:[&>ellipse]:stroke-red-500 peer-focus/tag_two:[&>ellipse]:stroke-2 peer-focus/tag_two:[&>ellipse]:fill-red-200 "
                margin = "0.13,0.055"
            ]
            c_text [
                fillcolor="#00000000"
                shape="rectangle"
                margin = "0.13,0.055"
                label = <<table
                    border="0"
                    cellborder="0"
                    cellpadding="0"
                    cellspacing="0"
                >
                    <tr>
                        <td align="left" balign="left">c</td>
                    </tr>
                </table>>
            ]
        }
        subgraph cluster_d {
            label = <>
            margin = "0.11,0.055"
            class = "outline-none"
            d [
                label = ""
                class = "outline-none [&>ellipse]:focus:outline-blue-500 [&>ellipse]:focus:outline-2 [&>ellipse]:focus:outline-dashed [&>ellipse]:stroke-slate-600 [&>ellipse]:stroke-1 [&>ellipse]:focus:stroke-slate-500 [&>ellipse]:focus:stroke-1 [&>ellipse]:focus:hover:stroke-slate-400 [&>ellipse]:focus:hover:stroke-1 [&>ellipse]:hover:stroke-slate-400 [&>ellipse]:hover:stroke-1 [&>ellipse]:focus:active:stroke-slate-500 [&>ellipse]:focus:active:stroke-1 [&>ellipse]:fill-slate-300 [&>ellipse]:focus:fill-slate-200 [&>ellipse]:focus:hover:fill-slate-100 [&>ellipse]:hover:fill-slate-100 [&>ellipse]:focus:active:fill-slate-200 px-1.5 py-1.5 cursor-pointer peer-focus/tag_two:[&>ellipse]:stroke-red-500 peer-focus/tag_two:[&>ellipse]:stroke-2 peer-focus/tag_two:[&>ellipse]:fill-red-200 "
                margin = "0.13,0.055"
            ]
            d_text [
                fillcolor="#00000000"
                shape="rectangle"
                margin = "0.13,0.055"
                label = <<table
                    border="0"
                    cellborder="0"
                    cellpadding="0"
                    cellspacing="0"
                >
                    <tr>
                        <td align="left" balign="left">d</td>
                    </tr>
                </table>>
            ]
        }
    }
    a0 -> b [
        id     = "a0__b"
        class = "focus:outline-blue-500 focus:outline-2 focus:outline-dashed [&>path]:stroke-slate-900 [&>polygon]:stroke-slate-900 [&>path]:stroke-1 [&>path]:focus:stroke-slate-800 [&>polygon]:focus:stroke-slate-800 [&>path]:focus:stroke-1 [&>path]:focus:hover:stroke-slate-700 [&>polygon]:focus:hover:stroke-slate-700 [&>path]:focus:hover:stroke-1 [&>path]:hover:stroke-slate-700 [&>polygon]:hover:stroke-slate-700 [&>path]:hover:stroke-1 [&>path]:focus:active:stroke-slate-800 [&>polygon]:focus:active:stroke-slate-800 [&>path]:focus:active:stroke-1 [&>polygon]:fill-slate-800 [&>polygon]:focus:fill-slate-700 [&>polygon]:focus:hover:fill-slate-600 [&>polygon]:hover:fill-slate-600 [&>polygon]:focus:active:fill-slate-700 cursor-pointer "
    ]
    a1 -> c [
        id     = "a1__c"
        class = "focus:outline-blue-500 focus:outline-2 focus:outline-dashed [&>path]:stroke-slate-900 [&>polygon]:stroke-slate-900 [&>path]:stroke-1 [&>path]:focus:stroke-slate-800 [&>polygon]:focus:stroke-slate-800 [&>path]:focus:stroke-1 [&>path]:focus:hover:stroke-slate-700 [&>polygon]:focus:hover:stroke-slate-700 [&>path]:focus:hover:stroke-1 [&>path]:hover:stroke-slate-700 [&>polygon]:hover:stroke-slate-700 [&>path]:hover:stroke-1 [&>path]:focus:active:stroke-slate-800 [&>polygon]:focus:active:stroke-slate-800 [&>path]:focus:active:stroke-1 [&>polygon]:fill-slate-800 [&>polygon]:focus:fill-slate-700 [&>polygon]:focus:hover:fill-slate-600 [&>polygon]:hover:fill-slate-600 [&>polygon]:focus:active:fill-slate-700 cursor-pointer "
    ]
    b -> d [
        id     = "bd"
        class = "focus:outline-blue-500 focus:outline-2 focus:outline-dashed [&>path]:stroke-slate-900 [&>polygon]:stroke-slate-900 [&>path]:stroke-1 [&>path]:focus:stroke-slate-800 [&>polygon]:focus:stroke-slate-800 [&>path]:focus:stroke-1 [&>path]:focus:hover:stroke-slate-700 [&>polygon]:focus:hover:stroke-slate-700 [&>path]:focus:hover:stroke-1 [&>path]:hover:stroke-slate-700 [&>polygon]:hover:stroke-slate-700 [&>path]:hover:stroke-1 [&>path]:focus:active:stroke-slate-800 [&>polygon]:focus:active:stroke-slate-800 [&>path]:focus:active:stroke-1 [&>polygon]:fill-slate-800 [&>polygon]:focus:fill-slate-700 [&>polygon]:focus:hover:fill-slate-600 [&>polygon]:hover:fill-slate-600 [&>polygon]:focus:active:fill-slate-700 cursor-pointer "
    ]
    c -> d [
        id     = "cd"
        class = "focus:outline-blue-500 focus:outline-2 focus:outline-dashed [&>path]:stroke-slate-900 [&>polygon]:stroke-slate-900 [&>path]:stroke-1 [&>path]:focus:stroke-slate-800 [&>polygon]:focus:stroke-slate-800 [&>path]:focus:stroke-1 [&>path]:focus:hover:stroke-slate-700 [&>polygon]:focus:hover:stroke-slate-700 [&>path]:focus:hover:stroke-1 [&>path]:hover:stroke-slate-700 [&>polygon]:hover:stroke-slate-700 [&>path]:hover:stroke-1 [&>path]:focus:active:stroke-slate-800 [&>polygon]:focus:active:stroke-slate-800 [&>path]:focus:active:stroke-1 [&>polygon]:fill-slate-800 [&>polygon]:focus:fill-slate-700 [&>polygon]:focus:hover:fill-slate-600 [&>polygon]:hover:fill-slate-600 [&>polygon]:focus:active:fill-slate-700 cursor-pointer "
    ]
}

The desired output -- it would be a bit less prettified than this (this has tailwind css styles)

image

Currently it parses and this is rendered:

image

I kind of prefer this PR to be merged since it's already at a nice checkpoint (the parsing into the right data model looks solid), and then we can chip away at the rendering issues.

@nadavrot would you be happy with that idea?

@azriel91
Copy link
Contributor

azriel91 commented Jul 5, 2025

Heya @nadavrot, would you be available to take a look at this -- it changes the data structures, so it would be nice to have them merged before additional features are developed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants