Graphviz is a software that renders graphs, Factorio is a game in which you build and maintain factories.

Let’s explore how both can complement each other.

Note

I assume you know a bit about Factorio and, as for the rest of concepts, I’ll introduce you to them as we go. If you haven’t played Factorio yet, I highly encourage you to give it a try!

Case study: Logistic science pack

The story goes: we’ve started the game, built a few mining drills (particularly for coal, iron, and copper), and got a basic electrical grid set up - time for science!

Certainly, we need a handful of laboratories:

factorio laboratories

For our laboratories to work, we need a production line that will provide a constant flow of science packs; since automation science packs are turbo-easy to produce, let’s ignore them and focus right-away on logistic science packs.

As we can find on Factorio’s Wiki:

  • To produce 1 logistic science pack, we’ll need 1 inserter and 1 transport belt.

  • Before though, to produce that 1 inserter, we’ll need 1 electronic circuit, 1 iron gear wheel, and 1 iron plate.

  • Before though, to produce that 1 electronic circuit, we’ll need…​ well, describing this in English gets tedious; what do you say we invent some notation to make the our recipe just a tad more legible?

Let’s start with a minimalistic textual format of:

product
    requirements

Oh, thinking about all the stuff we can do with this grammar-boi gives me shivers running down my spine!

Translating the former ingredients, we get:

logistic_science_pack
    inserter
    transport_belt

Since both inserter and transport_belt need to be produced too, let’s include them as well:

logistic_science_pack
    inserter
    transport_belt

inserter
    electronic_circuit
    iron_gear_wheel
    iron_plate

transport_belt
    iron_gear_wheel
    iron_plate

Similarly, we’ll need to produce electronic_circuit, iron_gear_wheel, and - generally - everything that’s not primitive (which in my case means that I’ll need to produce everything up to iron_plate and copper_plate):

logistic_science_pack
    inserter
    transport_belt

inserter
    electronic_circuit
    iron_gear_wheel
    iron_plate

transport_belt
    iron_gear_wheel
    iron_plate

electronic_circuit
    copper_cable
    iron_plate

copper_cable
    copper_plate

iron_gear_wheel
    iron_plate
Note

Some people like to build main buses with different constituents - if you have an abundance of e.g. iron gear wheels, feel free to skip them from the recipe.

This was the first step.

So far, we’ve gathered all the knowledge about what needs to be fetched (think: copper plates) and what needs to be built (think: iron gear wheels) - our original problem remains open though: where to place those assembly machines?

That’s when graphs come into play.

Introduction to graphs

Graphs are structures used to model relationships between objects - using graphs you can represent things like family trees or networks of friends:

%3PatrykPatrykDafneDafnePatryk--DafneNikoNikoDafne--NikoDulciaDulciaDafne--DulciaCharlieCharlieDulcia--Charlie

Graphs consist of nodes (Patryk, Dafne etc. on the image above) and edges (the lines connecting nodes).

Graphs can be undirected (like the one above) or directed:

%3PatrykPatrykDafneDafnePatryk->DafneDafne->PatrykNikoNikoDafne->NikoDulciaDulciaDafne->DulciaCharlieCharlieDulcia->Charlie

Graphs can be rendered by computers (like the one above) or, obviously, by hand:

hand drawn graph

Since this post is all about automating stuff, we’re going to focus solely on the computer-generated graphs - using Graphviz.

Graphviz is a software that transforms description of a graph (written in the DOT language) into an image; for instance, here’s source code of the directed graph you saw above:

# This instruction starts a directed graph
digraph {
    # This instruction makes the graph go left-to-right
    rankdir = "LR"

    # Those instructions define nodes and edges ("connections")
    Patryk -> Dafne
    Dafne -> Patryk
    Dafne -> Niko
    Dafne -> Dulcia
    Dulcia -> Charlie
}
Note

There are lots of fantastic online tools you can use to preview graphs written in the DOT language; I frequently use https://dreampuf.github.io/GraphvizOnline and https://rsms.me/graphviz/.

You can just open the page, copy & paste graph’s code and get a nice image in return.

What’s peculiar about the DOT language, and what we’re going to exploit in a second, is the fact that we don’t have to specify where our nodes and edges should be located - we just say Patryk → Dafne, Dafne → Niko and the program, almost magically, lays out everything for us in an aesthetically-pleasant way.

Let’s see how we can use this feature to answer the problem we had in the previous section.

Note

The overall subject of pretty-printing graphs is called force-directed graph drawing - it’s a nice rabbit hole to go down for a side programming project!

Case study: Logistic science pack (cont.)

Let’s recall our recipe:

logistic_science_pack
    inserter
    transport_belt

inserter
    electronic_circuit
    iron_gear_wheel
    iron_plate

transport_belt
    iron_gear_wheel
    iron_plate

