Examples

This chapter contains a few examples showing how to use drawgraph() to visualize a few graphs.

Julia type tree

This example tries to draw a type hierarchy diagram. The Buchheim layout algorithm can take a list of “vertex widths” that are normalized and then used to assign sufficient space for each label.

Code for this figure

This code generates the figure below:

using Karnak, Graphs, NetworkLayout, InteractiveUtils

add_numbered_vertex!(g) = add_vertex!(g)

function build_type_tree(g, T, level=0)
    add_numbered_vertex!(g)
    push!(labels, T)
    for t in subtypes(T)
        if occursin(".",  string(t)) # only Base
            continue
        end
        build_type_tree(g, t, level + 1)
        add_edge!(g,
            findfirst(isequal(T), labels),
            findfirst(isequal(t), labels))
    end
end

function manhattanline(pt1, pt2)
    mp = midpoint(pt1, pt2)
    poly([pt1,
            Point(pt1.x, mp.y),
            Point(pt1.x, mp.y),
            Point(pt2.x, mp.y),
            Point(pt2.x, mp.y),
            Point(pt2.x, pt2.y),
            pt2
        ], :stroke)
    circle(pt2, 1, :fill)
end

g = DiGraph()
labels = []
build_type_tree(g, Number)
labels = map(string, labels)

dg = @drawsvg begin
    background("grey20")
    fontsize(15)
    fontface("JuliaMono-Bold")
    setline(1)
    sethue("gold")
    nodesizes = Float64[]
    for l in eachindex(labels)
        tx = textextents(string(labels[l]))
        labelwidth = tx[3]
        push!(nodesizes, labelwidth)
    end
    drawgraph(g, margin=50,
        layout=Buchheim(nodesize=nodesizes),
        vertexfunction=(v, c) -> begin
            w = nodesizes[v]
            bbox  = BoundingBox(box(c[v], w/2, get_fontsize()))
            # box
            @layer begin
                sethue("white")
                box(bbox, 2, action=:fillpreserve)
                sethue("gold")
                strokepath()
            end
            #text
            @layer begin
                sethue("black")
                textfit(labels[v], bbox)
            end
        end,
        edgefunction=(n, s, d, f, t) -> manhattanline(f, t)
    )
end 1000 550
Example block output

This graph could do with a bit more tweaking.

Julia source tree

This example takes a Julia expression and displays it as a tree.

using Karnak, Graphs, NetworkLayout, Colors

# shamelessly stolen from Professor David Sanders' Tree !

add_numbered_vertex!(g) = (add_vertex!(g); top = nv(g))

function walk_tree!(g, labels, ex, show_call = true)
    top_vertex = add_numbered_vertex!(g)
    where_start = 1  # which argument to start with
    if !(show_call) && ex.head == :call
        f = ex.args[1]   # the function name
        push!(labels, f)
        where_start = 2   # drop "call" from tree
    else
        push!(labels, ex.head)
    end
    for i in where_start:length(ex.args)
        if isa(ex.args[i], Expr)
            child = walk_tree!(g, labels, ex.args[i], show_call)
            add_edge!(g, top_vertex, child)
        else
            n = add_numbered_vertex!(g)
            add_edge!(g, top_vertex, n)
            push!(labels, ex.args[i])
        end
    end
    return top_vertex
end

function walk_tree(ex::Expr, show_call = false)
    g = DiGraph()
    labels = Any[]
    walk_tree!(g, labels, ex, show_call)
    return (g, labels)
end

# build graph and labels
expression = :(2 + sin(30) * cos(15) / 2π - log(-1.02^exp(-1)))

g, labels = walk_tree(expression)

@drawsvg begin
    background("grey10")
    sethue("gold")
    drawgraph(g,
        margin=60,
            layout = buchheim,
            vertexlabels = labels,
            vertexshapes = :circle,
            vertexshapesizes = 20,
            edgefunction = (n, s, d, f, t) -> begin
                move(f)
                line(t)
                strokepath()
            end,
            vertexlabelfontsizes = 15,
            vertexlabelfontfaces = "JuliaMono-Bold", # probably won't be available for docs
            vertexlabeltextcolors = colorant"black")
    fontface("JuliaMono-Bold")
    fontsize(15)
    text(string(expression), boxbottomcenter() + (0, -20), halign=:center)
end
Example block output

LayeredLayouts.jl

LayeredLayouts is a package for working out how to layout graphs in a layered fashion: how to lay out directed acyclic graphs (DAGs), including trees, dependency graphs, and Sankey diagrams.

