Powered by AppSignal & Oban Pro

Visualize primary and replicas (inspired by Waterpark)

content/standalone_visualization.livemd

Visualize primary and replicas (inspired by Waterpark)

Mix.install([
  {:kino, "~> 0.17.0"}
], consolidate_protocols: false)

Library modules

This is a visualization of how one could distribute stateful actors (a primary + n replicas) across a cluster of VMs, spanning multiple regions and availability zones (in each region). This is inspired by the talk Waterpark: Transforming Healthcare with Distributed Actors - Bryan Hunter - NDC Oslo 2025.

  • You can find a polished transcript of that talk here.
  • Check the videos on Twitter or Blue Sky.

This short stand-alone livebook is supposed to be opened using livebook.dev. The data for one sample customer is hard-coded in this livebook.

If you have questions, comments, hit me up :-).

defmodule ComputeNode do
  @moduledoc """
  Represents a compute node in the distributed system.

  A compute node is uniquely identified by its region, availability zone, and VM ID.
  """

  @enforce_keys [:region, :zone, :id]
  defstruct [:region, :zone, :id]

  @type t :: %__MODULE__{
          region: String.t(),
          zone: String.t(),
          id: String.t()
        }

  @doc """
  Creates a new compute node.

  ## Examples

      iex> ComputeNode.new("Amsterdam", "AZ1", "VM1")
      %ComputeNode{region: "Amsterdam", zone: "AZ1", id: "VM1"}
  """
  def new(region, zone, id) when is_integer(id) do
    %__MODULE__{
      region: region,
      zone: zone,
      id: id |> Integer.to_string() |> String.pad_leading(4, "0")
    }
  end

  def new(region, zone, id) do
    %__MODULE__{
      region: region,
      zone: zone,
      id: id
    }
  end

  @doc """
  Returns the availability zone tuple for the node.
  """
  def az_tuple(%__MODULE__{region: region, zone: zone}) do
    {region, zone}
  end
end

defimpl String.Chars, for: ComputeNode do
  def to_string(%ComputeNode{region: region, zone: zone, id: id}) when is_integer(id) do
    "#{region}-#{zone}-#{String.pad_leading(Integer.to_string(id), 4, "0")}"
  end

  def to_string(%ComputeNode{region: region, zone: zone, id: id}) do
    "#{region}-#{zone}-#{id}"
  end
end

