Drawing and Writing Diagrams With draw.io

A skim back through this blog will turn up several posts over the years on the topic of “writing diagrams”, using text based scripts along with diagram generating applications to create diagrams from textual descriptions.

There are a several reasons I think such things useful, particularly in online, distance education context in an institution with a factory production model:

  1. diagram generation can be automated, with standardised style / rendering of the diagram separated from it’s logical description;
  2. maintaining diagrams is simplified: change the underlying text to change the logical content of the diagram, or change the rendering pipeline to change the output design or format;
  3. search: if you can search the text used to generate a diagram as well text that appears within a diagram, it supports discovery;
  4. accessibility: if we generate diagrams from text, there is a good chance we could also generate equivalent textual descriptions of diagrams from the same text.

Sometimes, though, it can be handy to be able to actually draw a diagram by actually drawing it, rather than generating it from a textual source.

Recently, I came across draw.io, via the jupyterlab-drawio extension. draw.io is a web-based [code] or electron app wrapped [code] SVG editor. draw.io is actually a front end application built on top of the mxGraph JavaScript diagramming library and based on the example  GraphEditor.

When you create a new diagram, you are prompted for a save location:

Local (device) storage is the default, but it looks like you can also link Google Drive or OneDrive online storage, though I haven’t tried this (yet!):

If you have a previously saved diagram, you can select it from a file browser. If you opt to create a new diagram, you can create a blank diagram with a default set of drawing tools, a particular diagram type or a diagram imported from a template URL:

If you go for the template URL option, you are prompted for the URL (I don’t know if there’s a catalogue / awesome list of template URLs anywhere?):

If you select one of the canned diagram type options, you are provided with a preview of the sorts of diagram you can create within that view:

If you click to select one of the example diagrams then click Create, the diagram editor opens with the example diagram and a set of custom diagram element options in the scratchpad sidebar:

If you don’t select a preview diagram, or you select the Blank diagram, you just get a default tool set.

Usefully for the course I’m looking at, one of the scratchpad collections provides diagram components that can be used to draw Crows Foot entity relation diagrams, as described here: Entity Relationship Diagrams with draw.io (see also: Entity Relationship Diagram (ERD)).

Clicking on an item in the toolbar previews the component and adds it to the canvas; you can also click and drag items from the sidebar and then drop them on the canvas.

(Partial) ERD diagrams can also be generated by importing database table definitions using the SQL import plugin.

Writing Diagrams

One of the nice features of draw.io is that you can also generate certain diagrams from data files. In the Arrange menu, the Insert option provides several options for importing different sorts of data or textual elements from which diagrams can be automatically generated.

Plugins extend the range of import options, as for example in the case of the sql-plugin. (The SQL plugin seems to add tables based on CREATE TABLE elements in the SQL; whilst it correctly identifies and highlights primary keys, it doesn’t identify relationships between them, so you have to add the crow’s foot lines yourself…)

See the full list of official plugins.

Data can be imported from CSV files, as described here: Automatically create draw.io diagrams from CSV files Not all columns need to be displayed; some columns may even be used to store metadata or styling information using reserved columns ( imagefill and stroke ). The first column in each row represents a node and may be styled according to details given in the styling columns.

Other columns contain values that can be included in the node or that specify which other nodes that node is connected to. Rules are used to define the styling and labelling of each edge, as well as identifying columns used to identify edge connections between nodes.

Not all columns need to be referenced / used in the diagram that is generated.

I haven’t fully explored all the possible CSV import settings yet; I’m also thinking it’d be nice if there were some Python tooling to help simplify the creation of the CSV import definition file.

(By the by, there is also a handy online CSV viewer webform available.)

As well as CSV import, UML diagrams can be generated using PlantUML, a tool for creating a wide variety of diagram types from imported UML and other diagram specifications: Use PlantUML in draw.io. (That said, when I tried with the online *draw.io editor, the PlantUML import didn’t work. It looks like it uses Graphviz underneath, so it may be something to do with that? I need to try on a local install really, or ideally in a container with JupyterLab using the jupyterlab-drawio extension.)*

Taken together, I wonder if these importers could be used with other Python tools for generating diagrams from code? e.g. could something like this approach to electrical circuit diagram generation with lcapy be used to generate diagrams that draw.io can render??

Another handy looking too comes in the form of drawio-batch, a “command line converter for draw.io diagrams” based on puppeteer (“a Node library which provides a high-level API to control Chrome”, operating it by default in headless mode) that wraps the online draw.io conversion code into an offline tool. (I’ve not had a chance to try this yet; from the tests, it looks like you call it with a draw.io XML diagram file and and output file and it gives you an output diagram back in a format corresponding to the filetype you specified by the output file suffix (pdf and svg in the tests)? Puppeteer it also new to me; a bit like Selenium, methinks, and a Javascript follower on from the now deprecated phantom.js?)

Of the plugins, replay looks interesting: it lets you render an animated version of a diagram, for example as you build up a complex flow diagram a piece at a time. There is also an anim plugin for what looks like creating more general animations.

All in all, it looks to be really handy, and something I could ship in out VM. The jupyterlab-drawio extension shows it works in JupyterLab, and I think it should also work with nbserverproxy?