The package offers the Zarate algorithm (David Cheng Zarate). Positions are returned as x and y vectors, and should be converted to Points when passed to layout.

using Graphs
using LayeredLayouts
using Karnak

tree = SimpleDiGraph(Edge.(
    [1 => 2, 2 => 3, 4 => 5, 4 => 6, 
     4 => 7, 4 => 8, 4 => 9, 4 => 10, 
     5 => 11, 5 => 12, 8 => 15, 8 => 16, 
     8 => 17, 8 => 18, 8 => 19, 9 => 20, 
     9 => 21, 10 => 22, 12 => 13, 13 => 14, 
     23 => 4, 23 => 24, 23 => 25, 23 => 26, 
     23 => 27, 23 => 28, 23 => 29, 23 => 30, 
     23 => 31, 28 => 32, 28 => 33, 29 => 35, 
     30 => 1, 30 => 38, 31 => 40, 33 => 34, 
     35 => 36, 35 => 37, 38 => 39, 40 => 41, 41 => 42]))

xs, ys, paths = solve_positions(Zarate(), tree)

@draw begin
    background("black")
    sethue("gold")
    drawgraph(tree, 
        vertexlabels = 1:nv(tree),
        edgestrokecolors = [Karnak.RGB(randomhue()...) for e in 1:ne(tree)],
        layout= boxmiddleleft() .+ 
            map(pt -> Point(90pt[1], 30pt[2]), zip(xs, ys))
    )
end 600 500

layered layouts

Simple dependency graph

You can draw a visual interpretation of a Julia package's dependencies easily enough by going through the TOML files.

Code for this figure

This code generates the figure below:

using Karnak
using Graphs
using NetworkLayout
using InteractiveUtils
using Colors
using TOML
using Base: active_project

# mostly stolen from PkgGraph.jl by tfiers!

manifest(proj_path) = replace(proj_path, "Project.toml" => "Manifest.toml")

if VERSION ≥ v"1.7"
    packages_in(manifest) = TOML.parsefile(manifest)["deps"]
else
    packages_in(manifest) = TOML.parsefile(manifest)
end

packages_in_active_manifest() = packages_in(manifest(active_project()))

function depgraph(pkgname)
    rootpkg = string(pkgname)
    packages = packages_in_active_manifest()
    if rootpkg ∉ keys(packages)
        error("""
        The given package ($pkgname) must be installed in the active project
        (which is currently `$(active_project())`)""")
    end
    deps = Vector{Pair{String,String}}()
    add_deps_of(name) = begin
        pkg_info = only(packages[name])
        direct_deps = get(pkg_info, "deps", [])
        for dep in direct_deps
            push!(deps, name => dep)
            add_deps_of(dep)
        end
    end
    add_deps_of(rootpkg)
    return unique!(deps)
end

function build_depgraph(pkgname)
    dgraphs = depgraph(pkgname)
    pkglist = String[]
    for (f, t) in dgraphs
        if f ∉ pkglist
            push!(pkglist, f)
        end
        if t ∉ pkglist
            push!(pkglist, t)
        end
    end
    g = DiGraph(length(pkglist))
    for (f, t) in dgraphs
        if f ∈ pkglist && t ∈ pkglist
            add_edge!(g, findfirst(isequal(f), pkglist), findfirst(isequal(t), pkglist))
        end
    end
    return g, pkglist
end

g, pkgnames = build_depgraph("DataFrames")

d = @drawsvg begin
    background("grey5")
    sethue("gold")
    fontsize(10)
    fontface("Avenir-Black")
    drawgraph(
        g,
        margin=40,
        layout = Stress(iterations = 100),
        edgegaps = 13,
        edgestrokeweights = 3,
        edgecurvature = 4,
        edgestrokecolors = [HSB(360rand(), 0.7, 0.8) for i in 1:ne(g)],
        vertexlabels = (vtx) -> begin
            string(pkgnames[vtx])
        end,
        vertexshapes = (v) -> begin
            tx = textextents(pkgnames[v])
            @layer begin
                setopacity(0.8)
                sethue("grey80")
                box(O, 1.2tx[5], 1.5tx[4], 5, :fill)
            end
        end,
    )
end 800 700
Example block output

There's an extended investigation of package dependencies later in this section.

The London Tube

One real-world example of a small network is the London Underground, known as “the Tube”. The 250 or so stations in the network can be modelled using a simple graph.

Setup

