Sponsored by AppSignal
Would you like to see your link here? Contact us
Notesclub

Attribute Heat Maps

notebooks/attribute_heat_map.livemd

Attribute Heat Maps

Mix.install([
  :vega_lite,
  :kino,
  :kino_vega_lite,
  {:ssbu, github: "rob-brown/amo_system", subdir: "apps/ssbu"}
])

alias VegaLite, as: Vl
alias AmiiboSerialization.Amiibo

Summary

This Livebook is used to aggregate a directory of bins into a heat map showing how their attributes coorelate. Additionally, a single bin file may be chosen to see how it compares to the aggregate.

Key Retail

In order to decrypt the bin, you will need to find a file named key_retail.bin on the Internet. Once found, you need to base 64 encode the key. Go into the Secrets tab in the side bar. Create a secret named KEY_RETAIL and paste in the base64 encoded data. If you did this right, then you can decrypt the amiibo bin files.

case System.get_env("LB_KEY_RETAIL") do
  nil ->
    IO.puts("You need to add a new secret named `KEY_RETAIL` to your Livebook")

  key ->
    System.put_env("KEY_RETAIL", key)
end

Inputs

The bin directory is required. The Livebook will grab all files in there with a .bin extension. All files with a .bin extension must be an amiibo. Otherwise, you may get an error when running. The Livebook will not dig into subdirectories.

The file name is optional. It must match exactly and just be the file name, not the path, ex. MyAmiibo.bin. If .bin is not included, then it will be added. If the Livebook can find this bin in the directory, it will overlay its attributes over the heat map. If left blank or not found, then there will be no overlay.

input = Kino.Input.text("Bin directory") |> Kino.render()
focus_input = Kino.Input.text("File name (optional)") |> Kino.render()
secondary_input = Kino.Input.text("Secondary file name (optional)") |> Kino.render()
:ok
focus_name = Kino.Input.read(focus_input)
secondary_name = Kino.Input.read(secondary_input)
dir = input |> Kino.Input.read() |> Path.expand()

secondary_name =
  if String.ends_with?(secondary_name, ".bin") do
    secondary_name
  else
    secondary_name <> ".bin"
  end

focus_name =
  if String.ends_with?(focus_name, ".bin") do
    focus_name
  else
    focus_name <> ".bin"
  end

The following cells show the list of files found and the number found. This is useful for debugging if something looks wrong.

bins =
  for f <- File.ls!(dir), Path.extname(f) == ".bin" do
    Path.join(dir, f)
  end
length(bins)

Next, grabs all the attributes and prepares it for display.

attributes =
  Enum.flat_map(bins, fn b ->
    {:ok, amiibo} = Amiibo.read_file(b)

    for {name, {_, _, value}} <- SSBU.Attributes.Serializer.parse_amiibo(amiibo) do
      %{"attribute" => name, "value" => value, "name" => Path.basename(b)}
    end
  end)
attribute_names = attributes |> Enum.map(&amp; &amp;1["attribute"]) |> Enum.uniq()

Show the focused bin’s personality just for some extra info.

bin = Enum.find(bins, &amp;(Path.basename(&amp;1) == focus_name))
{:ok, focus_amiibo} = Amiibo.read_file(bin)
SSBU.Personality.parse_amiibo(focus_amiibo)

Coarse Heat Map

Now for the actual heat map generation. This heat map shows the attributes grouped in buckets of 5%. This is probably a good level of detail but you can change accordingly.

Once generated, you can download the image by clicking on the elipses (...) or right-clicking and saving.

title =
  case {focus_name, secondary_name} do
    {"", _} ->
      "#{Path.basename(dir)} (#{length(bins)} bins)"

    {focus_name, ""} ->
      "#{Path.basename(dir)} (#{length(bins)} bins) focused on #{focus_name}"

    {focus_name, secondary_name} ->
      "#{Path.basename(dir)} (#{length(bins)} bins) focused on #{focus_name}/#{secondary_name}"
  end
Vl.new(width: 1000, height: 1000)
|> Vl.data_from_values(attributes)
|> Vl.encode_field(:y, "attribute",
  type: :nominal,
  sort: attribute_names,
  title: "Attribute"
)
|> Vl.layers([
  Vl.new(title: title)
  |> Vl.mark(:rect)
  |> Vl.encode_field(:x, "value",
    type: :quantitative,
    title: "Value",
    axis: [label_angle: 90],
    bin: [minbins: 20, maxbins: 20]
  )
  |> Vl.encode(:color, aggregate: :count),
  Vl.new()
  |> Vl.mark(:point, color: :firebrick)
  |> Vl.transform(filter: "datum.name == \"#{focus_name}\"")
  |> Vl.encode_field(:x, "value",
    type: :quantitative,
    title: "Value"
  ),
  Vl.new()
  |> Vl.mark(:square, color: :black)
  |> Vl.transform(filter: "datum.name == \"#{secondary_name}\"")
  |> Vl.encode_field(:x, "value",
    type: :quantitative,
    title: "Value"
  )
])

Granular Heat Map

This heat map is the same as the prior heat map. The only difference is the buckets are changed to 1%. This may give a better view of some detail but if the list of bins are different enough then any trends might not be obvious.

Vl.new(width: 1000, height: 1000)
|> Vl.data_from_values(attributes)
|> Vl.encode_field(:y, "attribute",
  type: :nominal,
  sort: attribute_names,
  title: "Attribute"
)
|> Vl.layers([
  Vl.new(title: title)
  |> Vl.mark(:rect)
  |> Vl.encode_field(:x, "value",
    type: :quantitative,
    title: "Value",
    axis: [label_angle: 90],
    bin: [minbins: 100, maxbins: 100]
  )
  |> Vl.encode(:color, aggregate: :count),
  Vl.new()
  |> Vl.mark(:point, color: :firebrick)
  |> Vl.transform(filter: "datum.name == \"#{focus_name}\"")
  |> Vl.encode_field(:x, "value",
    type: :quantitative,
    title: "Value"
  )
])