defmodule Drawing do
  @node_width 32
  @node_height 20
  @node_spacing 8
  @zone_padding 10
  @region_padding 20
  @canvas_padding 20
  @zone_header_height 20
  @region_header_height 25

  @doc """
  Generates an SVG visualization of a replication strategy.

  ## Parameters
  - `nodes`: List of ComputeNode structs, where the first is the primary
  - `replica_count`: Number of replicas to create from the rest of the list

  ## Returns
  A string containing the SVG markup
  """
  def generate_svg([primary | rest], replica_count) do
    # Determine node roles
    replicas = Enum.take(rest, replica_count)
    unused = Enum.drop(rest, replica_count)

    # Organize all nodes by region and zone
    all_nodes = [primary | rest]
    organized = organize_nodes(all_nodes)

    # Create color mapping for each node
    node_colors = create_node_color_map(primary, replicas, unused)

    # Calculate complete layout with positions
    layout = calculate_complete_layout(organized, node_colors)

    # Generate SVG content
    svg_content = generate_svg_structure(layout)

    # Generate arrows from primary to replicas
    arrows = generate_replica_arrows(primary, replicas, layout, organized)

    # Combine everything
    svg_content <> arrows <> "\n"
  end

  # Organize nodes into a nested map: region -> zone -> [nodes]
  defp organize_nodes(nodes) do
    nodes
    |> Enum.group_by(&amp; &amp;1.region)
    |> Enum.map(fn {region, region_nodes} ->
      zones =
        region_nodes
        |> Enum.group_by(&amp; &amp;1.zone)
        |> Enum.map(fn {zone, zone_nodes} ->
          sorted_nodes = Enum.sort_by(zone_nodes, &amp; &amp;1.id)
          {zone, sorted_nodes}
        end)
        |> Enum.sort_by(&amp;elem(&amp;1, 0))
        |> Map.new()

      {region, zones}
    end)
    |> Enum.sort_by(&amp;elem(&amp;1, 0))
    |> Map.new()
  end

  # Create a map of node -> color for easy lookup
  defp create_node_color_map(primary, replicas, unused) do
    primary_map = %{node_key(primary) => "#ff4444"}
    replica_map = replicas |> Enum.map(&amp;{node_key(&amp;1), "#4444ff"}) |> Map.new()
    unused_map = unused |> Enum.map(&amp;{node_key(&amp;1), "#999999"}) |> Map.new()

    Map.merge(primary_map, Map.merge(replica_map, unused_map))
  end

  # Create a unique key for a node
  defp node_key(node), do: "#{node.region}-#{node.zone}-#{node.id}"

  # Calculate complete layout with all positions
  defp calculate_complete_layout(organized, node_colors) do
    regions = Map.keys(organized) |> Enum.sort()

    # Calculate region grid layout
    {region_rows, region_cols} = calculate_grid_layout(length(regions), :region)

    # First pass: calculate dimensions for all regions
    region_dimensions =
      regions
      |> Enum.map(fn region_name ->
        zones = Map.get(organized, region_name, %{})
        {region_name, calculate_region_size(zones)}
      end)
      |> Map.new()

    # Calculate canvas size based on grid layout
    {canvas_width, canvas_height} =
      calculate_canvas_size(
        region_dimensions,
        regions,
        region_rows,
        region_cols
      )

    # Second pass: position regions and their contents
    positioned_regions =
      regions
      |> Enum.with_index()
      |> Enum.map(fn {region_name, index} ->
        row = div(index, region_cols)
        col = rem(index, region_cols)

        # Calculate region position
        {region_x, region_y} =
          calculate_region_position(
            region_dimensions,
            regions,
            row,
            col,
            region_cols
          )

        # Get region dimensions
        {region_width, region_height} = Map.get(region_dimensions, region_name)

        # Position zones within region
        zones = Map.get(organized, region_name, %{})
        positioned_zones = position_zones_in_region(zones, region_x, region_y, node_colors)

        %{
          name: region_name,
          x: region_x,
          y: region_y,
          width: region_width,
          height: region_height,
          zones: positioned_zones
        }
      end)

    # Collect all node positions for arrow drawing
    node_positions = collect_node_positions(positioned_regions)

    %{
      canvas_width: canvas_width,
      canvas_height: canvas_height,
      regions: positioned_regions,
      node_positions: node_positions
    }
  end

  # Calculate the size of a region based on its zones
  defp calculate_region_size(zones) when zones == %{} do
    # Minimum region size for empty regions
    {120, 80}
  end

  defp calculate_region_size(zones) do
    zone_count = map_size(zones)
    {zone_rows, zone_cols} = calculate_grid_layout(zone_count, :zone)

    # Calculate max zone dimensions
    max_nodes_in_zone =
      zones
      |> Map.values()
      |> Enum.map(&amp;length/1)
      |> Enum.max(fn -> 0 end)

    # Calculate zone dimensions
    {node_rows, node_cols} = calculate_grid_layout(max_nodes_in_zone, :node)
    zone_width = node_cols * @node_width + (node_cols + 1) * @node_spacing
    zone_height = @zone_header_height + node_rows * @node_height + (node_rows + 1) * @node_spacing

    # Calculate region dimensions
    region_width = zone_cols * zone_width + (zone_cols + 1) * @zone_padding

    region_height =
      @region_header_height + zone_rows * zone_height + (zone_rows + 1) * @zone_padding

    {region_width, region_height}
  end

  # Calculate total canvas size
  defp calculate_canvas_size(region_dimensions, regions, rows, cols) do
    # Calculate max width for each column
    col_widths =
      0..(cols - 1)
      |> Enum.map(fn col ->
        regions
        |> Enum.with_index()
        |> Enum.filter(fn {_, idx} -> rem(idx, cols) == col end)
        |> Enum.map(fn {region, _} ->
          elem(Map.get(region_dimensions, region, {0, 0}), 0)
        end)
        |> Enum.max(fn -> 0 end)
      end)

    # Calculate max height for each row
    row_heights =
      0..(rows - 1)
      |> Enum.map(fn row ->
        regions
        |> Enum.with_index()
        |> Enum.filter(fn {_, idx} -> div(idx, cols) == row end)
        |> Enum.map(fn {region, _} ->
          elem(Map.get(region_dimensions, region, {0, 0}), 1)
        end)
        |> Enum.max(fn -> 0 end)
      end)

    total_width = Enum.sum(col_widths) + (cols + 1) * @region_padding + 2 * @canvas_padding
    total_height = Enum.sum(row_heights) + (rows + 1) * @region_padding + 2 * @canvas_padding

    {total_width, total_height}
  end

  # Calculate the position of a region in the grid
  defp calculate_region_position(region_dimensions, regions, row, col, region_cols) do
    # Calculate x offset from previous columns
    x_offset =
      regions
      |> Enum.with_index()
      |> Enum.filter(fn {_, idx} ->
        div(idx, region_cols) == row &amp;&amp; rem(idx, region_cols) < col
      end)
      |> Enum.map(fn {region, _} ->
        elem(Map.get(region_dimensions, region, {0, 0}), 0)
      end)
      |> Enum.sum()

    # Calculate y offset from previous rows
    y_offset =
      regions
      |> Enum.with_index()
      |> Enum.filter(fn {_, idx} -> div(idx, region_cols) < row end)
      |> Enum.group_by(fn {_, idx} -> div(idx, region_cols) end)
      |> Enum.map(fn {_, group} ->
        group
        |> Enum.map(fn {region, _} ->
          elem(Map.get(region_dimensions, region, {0, 0}), 1)
        end)
        |> Enum.max(fn -> 0 end)
      end)
      |> Enum.sum()

    x = @canvas_padding + @region_padding + x_offset + col * @region_padding
    y = @canvas_padding + @region_padding + y_offset + row * @region_padding

    {x, y}
  end

  # Position zones within a region
  defp position_zones_in_region(zones, region_x, region_y, node_colors) do
    zone_list = zones |> Map.to_list() |> Enum.sort_by(&amp;elem(&amp;1, 0))
    zone_count = length(zone_list)

    {_zone_rows, zone_cols} = calculate_grid_layout(zone_count, :zone)

    zone_list
    |> Enum.with_index()
    |> Enum.map(fn {{zone_name, nodes}, index} ->
      row = div(index, zone_cols)
      col = rem(index, zone_cols)

      # Calculate zone dimensions
      node_count = length(nodes)
      {node_rows, node_cols} = calculate_grid_layout(node_count, :node)

      zone_width =
        if node_count > 0 do
          node_cols * @node_width + (node_cols + 1) * @node_spacing
        else
          80
        end

      zone_height =
        if node_count > 0 do
          @zone_header_height + node_rows * @node_height + (node_rows + 1) * @node_spacing
        else
          60
        end

      # Calculate zone position within region
      zone_x = region_x + @zone_padding + col * (zone_width + @zone_padding)

      zone_y =
        region_y + @region_header_height + @zone_padding + row * (zone_height + @zone_padding)

      # Position nodes within zone
      positioned_nodes =
        nodes
        |> Enum.with_index()
        |> Enum.map(fn {node, n_index} ->
          n_row = div(n_index, node_cols)
          n_col = rem(n_index, node_cols)

          node_x =
            zone_x + @node_spacing + n_col * (@node_width + @node_spacing) + @node_width / 2

          node_y =
            zone_y + @zone_header_height + @node_spacing + n_row * (@node_height + @node_spacing) +
              @node_height / 2

          %{
            node: node,
            x: node_x,
            y: node_y,
            color: Map.get(node_colors, node_key(node), "#999999")
          }
        end)

      %{
        name: zone_name,
        x: zone_x,
        y: zone_y,
        width: zone_width,
        height: zone_height,
        nodes: positioned_nodes
      }
    end)
  end

  # Collect all node positions for arrow drawing
  defp collect_node_positions(regions) do
    regions
    |> Enum.flat_map(fn region ->
      region.zones
      |> Enum.flat_map(fn zone ->
        zone.nodes
        |> Enum.map(fn node_info ->
          {node_key(node_info.node), {node_info.x, node_info.y}}
        end)
      end)
    end)
    |> Map.new()
  end

  # Calculate optimal grid layout based on count and type
  defp calculate_grid_layout(count, type) do
    case {type, count} do
      {_, 0} ->
        {1, 1}

      {_, 1} ->
        {1, 1}

      # Region layouts - prefer vertical for small counts
      {:region, n} when n <= 2 ->
        {n, 1}

      {:region, 3} ->
        {1, 3}

      {:region, 4} ->
        {2, 2}

      {:region, 5} ->
        {3, 2}

      {:region, 6} ->
        {3, 2}

      {:region, n} when n <= 9 ->
        {3, 3}

      {:region, n} when n <= 12 ->
        {4, 3}

      # Zone layouts - prefer vertical for small counts
      {:zone, n} when n <= 4 ->
        {n, 1}

      {:zone, n} when n <= 6 ->
        {3, 2}

      {:zone, n} when n <= 9 ->
        {3, 3}

      # Node layouts - prefer horizontal for small counts
      {:node, n} when n <= 4 ->
        {1, n}

      {:node, n} when n <= 8 ->
        {2, 4}

      {:node, n} when n <= 12 ->
        {3, 4}

      # Default: make it as square as possible
      {_, n} ->
        cols = :math.ceil(:math.sqrt(n)) |> round()
        rows = :math.ceil(n / cols) |> round()
        {rows, cols}
    end
  end

  # Generate the SVG structure
  defp generate_svg_structure(layout) do
    """
    
      #{generate_styles()}
      #{generate_regions_svg(layout.regions)}
    """
  end

  # Generate CSS styles
  defp generate_styles do
    """
    
      .region { fill: #f5f5f5; stroke: #000000; stroke-width: 2; }
      .zone { fill: #ffffff; stroke: #999999; stroke-width: 1; }
      .node { stroke-width: 1; }
      .node-text { fill: white; font-size: 10px; font-family: Arial, sans-serif; text-anchor: middle; }
      .region-title { font-size: 14px; font-weight: bold; font-family: Arial, sans-serif; }
      .zone-title { font-size: 11px; font-family: Arial, sans-serif; fill: #666666; }
      .arrow-thick { stroke: #ff4444; stroke-width: 3; opacity: 1.0; fill: none; }
      .arrow-medium { stroke: #ff4444; stroke-width: 2; opacity: 0.8; fill: none; }
      .arrow-thin { stroke: #ff4444; stroke-width: 1; opacity: 0.5; stroke-dasharray: 4,2; fill: none; }
      .arrowhead-thick { fill: #ff4444; opacity: 1.0; }
      .arrowhead-medium { fill: #ff4444; opacity: 0.8; }
      .arrowhead-thin { fill: #ff4444; opacity: 0.5; }
    
    """
  end

  # Generate SVG for all regions
  defp generate_regions_svg(regions) do
    regions
    |> Enum.map(&amp;generate_region_svg/1)
    |> Enum.join("\n")
  end

  # Generate SVG for a single region
  defp generate_region_svg(region) do
    """
    
      
      #{region.name}
      #{generate_zones_svg(region.zones)}
    
    """
  end

  # Generate SVG for all zones
  defp generate_zones_svg(zones) do
    zones
    |> Enum.map(&amp;generate_zone_svg/1)
    |> Enum.join("\n")
  end

  # Generate SVG for a single zone
  defp generate_zone_svg(zone) do
    """
    
      
      #{zone.name}
      #{generate_nodes_svg(zone.nodes)}
    
    """
  end

  # Generate SVG for all nodes
  defp generate_nodes_svg(nodes) do
    nodes
    |> Enum.map(&amp;generate_node_svg/1)
    |> Enum.join("\n")
  end

  # Generate SVG for a single node
  defp generate_node_svg(node_info) do
    # Node position is center, adjust to top-left for rectangle
    x = node_info.x - @node_width / 2
    y = node_info.y - @node_height / 2

    stroke_color =
      case node_info.color do
        "#ff4444" -> "#cc0000"
        "#4444ff" -> "#0000cc"
        _ -> "#666666"
      end

    """
    
      
      #{node_info.node.id}
    
    """
  end

  # Generate arrows from primary to replicas
  defp generate_replica_arrows(primary, replicas, layout, organized) do
    primary_pos = Map.get(layout.node_positions, node_key(primary))

    if primary_pos == nil do
      ""
    else
      regions = Map.keys(organized) |> length()

      # Track connected regions and zones for priority
      {arrows, _} =
        replicas
        |> Enum.with_index()
        |> Enum.map_reduce({MapSet.new(), MapSet.new()}, fn {replica, index},
                                                            {connected_regions, connected_zones} ->
          replica_pos = Map.get(layout.node_positions, node_key(replica))

          if replica_pos == nil do
            {"", {connected_regions, connected_zones}}
          else
            # Determine arrow style based on priority
            {arrow_class, new_regions, new_zones} =
              determine_arrow_priority(
                replica,
                index,
                regions,
                connected_regions,
                connected_zones
              )

            arrow_svg = draw_arrow_with_head(primary_pos, replica_pos, arrow_class)

            {arrow_svg, {new_regions, new_zones}}
          end
        end)

      arrows |> Enum.join("\n")
    end
  end

  # Determine arrow priority and style
  defp determine_arrow_priority(replica, _index, region_count, connected_regions, connected_zones) do
    zone_key = {replica.region, replica.zone}

    cond do
      # First connections to each region (thick arrows)
      not MapSet.member?(connected_regions, replica.region) and
          MapSet.size(connected_regions) < region_count ->
        {"arrow-thick", MapSet.put(connected_regions, replica.region),
         MapSet.put(connected_zones, zone_key)}

      # Connections to new zones (medium arrows)
      not MapSet.member?(connected_zones, zone_key) ->
        {"arrow-medium", connected_regions, MapSet.put(connected_zones, zone_key)}

      # Additional redundancy (thin arrows)
      true ->
        {"arrow-thin", connected_regions, connected_zones}
    end
  end

  # Draw arrow with proper intersection calculation
  defp draw_arrow_with_head({x1, y1}, {x2, y2}, arrow_class) do
    # Calculate angle from source to target
    dx = x2 - x1
    dy = y2 - y1
    angle = :math.atan2(dy, dx)

    # Calculate border intersection points
    {start_x, start_y} = calculate_rect_border_point(x1, y1, angle, @node_width, @node_height)

    {end_x, end_y} =
      calculate_rect_border_point(x2, y2, angle + :math.pi(), @node_width, @node_height)

    # Create arrow head
    arrowhead_class = String.replace(arrow_class, "arrow-", "arrowhead-")
    arrowhead = create_arrowhead(end_x, end_y, angle, arrowhead_class)

    """
    
      
      #{arrowhead}
    
    """
  end

  # Calculate where line intersects rectangle border
  defp calculate_rect_border_point(cx, cy, angle, width, height) do
    # Rectangle half-dimensions
    hw = width / 2
    hh = height / 2

    # Direction components
    cos_a = :math.cos(angle)
    sin_a = :math.sin(angle)

    # Calculate intersection with rectangle edges
    # We need the point where the ray from center intersects the rectangle

    # Check which edge we'll hit first
    t_x = if cos_a != 0, do: hw / abs(cos_a), else: 999_999
    t_y = if sin_a != 0, do: hh / abs(sin_a), else: 999_999

    t = min(t_x, t_y)

    # Calculate intersection point
    x = cx + t * cos_a
    y = cy + t * sin_a

    {x, y}
  end

  # Create SVG arrowhead
  defp create_arrowhead(x, y, angle, class) do
    # Arrowhead dimensions
    size = 8
    half_width = 3

    # Calculate the three points of the triangle
    cos_a = :math.cos(angle)
    sin_a = :math.sin(angle)

    # Perpendicular to arrow direction
    perp_cos = -sin_a
    perp_sin = cos_a

    # Base center (back from tip)
    base_x = x - size * cos_a
    base_y = y - size * sin_a

    # Two base points
    p1_x = base_x + half_width * perp_cos
    p1_y = base_y + half_width * perp_sin

    p2_x = base_x - half_width * perp_cos
    p2_y = base_y - half_width * perp_sin

    """
    
    """
  end