If you want to follow along, this is the setup required. The CSV file examples/tubedata-modified.csv contains the station names, latitude and longitudes, and connectivity details.

using Karnak, Graphs, NetworkLayout, Colors
using DataFrames, CSV

# positions are in LatLong

tubedata = CSV.File("examples/tubedata-modified.csv") |> DataFrame

amatrix = Matrix(tubedata[:, 4:270])

extrema_lat = extrema(tubedata.Latitude)
extrema_long = extrema(tubedata.Longitude)

# scale LatLong and flip in y to fit into current drawing

positions = @. Point(
    rescale(tubedata.Longitude, extrema_long..., -280, 280),
    rescale(tubedata.Latitude, extrema_lat..., 280, -280))

stations = tubedata[!,:Station]

find(str) = findfirst(isequal(str), stations)
find(x::Int64) = stations[x]

g = Graph(amatrix)

The tube “map” is stored in g, as a {267, 308} undirected simple Int64 graph.

The find() functions are just a quick way to convert between station names and ID numbers:

find("Waterloo")
find(244)

Not a map

Most London residents and visitors are used to seeing the famous Tube Map:

tube map

It’s a design classic, hand-drawn by Harry Beck in 1931, and updated regularly ever since. As an electrical engineer, Beck represented the sprawling London track network as a tidy circuit board. For Beck, the important thing about the map was to show the connections, rather than the accurate geography.

Our version looks very different, but it is at least geographically more accurate, because the latitude and longitude values of the stations are passed to layout.

@drawsvg begin
background("grey10")
sethue("grey50")
drawgraph(g,
    layout = positions,
    vertexshapes = :none,
    vertexlabeltextcolors = colorant"white",
    vertexlabels = find.(1:nv(g)),
    vertexlabelfontsizes = 6)
end

The layout algorithms - layout = spring and layout = stress - do a reasonable job, but people like to see north at the top of maps, and south at the bottom, not mixed up in any direction, like these.

@drawsvg begin
background("grey20")
tiles = Tiler(800, 400, 1, 2)
sethue("white")

@layer begin
    translate(first(tiles[1]))
    drawgraph(g,
        layout=spring,
        boundingbox = BoundingBox(box(O, 400, 400)),
        vertexshapes = :none,
        vertexlabeltextcolors = colorant"white",
        vertexlabels = find.(1:nv(g)),
        vertexlabelfontsizes = 6
        )
end

@layer begin
    translate(first(tiles[2]))
    drawgraph(g,
        layout=stress,
        boundingbox = BoundingBox(box(O, 400, 400)),
        vertexshapes = :none,
        vertexlabeltextcolors = colorant"white",
        vertexlabels = find.(vertices(g)),
        vertexlabelfontsizes = 6
        )
end

end 800 400

Train terminates here

Use the degree() function to show just the station names at the end of a line: a vertex with a degree of 1 is a terminus:

@drawsvg begin
background("grey90")
sethue("black")
drawgraph(g, layout=positions,
    vertexshapesizes = 2,
    vertexlabels = [(degree(g, n) == 1) ? find(n) : ""
        for n in vertices(g)],
    vertexlabeltextcolors = colorant"blue"
    )
end

These labels show names familiar to all Tube-riders - the ones shown on the front of trains and on platform indicators. (It's unusual to visit them all, unless you're like Geoff Marshall, who holds the world record for the fastest time visiting every Tube station.)

Neighbors

The best connected station is also one of the oldest, dating back to 1863:

find(argmax(degree(g, 1:nv(g))))

Its neighbors are:

find.(neighbors(g, find("Baker Street")))

Centrality

Using Graphs.jl's tools for measuring centrality, Baker Street is again at the top of the list, but Green Park (the Queen's nearest tube station), scores highly, despite not being in the top 20 busiest stations.

@drawsvg begin
background("grey10")
translate(0, -200)
scale(3)
bc = betweenness_centrality(g)
sethue("gold")
_, maxbc = extrema(bc)
drawgraph(g, layout = positions,
    vertexlabels = (vtx) -> bc[vtx] > maxbc * 0.6 && string(find(vtx)),
    vertexlabeltextcolors = colorant"cyan",
    vertexlabelfontsizes = 6,
    vertexshapesizes = 1 .+ 10bc,
    vertexfillcolors = HSB.(rescale.(bc, 0, maximum(bc), 0, 300), 0.7, 0.8),
    )
end 800 600

Mornington Crescent

A route from Heathrow Terminal 5 to Mornington Crescent can be found using a_star().