electronic_circuit
    copper_cable
    iron_plate

copper_cable
    copper_plate

iron_gear_wheel
    iron_plate

Since Graphviz doesn’t understand our notation (we’ve just invented it, right?), first we have to translate it into the DOT language.

Let’s start with logistic_science_pack:

digraph {
    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack
}
digraph {
    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack
}
%3inserterinserterlogistic_science_packlogistic_science_packinserter->logistic_science_packtransport_belttransport_belttransport_belt->logistic_science_pack

Now it’s time for inserter and transport_belt:

digraph {
    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack

    electronic_circuit -> inserter
    iron_gear_wheel -> inserter
    iron_plate -> inserter

    iron_gear_wheel -> transport_belt
    iron_plate -> transport_belt
}
digraph {
    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack

    electronic_circuit -> inserter
    iron_gear_wheel -> inserter
    iron_plate -> inserter

    iron_gear_wheel -> transport_belt
    iron_plate -> transport_belt
}
%3inserterinserterlogistic_science_packlogistic_science_packinserter->logistic_science_packtransport_belttransport_belttransport_belt->logistic_science_packelectronic_circuitelectronic_circuitelectronic_circuit->inserteriron_gear_wheeliron_gear_wheeliron_gear_wheel->inserteriron_gear_wheel->transport_beltiron_plateiron_plateiron_plate->inserteriron_plate->transport_belt

And so on, and so forth, until we finally end up with:

digraph {
    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack

    electronic_circuit -> inserter
    iron_gear_wheel -> inserter
    iron_plate -> inserter

    iron_gear_wheel -> transport_belt
    iron_plate -> transport_belt

    copper_cable -> electronic_circuit
    iron_plate -> electronic_circuit

    copper_plate -> copper_cable

    iron_plate -> iron_gear_wheel
}
digraph {
    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack

    electronic_circuit -> inserter
    iron_gear_wheel -> inserter
    iron_plate -> inserter

    iron_gear_wheel -> transport_belt
    iron_plate -> transport_belt

    copper_cable -> electronic_circuit
    iron_plate -> electronic_circuit

    copper_plate -> copper_cable

    iron_plate -> iron_gear_wheel
}
%3inserterinserterlogistic_science_packlogistic_science_packinserter->logistic_science_packtransport_belttransport_belttransport_belt->logistic_science_packelectronic_circuitelectronic_circuitelectronic_circuit->inserteriron_gear_wheeliron_gear_wheeliron_gear_wheel->inserteriron_gear_wheel->transport_beltiron_plateiron_plateiron_plate->inserteriron_plate->transport_beltiron_plate->electronic_circuitiron_plate->iron_gear_wheelcopper_cablecopper_cablecopper_cable->electronic_circuitcopper_platecopper_platecopper_plate->copper_cable

Neat, we’ve finally extracted some new information from the system: placement and wiring!

Granted, it’s not perfect (fat chance those curvy transport belts would actually work in the game), but it’s a nice starting point - now let’s try to improve it.

Since transport belts must be straight, let’s start by forcing the edges to be in line via splines = ortho:

digraph {
    splines = ortho

    /* ... */
}
digraph {
    splines = ortho

    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack
    electronic_circuit -> inserter
    iron_gear_wheel -> inserter
    iron_plate -> inserter
    iron_gear_wheel -> transport_belt
    iron_plate -> transport_belt
    copper_cable -> electronic_circuit
    iron_plate -> electronic_circuit
    copper_plate -> copper_cable
    iron_plate -> iron_gear_wheel
}
%3inserterinserterlogistic_science_packlogistic_science_packinserter->logistic_science_packtransport_belttransport_belttransport_belt->logistic_science_packelectronic_circuitelectronic_circuitelectronic_circuit->inserteriron_gear_wheeliron_gear_wheeliron_gear_wheel->inserteriron_gear_wheel->transport_beltiron_plateiron_plateiron_plate->inserteriron_plate->transport_beltiron_plate->electronic_circuitiron_plate->iron_gear_wheelcopper_cablecopper_cablecopper_cable->electronic_circuitcopper_platecopper_platecopper_plate->copper_cable
Note

There are many other spline algorithms you can experiment with - you can find them all in the documentation.

It looks somewhat better, but still kinda sloppy.

Since in my factory I’m going to transport copper plates next to iron plates, it will be helpful to align copper_plate on the same level as iron_plate (since both will effectively function as "inputs" to our module).

To align nodes, we can use the rank instruction:

