9

I can't find an example of a state/class/flow chart/org chart diagram with Vega. Are there any out there?

It feels like Vega is perfectly suited for this (if a bit overpowered), but without an example to start from it's a rather steep learning curve. There are some examples on a "How Vega Works" page, but no links to how they're built:

how vega works chart

There's also the tree layout example, but it's not clear how one would begin converting this into blocks suitable for a flow-chart style diagram.

enter image description here

Here's some examples of the sort of output desired (plus other shapes e.g. diamonds/triangles) from e.g. mermaid.js

class diagram

Brian M. Hunt
  • 81,008
  • 74
  • 230
  • 343

3 Answers3

7

Suppose you're able to represent your chart as follows:

"values": [
        {"id": "1", "parent": null, "title": "Animal"},
        {"id": "2", "parent": "1", "title": "Duck"},
        {"id": "3", "parent": "1", "title": "Fish"},
        {"id": "4", "parent": "1", "title": "Zebra"}
      ]

What you can then do is to lay the nodes out in a tree-like shape (stratify does the job):

"transform": [
        {
          "type": "stratify",
          "key": "id",
          "parentKey": "parent"
        },
        {
          "type": "tree",
          "method": "tidy",
          "separation": true,
          "size": [{"signal": "width"}, {"signal": "height"}]
        }
      ]

having laid out the nodes, you need to generate connecting lines, treelinks + linkpath combo does exactly that:

{
      "name": "links",
      "source": "tree", // take datasource "tree" as input
      "transform": [
        { "type": "treelinks" }, // apply transform 1
        { "type": "linkpath", // follow up with next transform
          "shape": "diagonal"
          }
      ]
    }

now that you've got your data sources, you want to draw actual objects. in Vega these are called marks. I guess this is where I'm going to deviate from your desired output as I'm only drawing one rectangle with a title for each data point and some basic lines to connect:

"marks": [
    {
      "type": "path",
      "from": {"data": "links"}, // dataset we defined above
      "encode": {
        "enter": {
          "path": {"field": "path"} // linkpath generated a dataset with "path" field in it - we just grab it here
        }
      }
    },
    {
      "type": "rect",
      "from": {"data": "tree"},
      "encode": {
        "enter": {
          "stroke": {"value": "black"},
          "width": {"value": 100},
          "height": {"value": 20},
          "x": {"field": "x"},
          "y": {"field": "y"}
        }
      }
    },
    {
      "type": "text",
      "from": {"data": "tree"}, // use data set we defined earlier
      "encode": {
        "enter": {
          "stroke": {"value": "black"},
          "text": {"field": "title"}, // we can use data fields to display actual values
          "x": {"field": "x"}, // use data fields to draw values from
          "y": {"field": "y"},
          "dx": {"value":50}, // offset the mark to appear in rectangle center
          "dy": {"value":13},
          "align": {"value": "center"}
        }
      }
    }
  ]

All in all I arrived at a very basic approximation of your target state. It's definitely not an exact match: the rectangles there should probably be replaced with groups and connection paths will need some work too. You will notice I'm not using any signalsto feed dynamic user inputs and update/exit/hover instructions - again, for simplicity.

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "width": 800,
  "height": 300,
  "padding": 5,

  "data": [
    {
      "name": "tree",
      "values": [
        {"id": "1", "parent": null, "title": "Animal"},
        {"id": "2", "parent": "1", "title": "Duck"},
        {"id": "3", "parent": "1", "title": "Fish"},
        {"id": "4", "parent": "1", "title": "Zebra"}
      ],
      "transform": [
        {
          "type": "stratify",
          "key": "id",
          "parentKey": "parent"
        },
        {
          "type": "tree",
          "method": "tidy",
          "separation": true,
          "size": [{"signal": "width"}, {"signal": "height"}]
        }
      ]      
    },
    {
      "name": "links",
      "source": "tree",
      "transform": [
        { "type": "treelinks" },
        { "type": "linkpath",
          "shape": "diagonal"
          }
      ]
    }, 
    {
      "name": "tree-boxes",
      "source": "tree",
      "transform": [
          { 
            "type": "filter",
            "expr": "datum.parent == null"
          }
        ]
    },
    {
      "name": "tree-circles",
      "source": "tree",
      "transform": [
        {
          "type": "filter",
          "expr": "datum.parent != null"
        }
      ]
    }
  ],
  "marks": [
    {
      "type": "path",
      "from": {"data": "links"},
      "encode": {
        "enter": {
          "path": {"field": "path"}
        }
      }
    },
    {
      "type": "rect",
      "from": {"data": "tree-boxes"},
      "encode": {
        "enter": {
          "stroke": {"value": "black"},
          "width": {"value": 100},
          "height": {"value": 20},
          "x": {"field": "x"},
          "y": {"field": "y"}
        }
      }
    },
    {
      "type": "symbol",
      "from": {"data": "tree-circles"},
      "encode": {
        "enter": {
          "stroke": {"value": "black"},
          "width": {"value": 100},
          "height": {"value": 20},
          "x": {"field": "x"},
          "y": {"field": "y"}
        }
      }
    },
    {
      "type": "rect",
      "from": {"data": "tree"},
      "encode": {
        "enter": {
          "stroke": {"value": "black"},
          "width": {"value": 100},
          "height": {"value": 20},
          "x": {"field": "x"},
          "y": {"field": "y"}
        }
      }
    },
    {
      "type": "text",
      "from": {"data": "tree"},
      "encode": {
        "enter": {
          "stroke": {"value": "black"},
          "text": {"field": "title"},
          "x": {"field": "x"},
          "y": {"field": "y"},
          "dx": {"value":50},
          "dy": {"value":13},
          "align": {"value": "center"}
        }
      }
    }
  ]
}