heathrow_to_morningtoncrescent = a_star(g,
    find("Heathrow Terminal 5"),
    find("Mornington Crescent"))

@drawsvg begin
background("grey70")
translate(0, -100)
scale(3)

sethue("grey50")
drawgraph(g,
    layout = positions,
    vertexshapesizes = 1)

sethue("black")
fontsize(4)
drawgraph(g,
    layout = positions,
    vertexshapes = :none,
    edgelist = heathrow_to_morningtoncrescent,
    edgestrokeweights = 3,
    vertexlabels = (vtx) -> begin
        if vtx ∈ src.(heathrow_to_morningtoncrescent) ||
           vtx ∈ dst.(heathrow_to_morningtoncrescent)
             circle(positions[vtx], 2, :fill)
             label(find(vtx), :e, positions[vtx])
        end
    end)
end

The route found by a_star is:

[find(dst(e)) for e in heathrow_to_morningtoncrescent]

Information about the required changes - at Victoria from the Piccadilly line to the Victoria Line, and at Warren Street from the Victoria Line to the Northern Line - is not part of the graph. Routes across the Tube network, like the trains, follow the tracks (edges). The concept of “lines” (Victoria, Circle, etc) isn’t part of the graph structure, but a colorful layer imposed on top of the track network.

Pandemic

Graphs.jl provides many functions for analysing graph networks. The diffusion() function appears to simulate the diffusion of an infection from some starting vertices and the probability of spreading.

The function returns an array of arrays, where each one contains the vertex numbers of newly "infected" vertices. For example, in this result:

[[1], Int64[], [22, 15, 25], ...]

the first stage showed vertex 1 "infected"; stage two was free of incident; but on stage 3 vertices 22, 15, and 25 have become "infected".

So here, apparently, is a simulation of what might happen when an infection arrives at Heathrow Airport's Terminal 5 tube station, and starts spreading through the tube network.

function frame(scene, framenumber, diffresult)
    background("black")
    sethue("gold")
    text(string(framenumber), boxbottomleft() + (10, -10))
    drawgraph(g, layout = positions, vertexshapesizes = 3)
    for k in 1:framenumber
        i = diffresult[k]
        drawgraph(
            g,
            layout = positions,
            edgelines = 0,
            vertexfunction = (v, c) -> begin
                if !isempty(i)
                    if v ∈ i
                        sethue("red")
                        circle(positions[v], 5, :fill)
                    end
                end
            end,
        )
    end
end

function main()
    amovie = Movie(600, 600, "diff")
    diffresult = diffusion(g, 0.2, 200, initial_infections=[find("Heathrow Terminal 5")])
    animate(amovie,
        Scene(amovie, (s, f) -> frame(s, f, diffresult), 1:length(diffresult)),
        framerate=10,
        creategif=true,
        pathname="/tmp/diff.gif")
end
main()

diffusion

The current logo for the Graphs.jl package was easily drawn using Karnak.

I wanted to use the graph coloring feature (greedy_color()), but unfortunately it was too clever, managing to color the graph using only two colors instead of the four I was hoping to use.

using Graphs
using Karnak
using Colors

function lighten(col::Colorant, f)
    c = convert(RGB, col)
    return RGB(f * c.r, f * c.g, f * c.b)
end

function julia_sphere(pt::Point, w, col::Colorant;
        action = :none)
    setmesh(mesh(
        makebezierpath(box(pt, w * 1.5, w * 1.5)),
        [lighten(col, .5),
         lighten(col, 1.75),
         lighten(col, 1.25),
         lighten(col, .6)]))
    circle(pt, w, action)
end

function draw_edge(pt1, pt2)
    for k in 0:0.1:1
        setline(rescale(k, 0, 1, 25, 1))
        sethue(lighten(colorant"grey50", rescale(k, 0, 1, 0.5, 1.5)))
        setopacity(rescale(k, 0, 1, 0.5, 0.75))
        line(pt1, pt2, :stroke)
    end
end

# positions for vertices

outerpts = ngonside(O, 450, 4, π/4, vertices=true)
innerpts = ngonside(O, 150, 4, π/2, vertices=true)
pts = vcat(outerpts, innerpts)

colors = map(c -> RGB(c...),
    [Karnak.Luxor.julia_blue, Karnak.Luxor.julia_red, Karnak.Luxor.julia_green, Karnak.Luxor.julia_purple])