digraph {
    /* ... */

    {
        # This instruction tells Graphviz
        # to align all nodes located in
        # this block next to each other
        rank = same

        copper_plate
        iron_plate
    }

    /* ... */
}
digraph {
    splines = ortho

    {
        rank = same

        copper_plate
        iron_plate
    }

    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack
    electronic_circuit -> inserter
    iron_gear_wheel -> inserter
    iron_plate -> inserter
    iron_gear_wheel -> transport_belt
    iron_plate -> transport_belt
    copper_cable -> electronic_circuit
    iron_plate -> electronic_circuit
    copper_plate -> copper_cable
    iron_plate -> iron_gear_wheel
}
%3copper_platecopper_platecopper_cablecopper_cablecopper_plate->copper_cableiron_plateiron_plateinserterinserteriron_plate->insertertransport_belttransport_beltiron_plate->transport_beltelectronic_circuitelectronic_circuitiron_plate->electronic_circuitiron_gear_wheeliron_gear_wheeliron_plate->iron_gear_wheellogistic_science_packlogistic_science_packinserter->logistic_science_packtransport_belt->logistic_science_packelectronic_circuit->inserteriron_gear_wheel->inserteriron_gear_wheel->transport_beltcopper_cable->electronic_circuit

Well, our new layout is both technically correct and a bit disappointing - even though we’ve managed to get copper_plate and iron_plate on the same level, we’ve also ended up with two crossing edges (next to transport_belt), which is a no-go for such a small module.

Let’s help Graphviz by additionally aligning inserter and transport_belt on the same level:

digraph {
    /* ... */

    {
        rank = same

        inserter
        transport_belt
    }

    /* ... */
}
digraph {
    splines = ortho

    {
        rank = same

        copper_plate
        iron_plate
    }

    {
        rank = same

        inserter
        transport_belt
    }

    copper_plate -> copper_cable
    copper_cable -> electronic_circuit
    iron_plate -> electronic_circuit
    electronic_circuit -> inserter
    iron_gear_wheel -> inserter
    iron_plate -> inserter
    iron_plate -> iron_gear_wheel
    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack
    iron_gear_wheel -> transport_belt
    iron_plate -> transport_belt
}
%3copper_platecopper_platecopper_cablecopper_cablecopper_plate->copper_cableiron_plateiron_plateinserterinserteriron_plate->insertertransport_belttransport_beltiron_plate->transport_beltelectronic_circuitelectronic_circuitiron_plate->electronic_circuitiron_gear_wheeliron_gear_wheeliron_plate->iron_gear_wheellogistic_science_packlogistic_science_packinserter->logistic_science_packtransport_belt->logistic_science_packcopper_cable->electronic_circuitelectronic_circuit->inserteriron_gear_wheel->inserteriron_gear_wheel->transport_belt

I’d say

meme: not-great-not-terrible

... at least this time we’ve got something we could actually build in the game!

No reason to rest on our laurels so soon though - since Factorio’s assembly machines are squares, it would make sense to make our appropriate nodes look like squares too:

digraph {
    /* ... */

    copper_cable [shape = box]
    electronic_circuit [shape = box]

    /* ... */
}
digraph {
    splines = ortho

    {
        rank = same

        copper_plate
        iron_plate
    }

    {
        rank = same

        inserter
        transport_belt
    }

    copper_cable [shape = box]
    electronic_circuit [shape = box]
    iron_gear_wheel [shape = box]
    inserter [shape = box]
    transport_belt [shape = box]
    logistic_science_pack [shape = box]

    copper_plate -> copper_cable
    copper_cable -> electronic_circuit
    iron_plate -> electronic_circuit
    electronic_circuit -> inserter
    iron_gear_wheel -> inserter
    iron_plate -> inserter
    iron_plate -> iron_gear_wheel
    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack
    iron_gear_wheel -> transport_belt
    iron_plate -> transport_belt
}
%3copper_platecopper_platecopper_cablecopper_cablecopper_plate->copper_cableiron_plateiron_plateinserterinserteriron_plate->insertertransport_belttransport_beltiron_plate->transport_beltelectronic_circuitelectronic_circuitiron_plate->electronic_circuitiron_gear_wheeliron_gear_wheeliron_plate->iron_gear_wheellogistic_science_packlogistic_science_packinserter->logistic_science_packtransport_belt->logistic_science_packcopper_cable->electronic_circuitelectronic_circuit->inserteriron_gear_wheel->inserteriron_gear_wheel->transport_belt

Seizing the day, let’s make them all of the same size as well:

