Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

D3 Wardley Map

d3wardleymap.livemd

D3 Wardley Map

Mix.install([
  :vega_lite,
  :kino,
  {:kino_vega_lite, "~> 0.1.1"}
])

Functionally Working, missing labels

This is an attempt to create a Wardley Mapping rendered in Livebook

defmodule Kino.D3JS do
  use Kino.JS

  def new(spec) do
    Kino.JS.new(__MODULE__, spec)
  end

  asset "main.js" do
    """
    import "https://d3js.org/d3.v7.js"
    export function init(ctx, spec) {

      let specDetails = JSON.parse(spec)

      const myId = specDetails.id || "my-id"
      const width = specDetails.width || 800;
      const height = specDetails.height || 400;
      const background = specDetails.background || "white"
      const padding = specDetails.padding || 20;
      const mapItems = specDetails["map"] || [];

      ctx.root.innerHTML = ``;

      // Note that the maths y axis is the reflection of the d3 one!
      const yaxis = d3.scaleLinear()
      .domain([0, 1.0]) 
      .range([height - padding, padding]);

      const xaxis = d3.scaleLinear()
      .domain([0, 1.0])
      .range([padding, width - padding]);

      let svg = d3.select(`#${myId}`)
        .attr('width', width)
        .attr('height', height)
        .style('background-color', background);

      // y axis  
      svg.append('line')
        .style("stroke", "black")
        .style("stroke-width", 2)
        .attr("x1", xaxis(0))
        .attr("y1", yaxis(0) )
        .attr("x2", xaxis(0))
        .attr("y2", yaxis(1)); 

      // x axis  
      svg.append('line')
        .style("stroke", "black")
        .style("stroke-width", 2)
        .attr("x1", xaxis(0))
        .attr("y1", yaxis(0) )
        .attr("x2", xaxis(1))
        .attr("y2", yaxis(0));   

      //Boundaries  
      svg.append('line')
        .style("stroke", "black")
        .style("stroke-width", 1)
        .style("stroke-dasharray", ("3, 3"))
        .attr("x1", xaxis(0.25))
        .attr("y1", yaxis(0) )
        .attr("x2", xaxis(0.25))
        .attr("y2", yaxis(1));      

     svg.append('line')
        .style("stroke", "black")
        .style("stroke-width", 1)
        .style("stroke-dasharray", ("3, 3"))
        .attr("x1", xaxis(0.5))
        .attr("y1", yaxis(0) )
        .attr("x2", xaxis(0.5))
        .attr("y2", yaxis(1));      
     
    svg.append('line')
        .style("stroke", "black")
        .style("stroke-width", 1)
        .style("stroke-dasharray", ("3, 3"))
        .attr("x1", xaxis(0.75))
        .attr("y1", yaxis(0) )
        .attr("x2", xaxis(0.75))
        .attr("y2", yaxis(1));      

    svg.append("text")
    .attr("x", xaxis(0))
    .attr("y", yaxis(0) + (padding * 0.75))
    .text(() => "Genesis")

     svg.append("text")
    .attr("x", xaxis(0.25))
    .attr("y", yaxis(0) + (padding * 0.75))
    .text(() => "Custom Built")

    svg.append("text")
    .attr("x", xaxis(0.5))
    .attr("y", yaxis(0) + (padding * 0.75))
    .text(() => "Product (+ Rental)")

    svg.append("text")
    .attr("x", xaxis(0.75))
    .attr("y", yaxis(0) + (padding * 0.75))
    .text(() => "Commodity (+ Utility)")

    svg.append("text")
    .attr("x", xaxis(0) - 1 * padding)
    .attr("y", yaxis(1) - 0.25 * padding)
    .text(() => "Value Chain");

    var name
    var parts
    var x
    var y
    var start
    var end
    var startEvolved
    var endEvolved
        
    let summary = mapItems.reduce((acc, item) => {
      if (item.startsWith("anchor")) {
         name = /anchor (?[^ $]*)/.exec(item).groups.name
         parts = item.split("[");
         parts = parts[1];
         parts = parts.split("]");
         parts = parts[0];
         parts = parts.split(",");
         x = parseFloat(parts[1])
         y = parseFloat(parts[0])
         
         acc[name] = {name: name, x: x, y: y, type: "anchor" }
      }

      if (item.startsWith("component")) {
        name = /component\s([^\[]*)\s\\[/.exec(item)[1]
        parts = item.split("[");
        parts = parts[1];
        parts = parts.split("]");
        parts = parts[0];
        parts = parts.split(",");
        x = parseFloat(parts[1]);
        y = parseFloat(parts[0]);

        acc[name] = {name: name, x: x, y: y, type: "component" }
      }

      if (item.startsWith("evolve")) {
        name = /evolve\s([^\[]*)\s\\[/.exec(item)[1]
        parts = item.split("[");
        parts = parts[1];
        parts = parts.split("]");
        parts = parts[0];
        parts = parts.split(",");
        x = parseFloat(parts[1]);
        y = parseFloat(parts[0]);

        acc[name + "__evolve"] = {name: name, x: x, y: y, type: "evolve" }
      }

      return acc
    },{});

    // Lines between
    mapItems.forEach((item) => {
      if(item.includes("->")) {
        parts = item.split("->");
        
        if(summary[parts[0]] != undefined && summary[parts[1]] != undefined) {
          let start = summary[parts[0]];
          let end = summary[parts[1]];
          let startEvolved = undefined
          let endEvolved = undefined

          svg.append('line')
          .style("stroke", "black")
          .style("stroke-width", 1)
          .attr("x1", xaxis(start.x))
          .attr("y1", yaxis(start.y) )
          .attr("x2", xaxis(end.x))
          .attr("y2", yaxis(end.y));
          
          if(summary[start.name + "__evolve"] != undefined) {
            startEvolved = summary[start.name + "__evolve"]
          
            svg.append('line')
            .style("stroke", "red")
            .style("stroke-width", 1)
            .attr("x1", xaxis(startEvolved.x))
            .attr("y1", yaxis(startEvolved.y) )
            .attr("x2", xaxis(end.x))
            .attr("y2", yaxis(end.y));
          }

          if(summary[end.name + "__evolve"] != undefined) {
            endEvolved = summary[end.name + "__evolve"]
          
            svg.append('line')
            .style("stroke", "red")
            .style("stroke-width", 1)
            .attr("x1", xaxis(start.x))
            .attr("y1", yaxis(start.y) )
            .attr("x2", xaxis(endEvolved.x))
            .attr("y2", yaxis(endEvolved.y));
          }

          if (startEvolved != undefined && endEvolved != undefined) {
              svg.append('line')
            .style("stroke", "red")
            .style("stroke-width", 1)
            .attr("x1", xaxis(startEvolved.x))
            .attr("y1", yaxis(startEvolved.y) )
            .attr("x2", xaxis(endEvolved.x))
            .attr("y2", yaxis(endEvolved.y));          
          }

        }
      }
    })

    // Circles and text
    for (const [key, item] of Object.entries(summary)) {
      if (item.type === "anchor") {
        svg.append("text")
        .attr("x", xaxis(item.x))
        .attr("y", yaxis(item.y))
        .text(() => item.name)
      }

      if (item.type === "component") {


        if(summary[item.name + "__evolve"] != undefined) {
          let evolved = summary[item.name + "__evolve"] 
          
          svg.append('line')
          .style("stroke", "red")
          .style("stroke-width", 1)
          .style("stroke-dasharray", ("3, 3"))
          .attr("x1", xaxis(item.x))
          .attr("y1", yaxis(item.y) )
          .attr("x2", xaxis(evolved.x))
          .attr("y2", yaxis(evolved.y)); 
        }

        svg.append("circle")
        .attr("cx", xaxis(item.x))
        .attr("cy", yaxis(item.y))
        .attr("r", 3)
        .style("stroke", "black")
        .style("fill", "white");

        svg.append('rect')
        .attr('x', xaxis(item.x) + 7)
        .attr('y', yaxis(item.y) - 7)
        .attr('width', 8 * item.name.length)
        .attr('height', 14)
        .attr('stroke', 'white')
        .attr('fill', 'white');

        svg.append("text")
        .attr("x", xaxis(item.x) + 7)
        .attr("y", yaxis(item.y) + 5)
        .text(() => item.name)
        

      }

      if(item.type === "evolve") {
        svg.append("circle")
        .attr("cx", xaxis(item.x))
        .attr("cy", yaxis(item.y))
        .attr("r", 3)
        .style("stroke", "red")
        .style("fill", "white")

        svg.append('rect')
        .attr('x', xaxis(item.x) + 7)
        .attr('y', yaxis(item.y) - 7)
        .attr('width', 8 * item.name.length)
        .attr('height', 14)
        .attr('stroke', 'white')
        .attr('fill', 'white');

        svg.append("text")
        .attr("x", xaxis(item.x) + 7)
        .attr("y", yaxis(item.y) + 5)
        .style('fill', 'red')
        .text(() => name)    
      }
    }


    // ==== end of JS ====
    }
    """
  end
end
Kino.D3JS.new("""
{
  "id": "myid7",
  "height": "400",
  "width": "800",
  "map" : [
    "anchor Business [0.95, 0.63]",
    "anchor Public [0.95, 0.78]",
    "component Cup of Tea [0.79, 0.61]",    
    "component Cup [0.73, 0.78]",
    "component Tea [0.63, 0.81]",
    "component Hot Water [0.52, 0.80]",
    "component Water [0.38, 0.82]",
    "component Kettle [0.43, 0.35]",
    "evolve Kettle [0.43, 0.62]",
    "component Power [0.10, 0.71]",
    "evolve Power [0.10, 0.89]",
    "Business->Cup of Tea",
    "Public->Cup of Tea",
    "Cup of Tea->Cup",
    "Cup of Tea->Tea",
    "Cup of Tea->Hot Water",
    "Hot Water->Water",
    "Hot Water->Kettle", 
    "Kettle->Power"
  ]
}
""")