@drawsvg begin
    squircle(O, 294, 294, :clip, rt=0.2)
    sethue("black")
    paint()
    g = SimpleGraph([
        Edge(1,2), Edge(2,3), Edge(3,4), Edge(1,4),
        Edge(5,6), Edge(6,7), Edge(7,8), Edge(5,8),
        Edge(1,5), Edge(2,6), Edge(3,7), Edge(4,8),
        ])

    drawgraph(Graph(g),
        layout=pts,
        vertexfunction = (v, c) -> begin
            d = distance(O, c[v])
            d > 200 ? k = 0 : k = 1
            julia_sphere(c[v],
                 rescale(d, 0, 200, 52, 50), colors[mod1(v + k, 4)],
                action=:fill)
        end,
        edgefunction = (k, s, d, f, t) -> draw_edge(f, t)
        )
end
Example block output

Julia Package Dependencies

This example was originally developed by Mathieu Besançon and presented as part of the workshop: Analyzing Graphs at Scale, at JuliaCon 2020. You can watch the video on YouTube.

The most important changes since the video was made are:

  • the renaming of LightGraphs.jl to Graphs.jl

  • the way to access the list of packages has changed

The code builds a dependency graph of the connections (ie which package depends on which package) for Julia packages in the General registry.

Then it's possible draw some pictures, such as this chonky SVG file showing the dependencies for the Colors.jl package:

package dependencies for Colors

Or this one, which attempts to highlight just the more connected packages in the Colors.jl dependency graph:

package dependencies for Colors

Setup:

using Graphs
using MetaGraphs
using TOML
using Karnak
using Colors

Finding the general registry

On my computer, the registry is in its default location. You might need to modify these lines if yours is is another location:

path_to_general = expanduser("~/.julia/registries/General")
registry_file = Pkg.TOML.parsefile(joinpath(path_to_general, "Registry.toml"))
packages_info = registry_file["packages"];

First we need the name and location of every package:

# Julia <= v1.6
pkg_paths = map(values(packages_info)) do d
    (name = d["name"], path = d["path"])
end
# Julia >= v1.7
pkg_paths = map(values(Pkg.Registry.reachable_registries()[1].pkgs)) do d
    (name = d.name, path = d.path)
end

The result in pkg_paths is a vector of tuples, containing the name and location of every package:

7495-element Vector{NamedTuple{(:name, :path), Tuple{String, String}}}:
 (name = "COSMA_jll", path = "C/COSMA_jll")
 (name = "CitableImage", path = "C/CitableImage")
 (name = "Trixi2Img", path = "T/Trixi2Img")
 (name = "ImPlot", path = "I/ImPlot")

Find packages that depend on a specific package

The function find_direct_deps() finds all the packages (names and locations) that directly depend on a specific named package.

function find_direct_deps(registry_path, pkg_paths, source)
    filter(pkg_paths) do pkg_path
        deps_file = joinpath(registry_path, pkg_path.path, "Deps.toml")
        # some packages don't have Deps.toml file
        isfile(deps_file) && begin
            deps_struct = Pkg.TOML.parsefile(deps_file)
            any(values(deps_struct)) do d
                source in keys(d)
            end
        end
    end
end

We can now find out how many packages depend on a particular package. For example, how many packages depend on Colors.jl (my favourite)?

find_direct_deps(path_to_general, pkg_paths, "Colors")

giving this result:

227-element Vector{NamedTuple{(:name, :path), Tuple{String, String}}}:
 (name = "TopologyPreprocessing", path = "T/TopologyPreprocessing")
 (name = "DynamicGrids", path = "D/DynamicGrids")
 (name = "SimpleSDMLayers", path = "S/SimpleSDMLayers")
 (name = "UnderwaterAcoustics", path = "U/UnderwaterAcoustics")
 (name = "ColorSchemeTools", path = "C/ColorSchemeTools")
 (name = "PrincipalMomentAnalysisApp", path = "P/PrincipalMomentAnalysisApp")
 ⋮
 (name = "SoilWater_ToolBox", path = "S/SoilWater_ToolBox")
 (name = "Starlight", path = "S/Starlight")
 (name = "Dojo", path = "D/Dojo")
 (name = "OpticSim", path = "O/OpticSim")
 (name = "LVServer", path = "L/LVServer")

Colors.jl has 227 packages that depend on it. When Mathieu ran this code in 2020 on "LightGraphs", the vector had 92 elements. Today, in 2022, for "Graphs", the vector has 115 elements.

Build a directed tree