By the by, the Google Drive / OneDrive integration was interesting (if it works; I haven’t had a chance to try it yet)… In particular, it makes me wonder: could the code that did that be reused to provide a similar storage workflow in JupyterHub?

PS in passing, there may be other useful tools in here —  10 JavaScript libraries to draw your own diagrams. (It’s been some time since I last did a round-up…)

Reproducible Diagram Generators – First Thoughts on Electrical Circuit Diagrams

I had a quick tinker with one of the demo notebooks I’m putting together to try to work through what I think I mean by various takes on the phrase “reproducible educational materials” this morning, so here’s a quick note to keep track of my thinking.

The images are drawn in Jupyter notebooks using tikzmagic loaded as %load_ext tikz_magic. (The original notebook this post is based on can be found here, although it’s likely subject to significant change…)

The circuitikz LaTeX package (manual) supports the drawing of electrical circuit diagrams. Circuits are constructed by drawing symbols that connect pairs of Cartesian co-ordinates or that:

%%tikz -p circuitikz -s 0.3
    %Draw a resistor labelled R_1 connecting points (0,0) and (2,0)
    \draw (0,0) to[R, l=$R_1$] (2,0);


The requirement to specify co-ordinates means you need to think about the layout – drafting a circuit on graph paper can help with this.

But things may be simplified in maintenance terms if you label co-ordinates and join those.

For example, consider the following circuit:

This can be drawn according to the following script:

%%tikz -p circuitikz -s 0.3
    %Draw a resistor labelled R_1 connecting points (0,0) and (2,0)
    %Extend the circuit out with a wire to (4,0)
    \draw (0,0) to[R, l=$R_1$] (2,0) -- (4,0);

    %Add a capacitor labelled C_1 connecting points (2,0) and (2,-2)
    \draw (2,0) to[C, l=$C_1$] (2,-2);

    %Add a wire along the bottom
    \draw (0,-2) -- (4,-2);

There are a lot of explicitly set co-ordinate values in there, and it can be hard to see what they refer to. Even with such a simple diagram, making changes to it could become problematic.

In the same way that it is good practice to replace inline numerical values in computer programs with named constants or variables, we can start to make the figure more maintainable by naming nodal points and then connecting these named nodes:

%%tikz -p circuitikz -s 0.3

    %Define some base component size dimensions
    \def\componentSize{2}

    %Define the size of the diagram in terms of component width and height
    %That is, how many horizontally aligned components wide is the diagram
    % and how many vertically aligned components high
    \def\componentWidth{2}
    \def\componentHeight{1}

    %Define the y co-ordinate of the top and bottom rails
    \def\toprail{0}
    \def\height{\componentSize * \componentHeight}
    \def\bottomrail{\toprail - \height}

    %Define the right and left extent x coordinate values
    \def\leftside{0}
    \def\width{\componentSize * \componentWidth}
    \def\rightside{\leftside + \width}

    %Name the coordinate locations of particular nodes
    \coordinate (InTop) at (\leftside,\toprail);
    \coordinate (OutTop) at (\rightside,\toprail);
    \coordinate (InBottom) at (\leftside,\bottomrail);
    \coordinate (OutBottom) at (\rightside,\bottomrail);

    %Draw the top rail
    %Define a convenience x coordinate as the
    %  vertical aligned to the topmost component out
    %The number (1) in the product term below is based on
    %  how many components in from the left we are
    \def\R1outX{1 * \componentSize}

    %Add a resistor labelled R_1
    \coordinate (R1out) at (\R1outX,\toprail);
    \draw (InTop) to[R, l=$R_1$] (R1out) -- (OutTop);

    %Add a capacitor labelled C_1
    \coordinate (C1out) at (\R1outX,\bottomrail);
    \draw (R1out) to[C, l=$C_1$] (C1out);

    %Draw the bottom rail
    \draw (InBottom) -- (OutBottom);

Some reflections about possible best practice drawn (!) from this:

  • define named limits on x values to set the width of the diagram, such as \leftside and \rightside. This can be done by counting the number of components wide the diagram is (if we can assume components have width one).
  • name the maximum and minimum height (y) values such as \toprail and \bottomrail. Again, counting vertically place components may help.  Use relative definitions where possible to make the diagram easier to maintain.
  • define connections relative to each other to minimise the number of numerical values that need to be set explicitly;
  • name points sensibly; if we read the diagram from top left to bottom right, we can make use of easily recognised verticals by using named x coordinate values set relative to the topmost component out x co-ordinates (for example, \R1outX) and top leftmost component out y values; full cartesian co-ordinate pairs can then be named relative to nodes associated with top leftmost component outs (for example, \R1out);
  • the code to produce the diagram looks like overkill in its length, but lots of could quickly become boilerplate that could potentially be included in a slightly higher level TeX package that bakes more definitions in. Despite the added length, it also makes the script more readable and supports its self-documenting, literate programming style nature.

PS imagining feedback… “Ah yes, but we don’t draw resistors like that, so it’s no good…” ;-)

%%tikz -p circuitikz -s 0.3
%To ward off the "we don't draw resistors like that" cries...
\ctikzset{resistor = european}

%Draw a resistor labelled R_1 connecting points (0,0) and (2,0)
\draw (0,0) to[R, l=$R_1$] (2,0);

resistor2
And that’s another reason why this approach make sense…