end
nodes = [
  %ComputeNode{region: "Utah", zone: "Zone B", id: "VM 5"},
  %ComputeNode{region: "Utah", zone: "Zone A", id: "VM 2"},
  %ComputeNode{region: "Florida", zone: "Zone A", id: "VM 1"},
  %ComputeNode{region: "Texas", zone: "Zone A", id: "VM 6"},
  %ComputeNode{region: "Tennessee", zone: "Zone A", id: "VM 1"},
  %ComputeNode{region: "Florida", zone: "Zone B", id: "VM 5"},
  %ComputeNode{region: "Texas", zone: "Zone B", id: "VM 7"},
  %ComputeNode{region: "Tennessee", zone: "Zone B", id: "VM 2"},
  %ComputeNode{region: "Florida", zone: "Zone A", id: "VM 8"},
  %ComputeNode{region: "Tennessee", zone: "Zone A", id: "VM 7"},
  %ComputeNode{region: "Texas", zone: "Zone A", id: "VM 2"},
  %ComputeNode{region: "Utah", zone: "Zone A", id: "VM 1"},
  %ComputeNode{region: "Florida", zone: "Zone B", id: "VM 6"},
  %ComputeNode{region: "Utah", zone: "Zone A", id: "VM 4"},
  %ComputeNode{region: "Texas", zone: "Zone B", id: "VM 6"},
  %ComputeNode{region: "Tennessee", zone: "Zone A", id: "VM 6"},
  %ComputeNode{region: "Florida", zone: "Zone A", id: "VM 5"},
  %ComputeNode{region: "Utah", zone: "Zone B", id: "VM 4"},
  %ComputeNode{region: "Tennessee", zone: "Zone A", id: "VM 2"},
  %ComputeNode{region: "Texas", zone: "Zone A", id: "VM 4"},
  %ComputeNode{region: "Florida", zone: "Zone B", id: "VM 4"},
  %ComputeNode{region: "Utah", zone: "Zone A", id: "VM 6"},
  %ComputeNode{region: "Tennessee", zone: "Zone B", id: "VM 5"},
  %ComputeNode{region: "Texas", zone: "Zone B", id: "VM 8"},
  %ComputeNode{region: "Florida", zone: "Zone B", id: "VM 1"},
  %ComputeNode{region: "Utah", zone: "Zone B", id: "VM 1"},
  %ComputeNode{region: "Tennessee", zone: "Zone A", id: "VM 4"},
  %ComputeNode{region: "Texas", zone: "Zone B", id: "VM 1"},
  %ComputeNode{region: "Florida", zone: "Zone B", id: "VM 3"},
  %ComputeNode{region: "Utah", zone: "Zone B", id: "VM 6"},
  %ComputeNode{region: "Tennessee", zone: "Zone B", id: "VM 8"},
  %ComputeNode{region: "Texas", zone: "Zone B", id: "VM 4"},
  %ComputeNode{region: "Florida", zone: "Zone A", id: "VM 4"},
  %ComputeNode{region: "Utah", zone: "Zone A", id: "VM 8"},
  %ComputeNode{region: "Tennessee", zone: "Zone A", id: "VM 5"},
  %ComputeNode{region: "Texas", zone: "Zone A", id: "VM 1"},
  %ComputeNode{region: "Florida", zone: "Zone A", id: "VM 3"},
  %ComputeNode{region: "Utah", zone: "Zone A", id: "VM 3"},
  %ComputeNode{region: "Tennessee", zone: "Zone B", id: "VM 6"},
  %ComputeNode{region: "Texas", zone: "Zone B", id: "VM 5"},
  %ComputeNode{region: "Florida", zone: "Zone A", id: "VM 6"},
  %ComputeNode{region: "Utah", zone: "Zone B", id: "VM 7"},
  %ComputeNode{region: "Tennessee", zone: "Zone B", id: "VM 7"},
  %ComputeNode{region: "Texas", zone: "Zone A", id: "VM 3"},
  %ComputeNode{region: "Florida", zone: "Zone B", id: "VM 7"},
  %ComputeNode{region: "Utah", zone: "Zone B", id: "VM 8"},
  %ComputeNode{region: "Tennessee", zone: "Zone A", id: "VM 3"},
  %ComputeNode{region: "Texas", zone: "Zone A", id: "VM 7"},
  %ComputeNode{region: "Florida", zone: "Zone B", id: "VM 8"},
  %ComputeNode{region: "Utah", zone: "Zone B", id: "VM 2"},
  %ComputeNode{region: "Texas", zone: "Zone B", id: "VM 3"},
  %ComputeNode{region: "Tennessee", zone: "Zone B", id: "VM 4"},
  %ComputeNode{region: "Utah", zone: "Zone A", id: "VM 7"},
  %ComputeNode{region: "Florida", zone: "Zone A", id: "VM 2"},
  %ComputeNode{region: "Tennessee", zone: "Zone A", id: "VM 8"},
  %ComputeNode{region: "Texas", zone: "Zone B", id: "VM 2"},
  %ComputeNode{region: "Utah", zone: "Zone B", id: "VM 3"},
  %ComputeNode{region: "Florida", zone: "Zone B", id: "VM 2"},
  %ComputeNode{region: "Texas", zone: "Zone A", id: "VM 5"},
  %ComputeNode{region: "Tennessee", zone: "Zone B", id: "VM 1"},
  %ComputeNode{region: "Florida", zone: "Zone A", id: "VM 7"},
  %ComputeNode{region: "Utah", zone: "Zone A", id: "VM 5"},
  %ComputeNode{region: "Tennessee", zone: "Zone B", id: "VM 3"},
  %ComputeNode{region: "Texas", zone: "Zone A", id: "VM 8"}
]