UPD: suppose, you would like to render different shapes for root and leaf nodes of your chart. One way to achieve this will be to add two filter transformations based on your tree dataset and filter them accordingly:

    {
      "name": "tree-boxes",
      "source": "tree", // grab the existing data
      "transform": [
          { 
            "type": "filter",
            "expr": "datum.parent == null" // run it through a filter defined by expression
          }
        ]
    },
    {
      "name": "tree-circles",
      "source": "tree",
      "transform": [
        {
          "type": "filter",
          "expr": "datum.parent != null"
        }
      ]
    }

then instead of rendering all marks as rect you'd want two different shapes for respective transformed datasets:

{
      "type": "rect",
      "from": {"data": "tree-boxes"},
      "encode": {
        "enter": {
          "stroke": {"value": "black"},
          "width": {"value": 100},
          "height": {"value": 20},
          "x": {"field": "x"},
          "y": {"field": "y"}
        }
      }
    },
    {
      "type": "symbol",
      "from": {"data": "tree-circles"},
      "encode": {
        "enter": {
          "stroke": {"value": "black"},
          "width": {"value": 100},
          "height": {"value": 20},
          "x": {"field": "x"},
          "y": {"field": "y"}
        }
      }
    }
timur
  • 14,239
  • 2
  • 11
  • 32
  • Thanks timur, this is very helpful. We want to programatically generate the layout with the nodes in proper positions, and you've certainly gotten me onto the right track here. I'm not sure what you mean by the WYSIWYG, that's not part of the question (or bounty) — all that's needed is visualization, and editing be a detriment for our use case. There's still some challenges in the node layout but those might be straightforward. – Brian M. Hunt Mar 03 '20 at 12:56
  • right. I'll cut the wysiwig part out then, so the answer is more focused – timur Mar 03 '20 at 15:50
  • Thanks @timur, that's helpful. I'm going to leave this question open to give others an opportunity at the bounty, but if nothing better comes along I'll grant the bounty to this answer. – Brian M. Hunt Mar 03 '20 at 23:42
3

You can refer to this solution - Working with trees which covers

Step 1 - Extracting Nodes from Tabular Data

Step 2 - Extracting Links from Stratified Node Data

Step 3 - How to bring them together

Step 4 - Add labels

Step 5 - Add Color

Devesh mehta
  • 1,505
  • 8
  • 22
2

I have built an example which is the closest so far to what is described in this question. I based my solution on the accepted answer here, thanks to @timur.

Click here to view it in the Vega Editor.

It displays tree nodes as groups with multiple texts inside. It supports expanding & collapsing of nodes as well as switching between horizontal and vertical layout (which you can control by setting the default value of horizontal signal).

There are a few limitations I faced though:

  • Switching between horizontal & vertical layouts doesn't re-render all marks properly (raised an issue here). It works only if you manually change the default value of horizontal signal in the code
  • I couldn't find a way to properly collapse the root-node of the tree without manually collapsing nested nodes (posted a relevant question here)

Anyway, it should be useful to anyone looking for a way to build Org Chart kind of visualization with Vega - without having closer examples, I had to spend many hours to figure out all the caveats and fix almost all the issues.

Alexey Grinko
  • 2,773
  • 22
  • 21