digraph {
    /* ... */

    copper_cable [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    electronic_circuit [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    /* ... */
}
digraph {
    splines = ortho

    {
        rank = same

        copper_plate
        iron_plate
    }

    {
        rank = same

        inserter
        transport_belt
    }

    copper_cable [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    electronic_circuit [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    iron_gear_wheel [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    inserter [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    transport_belt [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    logistic_science_pack [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    copper_plate -> copper_cable
    copper_cable -> electronic_circuit
    iron_plate -> electronic_circuit
    electronic_circuit -> inserter
    iron_gear_wheel -> inserter
    iron_plate -> inserter
    iron_plate -> iron_gear_wheel
    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack
    iron_gear_wheel -> transport_belt
    iron_plate -> transport_belt
}
%3copper_platecopper_platecopper_cablecopper_cablecopper_plate->copper_cableiron_plateiron_plateinserterinserteriron_plate->insertertransport_belttransport_beltiron_plate->transport_beltelectronic_circuitelectronic_circuitiron_plate->electronic_circuitiron_gear_wheeliron_gear_wheeliron_plate->iron_gear_wheellogistic_science_packlogistic_science_packinserter->logistic_science_packtransport_belt->logistic_science_packcopper_cable->electronic_circuitelectronic_circuit->inserteriron_gear_wheel->inserteriron_gear_wheel->transport_belt

Not sure about you, but I am in awe seeing how well Graphviz managed to lay our graph out - we could reconstruct it almost 1:1 in the game!

There’s just one itsy-bitsy tiny thing we may still iterate on:

Do you see that iron_gear_wheel near the centre?

Currently it’s responsible for producing wheels both for transport_belt and inserter, which means we’d have to apply a splitter - this seems overly troublesome by my standards, so let’s just create two separate assembly machines instead:

digraph {
    /* ... */

    iron_plate -> iron_gear_wheel_1
    iron_gear_wheel_1 -> inserter

    iron_plate -> iron_gear_wheel_2
    iron_gear_wheel_2 -> transport_belt

    /* ... */
}
digraph {
    splines = ortho

    {
        rank = same

        copper_plate
        iron_plate
    }

    {
        rank = same

        inserter
        transport_belt
    }

    copper_cable [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    electronic_circuit [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    iron_gear_wheel_1 [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    iron_gear_wheel_2 [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    inserter [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    transport_belt [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    logistic_science_pack [
        shape = box,
        width = 1.5,
        height = 1.5,
        fixedsize = true
    ]

    copper_plate -> copper_cable
    copper_cable -> electronic_circuit
    iron_plate -> electronic_circuit
    electronic_circuit -> inserter
    iron_gear_wheel_1 -> inserter
    iron_plate -> inserter
    iron_plate -> iron_gear_wheel_1
    iron_plate -> iron_gear_wheel_2
    inserter -> logistic_science_pack
    transport_belt -> logistic_science_pack
    iron_gear_wheel_2 -> transport_belt
    iron_plate -> transport_belt
}
%3copper_platecopper_platecopper_cablecopper_cablecopper_plate->copper_cableiron_plateiron_plateinserterinserteriron_plate->insertertransport_belttransport_beltiron_plate->transport_beltelectronic_circuitelectronic_circuitiron_plate->electronic_circuitiron_gear_wheel_1iron_gear_wheel_1iron_plate->iron_gear_wheel_1iron_gear_wheel_2iron_gear_wheel_2iron_plate->iron_gear_wheel_2logistic_science_packlogistic_science_packinserter->logistic_science_packtransport_belt->logistic_science_packcopper_cable->electronic_circuitelectronic_circuit->inserteriron_gear_wheel_1->inserteriron_gear_wheel_2->transport_belt

Seems like we’ve made it - this our our toy at work:

The first potion gets produced at 0:25, which is a rather long time, but - even so - it’s a success!

Now, there’s a vast array of things we could still work on - mainly:

Up to this point we didn’t really care about how long it takes to produce each part - and so at 0:33 we can see that everything gets bottle-necked at the final, logistic_science_pack assembly machine.

Solving this issue is left as an exercise for the reader :-)

Case study: Chemical science pack

This one is actually going to be way shorter - let’s skip all the ceremony and jump straight into the recipe:

chemical_science_pack
    advanced_circuit
    engine_unit
    sulfur

advanced_circuit
    copper_cable
    electronic_circuit
    plastic_bar

copper_cable
    copper_plate

electronic_circuit
    copper_cable
    iron_plate

plastic_bar
    coal
    petroleum_gas

engine_unit
    iron_gear_wheel
    pipe
    steel_plate

iron_gear_wheel
    iron_plate

pipe
    iron_plate

steel_plate
    iron_plate

sulfur
    petroleum_gas
    water

Since I don’t quite enjoy arduous, repetitive tasks - and I’m a programmer by heart - instead of translating the entire recipe by hand, I’ve prepared a tiny application that can do it for me; it’s available at https://factorio-layouter.pwychowaniec.com - feel free to use it!

Summary & future work

As with everything, so doesn’t using Graphviz solve all our problems.

I find it helpful in planning the initial sketches of various modules (which I later reiterate on a piece of paper before eventually reconstructing in Factorio), and that’s why I wanted to share this method with you.

In the following post I’m going to describe how I created that simple factorio-layouter application - it will be a purely technical article where we’ll take a look at parser combinators, Rust, WebAssembly, and a few other things I’ve molded together.