The next function, build_tree(), will build a directed graph of the dependencies on Colors.jl. Starting at the root package (Colors) the loop finds all its dependencies, then finds the dependencies of all of those dependent packages, and continues doing this until it reaches packages that have no dependencies. These are the "leaves" at the tip of the tree's branches.

function build_tree(registry_path, pkg_paths, root)
    g = MetaDiGraph()
    add_vertex!(g)
    set_prop!(g, 1, :name, root)
    i = 1
    explored_nodes = Set{String}((root,))
    while true
        i % 50 == 0 && print(i, " ")
        current_node = get_prop(g, i, :name)
        direct_deps = find_direct_deps(registry_path, pkg_paths, current_node)
        filter!(d -> d.name ∉ explored_nodes, direct_deps)
        if isempty(direct_deps) && i >= nv(g)
           break
        end
        for ddep in direct_deps
           push!(explored_nodes, ddep.name)
           add_vertex!(g)
           set_prop!(g, nv(g), :name, ddep.name)
           add_edge!(g, i, nv(g))
        end
        i += 1
    end
    return g
end
Note

This function takes some time to run - about 8 minutes for about 1400 iterations on my computer.

g = build_tree(path_to_general, pkg_paths, "Colors")

{1375, 1374} directed Int64 metagraph with Float64 weights defined by :weight (default weight 1.0)

Notice that there are 1375 nodes, but one less edge. The Colors.jl package is the root of the tree, and doesn't connect to anything else, in this analysis.) Of course, it depends on quite a few, but that's another graph story.)

The result is a directed metagraph. In a metagraph, as implemented by MetaGraphs.jl, it's possible to add information to vertices using set_prop() and get_prop().

To find all the package names in the graph that are directly connected to Colors.jl, we can broadcast get_prop() like this:

get_prop.(Ref(g), outneighbors(g, 1), :name)

227-element Vector{String}:
 "SqState"
 "InteractBase"
 "ImageMetadata"
 "PlantGeom"
 "MicrobiomePlots"
 "MeshViz"
 "SGtSNEpi"
 "ColorSchemes"
 "CairoMakie"
 ⋮
 "GenomicMaps"
 "ModiaPlot"
 "Thebes"
 "ConstrainedDynamics"
 "AutomotiveVisualization"
 "Flux"

outneighbors returns a list of all neighbors connected to vertex v by an outgoing edge.

Shortest paths and lengths of branches

The dijkstra_shortest_paths() function finds the paths between the designated package and all its dependencies.

The returned value is a DijkstraState object, with fields parents, dists, predecessors, pathcounts, and closest_vertices.

Looking at the dists (distances), we see that one package is very close indeed at 0.0 - that's Colors.jl itself.

spath_result = dijkstra_shortest_paths(g, 1)

spath_result.dists

1375-element Vector{Float64}:
 0.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 ⋮
 5.0
 5.0
 5.0
 6.0
 6.0
 6.0
 6.0
 6.0
 6.0
 7.0
 7.0

Or in a barchart:

scores = [count(==(i), spath_result.dists) for i in unique(spath_result.dists)]
Example block output

The "furthest" packages from Colors.jl - the two seven steps away - are:

for idx in eachindex(spath_result.dists)
    if spath_result.dists[idx] == 7
         println(get_prop(g, idx, :name))
    end
end

QuantumESPRESSOExpress
Recommenders

Computing a full subgraph

All the package names are obtained with:

all_packages = get_prop.(Ref(g), vertices(g), :name)

Vector{String}:
 "Colors"
 "TopologyPreprocessing"
 "DynamicGrids"
 "SimpleSDMLayers"
 "UnderwaterAcoustics"
 "ColorSchemeTools"
 ⋮
 "ReservoirComputing"
 "TreeParzen"
 "GeoStatsImages"
 "StoppingInterface"
 "QuantumESPRESSO"
 "Recommenders"
 "QuantumESPRESSOExpress"

These next commands build a metagraph, using the package names:

full_graph = MetaDiGraph(length(all_packages))

{1375, 0} directed Int64 metagraph with Float64 weights defined by :weight (default weight 1.0)

Assigning names to the vertices:

for v in vertices(full_graph)
    set_prop!(full_graph, v, :name, all_packages[v])
end

Build the full graph:

for v in vertices(full_graph)
    pkg_name = get_prop(full_graph, v, :name)
    dependent_packages = find_direct_deps(path_to_general, pkg_paths, pkg_name)
    for dep_pkg in dependent_packages
        pkg_idx = findfirst(==(dep_pkg.name), all_packages)
        # only packages in graph
        if pkg_idx !== nothing
            add_edge!(full_graph, pkg_idx, v)
        end
    end
