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(& &1["attribute"]) |> Enum.uniq()
Show the focused bin’s personality just for some extra info.
bin = Enum.find(bins, &(Path.basename(&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"
)
])