Kino.nothing()
determine_nodes = fn (_name, _regions, _zones, _vms) ->
  # datacenters =
  #   for region <- regions,
  #       az <- zones,
  #       id <- vms do
  #     ComputeNode.new(region, az, id)
  #   end
  #
  # bucket_hashes = 
  #   datacenters
  #   |> RendevousHash.pre_compute_list()
  #      
  # bucket_hashes
  # |> RendevousHash.list(name)
  # |> RendevousHash.sort_by_optimum_storage_resiliency()

  nodes
end

# Create frames for dynamic content
slider_frame = Kino.Frame.new()
value_frame = Kino.Frame.new()
image2_frame = Kino.Frame.new()

# Variable to track current nodes and slider
current_slider = nil

# Function to render images and value display
render_content = fn nodes, scale_factor ->
  max = length(nodes) - 1
  max = if max < 1, do: 1, else: max
  
  # Update value display
  value_display = Kino.Markdown.new("**Replication Factor:** #{scale_factor} | **Nodes:** #{length(nodes)} | **Max:** #{max}")
  Kino.Frame.render(value_frame, value_display)
  
  # Generate and display images
  image2 = Drawing.generate_svg(nodes, scale_factor) |> Kino.Image.new(:svg)
  Kino.Frame.render(image2_frame, image2)
end

# Function to create and set up a new slider
create_slider = fn nodes ->
  max = length(nodes) - 1
  max = if max < 1, do: 1, else: max  # Ensure max is at least 1
  
  # Use consistent default value
  default_scale = min(length(nodes), max)
  
  slider = Kino.Input.range("Replication Factor", min: 1, max: max, default: default_scale, step: 1, debounce: 100)
  
  slider
  |> Kino.Control.stream()
  |> Kino.listen(fn event ->
    scale_factor = event.value |> trunc()
    render_content.(nodes, scale_factor)
  end)
  
  # Return both slider and its default value
  {slider, default_scale}
end

# Function to update everything when text changes
update_from_text = fn  ->
  # Determine nodes from text value
  nodes = determine_nodes.(nil, nil, nil, nil)
  
  # Create new slider with correct max and get its default value
  {slider, default_scale} = create_slider.(nodes)
  
  # Render the new slider
  Kino.Frame.render(slider_frame, slider)
  
  # Immediately render content with the slider's default value
  render_content.(nodes, default_scale)
end

# Display the layout first
layout = [slider_frame,value_frame,image2_frame] |> Kino.Layout.grid(columns: 1)

# Initial setup with default value
update_from_text.()  # Use the same default as the text input

layout