end

It's useful to be able to save and load this graph:

# using Graphs, MetaGraphs
# save:
savegraph("examples/full_graph.lg", full_graph))

# load:
full_graph = loadgraph("examples/full_graph.lg", MGFormat())

All roads lead to home

The code in this next example draws the vertices as an impressionistic point cloud, and uses the a_star() function to find a path from some random package back to Colors.jl.

@drawsvg begin
    background("black")
    sethue("white")
    fontface("BarlowCondensed-Bold")
    random_package = rand(1:nv(full_graph))
    astar = a_star(full_graph, random_package, 1)
    astar_vertices = sort(unique(vcat([src(e) for e in astar], [dst(e) for e in astar])), rev=true)
    drawgraph(g,
        edgelist=astar,
        layout=spring,
        vertexlabels = (v) -> v ∈ astar_vertices[[begin, end]] && get_prop(full_graph, v, :name),
        vertexlabeltextcolors = colorant"white",
        vertexlabelfontsizes = 20,
        vertexlabelfontfaces = "BarlowCondensed-Bold",
        vertexshapesizes = .5,
        vertexstrokecolors = :none)
    textfit(string(join(get_prop.(Ref(full_graph), astar_vertices, :name), " > ")),
        BoundingBox(box(boxbottomcenter() + (0, -30), 600, 50)))
end 800 800

chain of deps

Pagerank

This code computes the pagerank of the graph. It returns a long list of numbers, the centrality score for each vertex.

ranks = pagerank(full_graph)

1375-element Vector{Float64}:
 0.15339826572024867
 0.00020384989099126913
 0.00043081071431843264
 0.0002471787754446367
 0.0005504809666182096
 0.00020384989099126913
 0.00020384989099126913
 0.00034105802509359976
 0.0012284800170342895
 ⋮
 0.00020384989099126913
 0.00020384989099126913
 0.00042629607921470863
 0.00020384989099126913
 0.0002616217369290926
@drawsvg begin
    background("black")
    sethue("white")
    fontface("BarlowCondensed-Bold")
    ranks = pagerank(full_graph)
    drawgraph(g,
        edgelist = [],
        layout=spring,
        vertexshapes = :none,
        vertexlabels = (v) -> ranks[v] > 0.001 && get_prop(full_graph, v, :name),
        vertexlabelfontsizes = 500ranks,
        vertexlabeltextcolors = colorant"white")
end 800 800

pagerank

The problem with this representation is one of overlapping labels. This isn't an issue we can fix easily in Karnak.

Highly ranked

With some sorting, we can find the highest ranked packages in this part of the ecosystem.

sorted_indices = sort(eachindex(ranks), by=i->ranks[i], rev=true)

1375-element Vector{Int64}:
   1
 543
 137
 112
 144
 164
   ⋮
 259
 258
 729
 730
 688
get_prop.(Ref(full_graph), sorted_indices, :name)

1375-element Vector{String}:
 "Colors"
 "Plots"
 "ImageCore"
 "PlotUtils"
 "ColorSchemes"
 "ColorVectorSpace"
 ⋮
 "TopOptMakie"
 "VTKDataIO"
 "EFTfitter"
 "SpmGrids"
 "ElectronTests"

Most dependencies, most depended on

indegree() returns the number of edges which end at a vertex. For a package, this is another way of seeing how many other packages depend on it.

in_sorted_indices = sort(vertices(full_graph),
    by = i -> indegree(full_graph, i), rev = true)

1375-element Vector{Int64}:
 543
   1
  65
  98
 133
 137
   ⋮
 287
 743
 744
 285
 688
get_prop.(Ref(full_graph), in_sorted_indices, :name)

1375-element Vector{String}:
 "Plots"
 "Colors"
 "Flux"
 "Images"
 "PyPlot"
 "ImageCore"
 ⋮
 "PolaronMobility"
 "CineFiles"
 "MadNLPGraph"
 "MicroscopyLabels"
 "ElectronTests"

outdegree() finds the number of edges which start at a vertex.

out_sorted_indices = sort(vertices(full_graph),
    by = i -> outdegree(full_graph, i), rev=true)

1375-element Vector{Int64}:
 372
  98
  35
  24
 300
 153
   ⋮
 776
 777
 778
 779
   1
get_prop.(Ref(full_graph), out_sorted_indices, :name)

