Mix.install([
{:kino, "~> 0.15.3"},
{:runic, github: "zblanco/runic", branch: "zw/map"}
])
defmodule Kino.Cytoscape do
use Kino.JS
def new(g, options \\ [])
def new(%Graph{} = graph, options) do
graph
|> to_edgelist_json()
|> new(options(options))
end
def new(edge_list, options) when is_list(options) do
nodes =
edge_list
|> Enum.flat_map(fn %{data: edge} ->
[
%{data: %{id: "#{edge.source}", name: "#{edge.source}"}},
%{data: %{id: "#{edge.target}", name: "#{edge.target}"}}
]
end)
|> Enum.uniq_by(& &1.data.id)
new(nodes ++ edge_list, options(options))
end
def new(graph, options) do
Kino.JS.new(__MODULE__, Map.put(options, :elements, graph))
end
defp options(:dag) do
options = options([])
dag_layout = %{
name: "breadthfirst",
fit: true,
directed: true,
avoidOverlap: true,
spacingFactor: 1.0,
grid: false,
padding: 0,
anime: true,
circle: false,
userZoomingEnabled: true
}
Map.put(options, :layout_options, dag_layout)
end
defp options(options) do
cytoscape =
options[:cytoscape] ||
%{
"zoomingEnabled" => true,
"userZoomingEnabled" => true,
"panningEnabled" => true,
"userPanningEnabled" => true,
"boxSelectionEnabled" => false
}
node_style =
options[:node_style] ||
%{
"background-color" => "#ffffff",
"font-size" => 14,
"width" => 120,
"shape" => "ellipse",
"height" => 80,
"text-wrap" => "wrap",
"text-max-width" => 56,
"color" => "#475569",
"label" => "data(name)",
"text-halign" => "center",
"text-valign" => "center",
"border-width" => 2,
"border-color" => "#94a3b8",
"border-opacity" => 0.5
}
edge_style =
options[:edge_style] ||
%{
"width" => 2,
"font-size" => 12,
"label" => "data(label)",
"line-color" => "#94a3b8",
"target-arrow-color" => "#94a3b8",
"target-arrow-shape" => "triangle",
"curve-style" => "bezier"
}
layout_options =
options[:layout_options] ||
%{
name: "cose",
fit: true,
directed: true,
avoidOverlap: true,
spacingFactor: 1.0,
grid: false,
padding: 0,
anime: true,
circle: false,
userZoomingEnabled: true
}
%{
cytoscape: cytoscape,
node_style: node_style,
edge_style: edge_style,
layout_options: layout_options
}
end
defp to_edgelist_json(graph) do
vertices =
graph
|> Graph.vertices()
|> Enum.map(fn v ->
vertex_label = vertex_label(v)
%{data: %{id: "#{vertex_label}", name: "#{vertex_label}"}}
end)
edges =
graph
|> Graph.edges()
|> Enum.map(fn edge ->
edge_label = edge_label(edge.label)
v1 = vertex_label(edge.v1)
v2 = vertex_label(edge.v2)
%{
data: %{
id: "#{edge_label}-#{v1}#{v2}" |> :erlang.phash2(),
label: "#{edge_label}",
source: v1,
target: v2
}
}
end)
vertices ++ edges
end
defp edge_label(label), do: label
defp vertex_label({_, _, _} = ast), do: Macro.to_string(ast)
defp vertex_label(%Runic.Workflow.Root{}), do: :root
defp vertex_label(%{name: name}) when not is_nil(name), do: name
defp vertex_label(%{__struct__: struct} = vertex),
do: "#{to_string(struct)}#{:erlang.phash2(vertex)}"
defp vertex_label(otherwise), do: otherwise
asset "main.js" do
"""
import "https://cdn.jsdelivr.net/npm/[email protected]/dist/cytoscape.min.js";
export function init(ctx, cyto_data) {
ctx.root.innerHTML = `<div id='cyto' style='width: 896px; height: 400px;'></div>`;
var cy = cytoscape({
...cyto_data.cytoscape,
...{
container: document.getElementById('cyto'),
elements: cyto_data.elements,
style: [
{
selector: 'node',
style: cyto_data.node_style
},
{
selector: 'edge',
style: cyto_data.edge_style
}
]
}
});
cy.layout(cyto_data.layout_options).run();
}
"""
end
end
Runic is a tool for modeling your workflows as data that can be composed together at runtime.
Runic components can be integrated into a Runic.Workflow and evaluated lazily in concurrent contexts.
Runic Workflows are a decorated dataflow graph (a DAG - "directed acyclic graph") capable of modeling rules, pipelines, and state machines and more.
Basic data flow dependencies such as in a pipeline are modeled as %Step{}
structs (nodes/vertices) in the graph with directed edges (arrows) between steps.
A step can be thought of as a simple input -> output lambda function. e.g.
require Runic
step = Runic.step(fn x -> x + 1 end, name: :add_one)
workflow = Runic.workflow(
name: "example pipeline workflow",
steps: [
Runic.step(fn x -> x + 1 end, name: :add_one),
Runic.step(fn x -> x * 2 end, name: :times_two),
Runic.step(fn x -> x - 1 end, name: :minus_one)
]
)
workflow.graph
|> Kino.Cytoscape.new(:dag)
alias Runic.Workflow
wrk =
workflow
|> Workflow.react_until_satisfied(2)
wrk |> Workflow.raw_productions()
wrk |> Workflow.productions()
serial_pipeline =
Runic.workflow(
name: "serial_pipeline",
steps: [
{Runic.step(fn x -> x + 1 end, name: :add_one), [
{Runic.step(fn x -> x * 2 end, name: :times_two), [
Runic.step(fn x -> x - 1 end, name: :minus_one)
]}
]},
Runic.step(fn x -> x + 5 end, name: :add_five)
]
)
serial_pipeline.graph
|> Kino.Cytoscape.new(:dag)
defmodule TextProcessing do
def tokenize(text) do
text
|> String.downcase()
|> String.split(~r/[^[:alnum:]\-]/u, trim: true)
end
def count_words(list_of_words) do
list_of_words
|> Enum.reduce(Map.new(), fn word, map ->
Map.update(map, word, 1, &(&1 + 1))
end)
end
def count_uniques(word_count) do
Enum.count(word_count)
end
def first_word(list_of_words) do
List.first(list_of_words)
end
def last_word(list_of_words) do
List.last(list_of_words)
end
end
import TextProcessing
word_count =
"anybody want a peanut?"
|> tokenize()
|> count_words()
|> dbg()
first_word =
"anybody want a peanut?"
|> tokenize()
|> first_word()
|> dbg()
last_word =
"anybody want a peanut?"
|> tokenize()
|> last_word()
|> dbg()
text_processing_workflow =
Runic.workflow(
name: "basic text processing example",
steps: [
{Runic.step(&tokenize/1),
[
{Runic.step(&count_words/1),
[
Runic.step(&count_uniques/1)
]},
Runic.step(&first_word/1),
Runic.step(&last_word/1)
]}
]
)
text_processing_workflow.graph
|> Kino.Cytoscape.new(:dag)
text_processing_workflow
|> Workflow.react_until_satisfied("anybody want a peanut?")
|> Workflow.raw_productions()
join_with_many_dependencies =
Runic.workflow(
name: "workflow with joins",
steps: [
{[Runic.step(fn num -> num * 2 end), Runic.step(fn num -> num * 3 end)],
[
Runic.step(fn num_1, num_2 -> num_1 * num_2 end),
Runic.step(fn num_1, num_2 -> num_1 + num_2 end),
Runic.step(fn num_1, num_2 -> num_2 - num_1 end)
]}
]
)
join_with_many_dependencies.graph
|> Kino.Cytoscape.new(:dag)
rules_workflow =
Runic.workflow(
name: "a test workflow",
rules: [
Runic.rule(fn :foo -> :bar end, name: "foobar"),
Runic.rule(
if: fn lhs -> lhs == :potato end,
do: fn rhs -> rhs == :tomato end,
name: "tomato when potato"
),
Runic.rule(
fn item when is_integer(item) and item > 41 and item < 43 ->
"the answer to life the universe and everything"
end,
name: "what about the question?"
)
]
)
rules_workflow.graph
|> Kino.Cytoscape.new(:dag)
state_machine =
Runic.state_machine(
name: "transitional_factor",
init: 0,
reducer: fn
num, state when is_integer(num) and state >= 0 and state < 10 -> state + num * 1
num, state when is_integer(num) and state >= 10 and state < 20 -> state + num * 2
num, state when is_integer(num) and state >= 20 and state < 30 -> state + num * 3
_num, state -> state
end
)
|> Runic.transmute()
state_machine.graph
|> Kino.Cytoscape.new(:dag)
map_expression =
Runic.workflow(
name: "map test",
steps: [
{Runic.step(fn num -> Enum.map(0..3, &(&1 + num)) end),
[
Runic.map(
{Runic.step(fn num -> num * 2 end),
[
Runic.step(fn num -> num + 1 end),
Runic.step(fn num -> num + 4 end)
]}
)
]}
]
)
map_expression.graph
|> Kino.Cytoscape.new(:dag)
reduce_example =
Runic.workflow(
name: "reduce test",
steps: [
{Runic.step(fn num -> Enum.map(0..3, &(&1 + num)) end),
[
{Runic.map(fn num -> num * 2 end),
[
Runic.reduce([], fn num, acc -> [num | acc] end)
]}
]}
]
)
reduce_example.graph
|> Kino.Cytoscape.new(:dag)
reduce_wrk =
Runic.workflow(
name: "reduce test",
steps: [
{Runic.step(fn _ -> 0..3 end),
[
Runic.map(
{step(fn num -> num + 1 end),
[
Runic.step(fn num -> num + 4 end),
Runic.step(fn num -> num + 2 end, name: :plus2)
]},
name: "map"
)
]}
]
)
reduce_wrk =
Workflow.add(
reduce_wrk,
Runic.reduce(0, fn num, acc -> num + acc end, name: "reduce", map: "map"),
to: :plus2
)
reduce_wrk
|> Workflow.react_until_satisfied(:anything)
|> Workflow.productions()
reduce_wrk.graph
|> Kino.Cytoscape.new(:dag)