1375-element Vector{String}:
 "StatisticalRethinking"
 "Images"
 "Makie"
 "MakieGallery"
 "PredictMDExtra"
 "GLMakie"
 ⋮
 "MimiPAGE2020"
 "MimiSNEASY"
 "OptiMimi"
 "SyntheticNetworks"
 "Colors"
ranks_betweenness = betweenness_centrality(full_graph)

1375-element Vector{Float64}:
 0.0
 0.0
 3.1186467511475384e-5
 5.300816007616213e-7
 5.830897608377834e-5
 0.0
 ⋮
 0.0
 0.0
 4.24065280609297e-6
 0.0
 1.0601632015232426e-6
sorted_indices_betweenness = sort(vertices(full_graph),
    by = i -> ranks_betweenness[i], rev=true)

1375-element Vector{Int64}:
 144
  98
 112
 543
 461
  35
   ⋮
 562
 563
 564
 565
   1
get_prop.(Ref(full_graph), sorted_indices_betweenness, :name)

1375-element Vector{String}:
 "ColorSchemes"
 "Images"
 "PlotUtils"
 "Plots"
 "ImageIO"
 "Makie"
 ⋮
 "BridgeDiffEq"
 "BridgeLandmarks"
 "FCA"
 "BEASTDataPrep"
 "Colors"

Is_cyclic

is_cyclic() returns true if the graph contains a cycle.

is_cyclic(full_graph)

true

for cycle in simplecycles(full_graph)
    names = get_prop.(Ref(full_graph), cycle, :name)
    @info names
end

["ImageCore", "MosaicViews"]
["Images", "ImageSegmentation"]
["Makie", "GLMakie"]
["POMDPPolicies", "BeliefUpdaters", "POMDPModels", "POMDPSimulators"]
["BeliefUpdaters", "POMDPModels"]
["BeliefUpdaters", "POMDPModels", "POMDPSimulators"]
["ReinforcementLearning", "ReinforcementLearningEnvironmentDiscrete"]
["Modia3D", "Modia"]
["RasterDataSources", "GeoData"]
["DSGE", "StateSpaceRoutines"]

For that first cycle: ImageCore.jl's Project.toml file has MosaicViews.jl in its [deps] section, and MosaicViews.jl has ImageCore.jl in the [extras] section of its Project.toml file.

Draw some graphs

Visualizations of graphs are sometimes (often?) better at communicating vague ideas such as complexity and shape. But it's quite difficult to render graphs as rich as these to show the connections clearly while also showing all the labels such that they're easy to read.

The solution may be to print out these graph representations and stick them on a nearby wall, although, with Julia's General Registry changing every day, it would be out of date before the glue dries.

wall art office graph dependency

The images above were made with the following code.

@pdf begin
    background("black")
    sethue("gold")
    setline(0.3)
    drawgraph(g,
        layout = stress,
        edgefunction = (k, s, d, f, t) -> begin
            @layer begin
                sl = slope(O, t)
                sethue(HSVA(rescale(sl, 0, 2π, 0, 360), 0.7, 0.7, .9))
                line(f, t, :stroke)
            end
        end,
        vertexfunction = (v, c) -> begin
            @layer begin
                t = get_prop(g, v, :name)
                te = textextents(t)
                setopacity(0.7)
                sethue("grey10")
                fontsize(3)
                box(c[v], te[3]/2, te[4]/2, :fill)
                setopacity(1)
                sethue("white")
                text(t, c[v], halign=:center, valign=:middle)
            end
        end)
    @info " finish drawing"
end 2500 2500 "/tmp/graph-dependencies-colors.pdf"
using ColorSchemes

@svg begin
    background("black")
    maxdeg = maximum(degree(full_graph))
    drawgraph(full_graph,
        layout = spring,
        edgelines = 0,
        vertexfunction = (v, c) -> begin
            d = degree(full_graph, v)
            @layer begin
                sethue(get(ColorSchemes.darkrainbow, rescale(d, 1, maxdeg)))
                circle(c[v], rescale(d, 1, 270, 2, 20), :fill)
            end
            if d > 20
                fontsize(rescale(d, 1, maxdeg, 5, 20))
                setcolor("white")
                textoutlines(all_packages[v], c[v], halign=:center, valign=:bottom, :fill)
                setline(rescale(d, 1, maxdeg, 0.25, 1))
                sethue("black")
                textoutlines(all_packages[v], c[v], halign=:center, valign=:bottom, :stroke)
            end
        end)
end 1200 1200 "/tmp/graph-dependencies-2.svg"