Compare commits

...

71 Commits

Author SHA1 Message Date
alvaro f6b4a211b8 save display_mode to query string
ci / docker (push) Failing after 3m14s
2024-06-29 21:17:26 +02:00
alvaro 4c8364328c remove log
ci / docker (push) Failing after 3m43s
2024-06-29 18:51:27 +02:00
alvaro 5192978917 render champions as list or grid
ci / docker (push) Has been cancelled
2024-06-29 18:48:06 +02:00
alvaro 4dfae60875 create broadway processor for matches
ci / docker (push) Failing after 3m43s
2024-06-28 23:39:54 +02:00
alvaro b7723c91df add GenStage dep 2024-06-28 16:07:41 +02:00
alvaro 3993f97de1 add last_processed_at column to dim_player
ci / docker (push) Failing after 3m58s
2024-06-27 09:30:00 +02:00
alvaro f0a7fc1303 fix patch sorting order in web 2024-06-27 09:29:13 +02:00
alvaro 8ed22bebf0 fix match processing status
ci / docker (push) Failing after 4m14s
2024-06-24 13:57:19 +02:00
alvaro e364b20b1b remove old match repo
ci / docker (push) Failing after 3m58s
2024-06-23 12:59:05 +02:00
alvaro 305b9237c8 fix
ci / docker (push) Failing after 4m21s
2024-06-22 22:12:39 +02:00
alvaro 39c729b420 fix patch_number param on some calls, add prod url
ci / docker (push) Has been cancelled
2024-06-22 22:08:11 +02:00
alvaro d290a2c457 Update queues, fix patch_selector
ci / docker (push) Failing after 4m14s
2024-06-22 21:54:53 +02:00
alvaro 078682fd48 add patch_number and queue_id to dim_match
ci / docker (push) Failing after 3m56s
2024-06-22 20:00:02 +02:00
alvaro 56ba989ad8 asdf
ci / docker (push) Failing after 3m31s
2024-06-22 17:30:53 +02:00
alvaro 667bf76591 add processing info to match 2024-06-22 17:30:35 +02:00
alvaro 2768e4434a add fact processing status migrations
ci / docker (push) Failing after 3m32s
2024-06-22 13:05:18 +02:00
alvaro caf0e44f3e add title to win rate plot
ci / docker (push) Failing after 3m34s
2024-06-19 23:08:42 +02:00
alvaro 11c8aade4c Add method to get running matches processors
ci / docker (push) Has been cancelled
2024-06-18 20:19:57 +02:00
alvaro 71cb4c8868 fix typo
ci / docker (push) Waiting to run
2024-06-18 20:16:57 +02:00
alvaro 3c817b910a Add method to spawn task for processing all matches
ci / docker (push) Waiting to run
2024-06-18 20:15:57 +02:00
alvaro be2f02560c add patch number to summoner spells
ci / docker (push) Waiting to run
2024-06-18 20:07:39 +02:00
alvaro e0aa1b2476 update readme
ci / docker (push) Waiting to run
2024-06-18 19:38:14 +02:00
alvaro 90d54f85d9 Update docker-compose
ci / docker (push) Waiting to run
2024-06-18 01:26:48 +02:00
alvaro fdca3e37d1 .
ci / docker (push) Waiting to run
2024-06-18 00:12:55 +02:00
alvaro d2463fd1f3 . 2024-06-17 23:54:24 +02:00
alvaro 854756c5dd round images 2024-06-17 23:53:04 +02:00
alvaro 50837411d3 remove phx version
ci / docker (push) Waiting to run
2024-06-17 01:41:03 +02:00
alvaro 9e7bd07ae0 update patch selector width 2024-06-17 01:40:53 +02:00
alvaro c09c4a0664 remove win rate plot legendsmoother loading of champion detail
ci / docker (push) Waiting to run
2024-06-17 00:58:46 +02:00
alvaro eb5e1cf41d remove logs
ci / docker (push) Waiting to run
2024-06-17 00:37:58 +02:00
alvaro 3e443617e9 champion detail win rate chart 2024-06-17 00:37:33 +02:00
alvaro 648dacdd1a load champion detail asynchronously
ci / docker (push) Waiting to run
2024-06-16 19:32:23 +02:00
alvaro 7a613ac191 champion picked items by patch
ci / docker (push) Waiting to run
2024-06-16 16:04:38 +02:00
alvaro 1414439ceb Update "wins / total games" text size
ci / docker (push) Waiting to run
2024-06-16 15:05:23 +02:00
alvaro 036c0cdd5b add patch to query
ci / docker (push) Waiting to run
2024-06-16 14:52:03 +02:00
alvaro ccac1fec15 Filter by patch
ci / docker (push) Waiting to run
2024-06-16 12:53:26 +02:00
alvaro bba1913d65 fix 2024-06-15 23:25:38 +02:00
alvaro d24dc4726b Log errors on fact processors 2024-06-15 21:24:08 +02:00
alvaro 0c0c7f1230 Update facts processors error handling
ci / docker (push) Waiting to run
2024-06-15 19:06:12 +02:00
alvaro 1c06ffb8a8 fix ChampionPlayedGame Repo insert
ci / docker (push) Waiting to run
2024-06-15 17:40:46 +02:00
alvaro 8cfc39e381 add all facts
ci / docker (push) Has been cancelled
2024-06-14 05:00:19 +02:00
alvaro 6d89ed657c spawn tasks for processing matches
ci / docker (push) Waiting to run
2024-06-14 04:58:40 +02:00
alvaro 3e3017f31a request only rankeds
ci / docker (push) Waiting to run
2024-06-14 03:36:26 +02:00
alvaro 37dcd67bdc .
ci / docker (push) Waiting to run
2024-06-14 03:19:41 +02:00
alvaro ffb31bf2b1 aaaa
ci / docker (push) Waiting to run
2024-06-14 03:16:08 +02:00
alvaro 4c8f7fd11d fixfix
ci / docker (push) Waiting to run
2024-06-14 03:15:04 +02:00
alvaro d07e604cbe fix
ci / docker (push) Waiting to run
2024-06-14 03:10:38 +02:00
alvaro 75ec6a49e9 directly save ranked to ranked bucket 2024-06-14 03:01:06 +02:00
alvaro c3de834f57 update tooltip 2024-06-14 02:57:55 +02:00
alvaro a5cb6dc299 display boots section in champion detail 2024-06-14 02:56:34 +02:00
alvaro 56af93b14e pass team_position to get_win_rates_by_patch 2024-06-14 02:54:22 +02:00
alvaro 45f72cb836 remove inspect 2024-06-14 02:54:03 +02:00
alvaro 656edf30a7 fix summoner spells team position
ci / docker (push) Waiting to run
2024-06-14 01:18:36 +02:00
alvaro aafb805194 Display picked items by champion
ci / docker (push) Waiting to run
2024-06-14 01:08:23 +02:00
alvaro bd8148f3e2 update summoner_spells component
ci / docker (push) Waiting to run
2024-06-13 21:57:37 +02:00
alvaro 4823e68700 process champion picked item
ci / docker (push) Waiting to run
2024-06-13 21:03:59 +02:00
Álvaro Girona Arias 4371701ca9 wip process champion picked item
ci / docker (push) Waiting to run
2024-06-13 18:37:46 +02:00
Álvaro Girona Arias 60bf927c67 fix typo ChhampionPickedItem
ci / docker (push) Waiting to run
2024-06-13 15:41:03 +02:00
Álvaro Girona Arias f79f0db862 create behaviour for fact processor
ci / docker (push) Waiting to run
2024-06-13 15:36:57 +02:00
Álvaro Girona Arias 7e30db3864 Create fact for champion picked item 2024-06-13 15:36:05 +02:00
alvaro 0987290dca update pool size and concurrency default value for facts runner
ci / docker (push) Successful in 4m19s
2024-06-09 14:15:37 +02:00
alvaro 4e9d98afe2 Increase default concurrency for facts processing
ci / docker (push) Successful in 3m53s
2024-06-09 13:56:05 +02:00
alvaro ce825b75b1 Add patch number to champion played match
ci / docker (push) Has been cancelled
2024-06-09 13:53:43 +02:00
alvaro 0c72f96e16 fix default role filter
ci / docker (push) Failing after 4m27s
2024-06-09 13:24:24 +02:00
alvaro fb4fe9d487 update role filters overflow
ci / docker (push) Successful in 4m10s
2024-06-09 13:15:03 +02:00
alvaro b5e58c4c25 make champions the root route, change filters overflow to auto
ci / docker (push) Has been cancelled
2024-06-09 13:10:52 +02:00
alvaro b611b296c3 Improve responsiveness
ci / docker (push) Successful in 4m13s
2024-06-09 12:52:54 +02:00
alvaro 16437b5deb filter out champ roles with less than 100 games
ci / docker (push) Successful in 4m48s
2024-06-09 03:38:13 +02:00
alvaro 33bcbece88 fix role filtering
ci / docker (push) Successful in 4m7s
2024-06-09 03:25:31 +02:00
alvaro 74a086c855 update champion card
ci / docker (push) Successful in 4m17s
2024-06-09 02:48:45 +02:00
alvaro 192748bb1d create loader component and load champs asynchronously 2024-06-09 02:48:29 +02:00
71 changed files with 1624 additions and 518 deletions
+52 -1
View File
@@ -1 +1,52 @@
# LoLAnalytics.Umbrella
# LoLAnalytics
## Requirements
- PostgreSQL
- Elixir
- RabbitMQ
- MinIO
A `docker-compose` file is provided to run them locally.
## Environment variables
The followign environment variables are required:
```
export RIOT_API_KEY="{API-KEY}"
export EX_AWS_SECRET_KEY="{SECRET}"
export EX_AWS_ACCESS_KEY="{ACCESS}"
export EX_AWS_ENDPOINT="{HOST}"
export EX_AWS_PORT="{PORT}" # minio defaults to 9000
export DATABASE_URL="ecto://{USERNAME}:{PASSWORD}@{HOST}/lol-analytics"
export SECRET_KEY_BASE="SECRET-KEY"
```
## Running
```
mix deps.get
mix ecto.create && mix ecto.migrate
iex -S mix phx.server
```
## Queueing a player
```
Scrapper.Queues.PlayerQueue.enqueue_puuid("PUUID")
```
The scrapper will retrieve player ranked history, enqueue it's teammates, and store every match in minio.
## Processing
A `Task` to process the matches stored for a patch can be spawned by running:
```
LolAnalytics.MatchesProcessor.process_for_patch "14.12.594.4901"
```
## Web site
A web site is exposed at `localhost:4000`.
@@ -10,7 +10,10 @@ defmodule LoLAnalytics.Application do
children = [
LoLAnalytics.Repo,
{DNSCluster, query: Application.get_env(:lol_analytics, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: LoLAnalytics.PubSub}
{Phoenix.PubSub, name: LoLAnalytics.PubSub},
{Task.Supervisor, name: LoLAnalytics.TaskSupervisor},
# {LolAnalytics.MatchProcessor.MatchesBroadwayProcessor, []},
{LolAnalytics.MatchProcessor.MatchesProducer, []}
# Start a worker by calling: LoLAnalytics.Worker.start_link(arg)
# {LoLAnalytics.Worker, arg}
]
@@ -1,56 +0,0 @@
defmodule LolAnalytics.ChampionWinRate.ChampionWinRateRepo do
import Ecto.Query
alias LolAnalytics.ChampionWinRate.ChampionWinRateSchema
alias LoLAnalytics.Repo
@spec add_champion_win_rate(
champion_id :: String.t(),
patch :: String.t(),
position :: String.t(),
win? :: boolean
) :: {:ok, ChampionWinRateSchema.t()} | {:error, Ecto.Changeset.t()}
def add_champion_win_rate(champion_id, patch, position, win?) do
Repo.transaction(fn ->
champion_query =
from cwr in LolAnalytics.ChampionWinRate.ChampionWinRateSchema,
where: cwr.champion_id == ^champion_id and cwr.position == ^position,
lock: "FOR UPDATE"
champion_data = Repo.one(champion_query)
case champion_data do
nil ->
ChampionWinRateSchema.changeset(%ChampionWinRateSchema{}, %{
champion_id: champion_id,
patch: patch,
total_games: 1,
position: position,
total_wins: if(win?, do: 1, else: 0)
})
|> Repo.insert!()
_ ->
total_games = champion_data.total_games + 1
total_wins = champion_data.total_wins + if win?, do: 1, else: 0
ChampionWinRateSchema.changeset(champion_data, %{
total_games: total_games,
total_wins: total_wins
})
|> Repo.update!()
end
end)
end
def list_win_rates() do
Repo.all(ChampionWinRateSchema)
end
def get_champion_win_rate(champion_id, _patch) do
champion_query =
from cwr in LolAnalytics.ChampionWinRate.ChampionWinRateSchema,
where: cwr.champion_id == ^champion_id
Repo.one(champion_query)
end
end
@@ -1,20 +0,0 @@
defmodule LolAnalytics.ChampionWinRate.ChampionWinRateSchema do
use Ecto.Schema
import Ecto.Changeset
schema "champion_win_rate" do
field :champion_id, :integer
field :total_games, :integer
field :patch, :string
field :position, :string
field :total_wins, :integer
timestamps()
end
def changeset(%__MODULE__{} = champion_win_rate, attrs) do
champion_win_rate
|> cast(attrs, [:champion_id, :total_games, :patch, :total_wins, :position])
|> validate_required([:champion_id, :total_games, :patch, :total_wins, :position])
end
end
@@ -0,0 +1,22 @@
defmodule LolAnalytics.Dimensions.Item.ItemMetadata do
alias LolAnalytics.Dimensions.Item.ItemRepo
@items_data_url "https://ddragon.leagueoflegends.com/cdn/14.11.1/data/en_US/item.json"
def update_metadata() do
get_items()
|> Enum.each(&save_metadata/1)
end
defp get_items() do
with {:ok, resp} <- HTTPoison.get(@items_data_url),
%{"data" => data} <- Poison.decode!(resp.body) do
data
else
_ -> {:error, :get_items_error}
end
end
defp save_metadata({item_id, metadata}) do
ItemRepo.update(item_id, %{metadata: metadata})
end
end
@@ -2,13 +2,15 @@ defmodule LolAnalytics.Dimensions.Item.ItemRepo do
alias LolAnalytics.Dimensions.Item.ItemSchema
alias LoLAnalytics.Repo
def get_or_create(item_id) do
item = Repo.get(ItemSchema, item_id: item_id)
import Ecto.Query
case item do
def get_or_create(item_id) do
query = from i in ItemSchema, where: i.item_id == ^item_id
case Repo.one(query) do
nil ->
item_changeset = ItemSchema.changeset(%ItemSchema{}, %{item_id: item_id})
Repo.insert(item_changeset)
Repo.insert!(item_changeset)
item ->
item
@@ -18,4 +20,11 @@ defmodule LolAnalytics.Dimensions.Item.ItemRepo do
def list_items() do
Repo.all(ItemSchema)
end
@spec update(item_id :: String.t(), attrs :: map()) :: any()
def update(item_id, attrs) do
get_or_create(item_id)
|> ItemSchema.changeset(attrs)
|> Repo.update()
end
end
@@ -2,14 +2,17 @@ defmodule LolAnalytics.Dimensions.Item.ItemSchema do
use Ecto.Schema
import Ecto.Changeset
@args [:item_id, :metadata]
schema "dim_item" do
field :item_id, :integer
field :metadata, :map
timestamps()
end
def changeset(item = %__MODULE__{}, attrs \\ %{}) do
item
|> cast(attrs, [:item_id])
|> cast(attrs, @args)
|> validate_required([:item_id])
end
end
@@ -1,30 +1,85 @@
defmodule LolAnalytics.Dimensions.Match.MatchRepo do
alias LolAnalytics.Dimensions.Patch.PatchRepo
alias LolAnalytics.Dimensions.Match.MatchSchema
alias LoLAnalytics.Repo
import Ecto.Query
@spec get_or_create(String.t()) :: %MatchSchema{}
def get_or_create(match_id) do
@spec get_or_create(%{
:match_id => String.t(),
:queue_id => integer(),
:patch_number => String.t()
}) :: %MatchSchema{}
def get_or_create(%{match_id: match_id, queue_id: queue_id, patch_number: patch_number}) do
_patch = PatchRepo.get_or_create(patch_number)
query = from m in MatchSchema, where: m.match_id == ^match_id
match = Repo.one(query)
case match do
nil ->
match_changeset =
MatchSchema.changeset(
%MatchSchema{},
%{match_id: match_id}
)
Repo.insert(match_changeset)
%MatchSchema{}
|> MatchSchema.changeset(%{
match_id: match_id,
patch_number: patch_number,
queue_id: queue_id,
fact_champion_played_game_status: 0,
fact_champion_picked_item_status: 0,
fact_champion_picked_summoner_spell_status: 0
})
|> Repo.insert!()
match ->
match
end
end
@spec get(String.t()) :: nil | %MatchSchema{}
def get(match_id) do
query = from m in MatchSchema, where: m.match_id == ^match_id
Repo.one(query)
end
@type update_attrs :: %{
optional(:fact_champion_played_game_status) => process_status(),
optional(:fact_champion_picked_item_status) => process_status(),
optional(:fact_champion_picked_summoner_spell_status) => process_status()
}
@spec update(%MatchSchema{}, update_attrs()) :: %MatchSchema{}
def update(match, attrs) do
mapped_attrs =
attrs
|> Enum.map(fn {k, v} -> {k, process_status_atom_to_db(v)} end)
|> Map.new()
match
|> MatchSchema.changeset(mapped_attrs)
|> Repo.update!()
end
def list_matches() do
Repo.all(MatchSchema)
end
def list_unprocessed_matches(limit, queue \\ 420) do
query =
from m in MatchSchema,
where:
(m.fact_champion_picked_item_status == 0 or
m.fact_champion_picked_summoner_spell_status == 0 or
m.fact_champion_played_game_status == 0) and
m.queue_id == ^queue,
order_by: [desc: m.updated_at],
limit: ^limit
Repo.all(query)
end
@type process_status :: :not_processed | :processed | :error
defp process_status_atom_to_db(:not_processed), do: 0
defp process_status_atom_to_db(:enqueued), do: 1
defp process_status_atom_to_db(:processed), do: 2
defp process_status_atom_to_db(:error), do: 3
defp process_status_atom_to_db(:error_match_not_found), do: 4
defp process_status_atom_to_db(_), do: raise("Invalid processing status")
end
@@ -2,14 +2,28 @@ defmodule LolAnalytics.Dimensions.Match.MatchSchema do
use Ecto.Schema
import Ecto.Changeset
@casting_attrs [
:match_id,
:queue_id,
:patch_number,
:fact_champion_picked_item_status,
:fact_champion_picked_summoner_spell_status,
:fact_champion_played_game_status
]
schema "dim_match" do
field :match_id, :string
field :patch_number, :string
field :queue_id, :integer
field :fact_champion_picked_item_status, :integer
field :fact_champion_picked_summoner_spell_status, :integer
field :fact_champion_played_game_status, :integer
timestamps()
end
def changeset(match = %__MODULE__{}, attrs \\ %{}) do
match
|> cast(attrs, [:match_id])
|> cast(attrs, @casting_attrs)
|> validate_required([:match_id])
end
end
@@ -2,10 +2,12 @@ defmodule LolAnalytics.Dimensions.Patch.PatchRepo do
alias LolAnalytics.Dimensions.Patch.PatchSchema
alias LoLAnalytics.Repo
def get_or_create(patch_number) do
patch = Repo.get(PatchSchema, patch_number: patch_number)
import Ecto.Query
case patch do
def get_or_create(patch_number) do
query = from p in PatchSchema, where: p.patch_number == ^patch_number
case Repo.one(query) do
nil ->
patch_changeset =
PatchSchema.changeset(
@@ -11,5 +11,6 @@ defmodule LolAnalytics.Dimensions.Patch.PatchSchema do
patch
|> cast(attrs, [:patch_number])
|> validate_required([:patch_number])
|> unique_constraint([:patch_number])
end
end
@@ -6,15 +6,15 @@ defmodule LolAnalytics.Dimensions.Player.PlayerRepo do
def get_or_create(puuid) do
query = from p in PlayerSchema, where: p.puuid == ^puuid
player = Repo.one(query)
case player do
case Repo.one(query) do
nil ->
player_changeset =
PlayerSchema.changeset(
%PlayerSchema{},
%{puuid: puuid}
)
Repo.insert(player_changeset)
player ->
@@ -2,14 +2,21 @@ defmodule LolAnalytics.Dimensions.Player.PlayerSchema do
use Ecto.Schema
import Ecto.Changeset
@attrs [:puuid, :last_processed_at]
schema "dim_player" do
field :puuid, :string
field :last_processed_at, :utc_datetime,
default: DateTime.utc_now() |> DateTime.truncate(:second)
timestamps()
end
def changeset(player = %__MODULE__{}, attrs \\ %{}) do
player
|> cast(attrs, [:puuid])
|> cast(attrs, @attrs)
|> validate_required([:puuid])
|> unique_constraint([:puuid])
end
end
@@ -0,0 +1,87 @@
defmodule LolAnalytics.Facts.ChampionPickedItem.FactProcessor do
require Logger
alias LolAnalytics.Dimensions.Match.MatchSchema
alias LolAnalytics.Dimensions.Match.MatchRepo
alias LolAnalytics.Facts.ChampionPickedItem.Repo
@doc """
iex> LolAnalytics.Facts.ChampionPickedItem.FactProcessor.process_game_at_url("http://192.168.1.55:9000/ranked/14.3.558.106/EUW1_6803789466.json")
"""
@impl true
def process_game_at_url(url) do
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
HTTPoison.get(url),
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
process_game_data(decoded_match)
else
_ ->
Logger.error("Could not process data from #{url} for ChampionPickedItem")
{:error, "Could not process data from #{url}"}
end
end
@spec process_match(%MatchSchema{}) :: :ok | {:error, String.t()}
def process_match(match) do
match_url = "http://192.168.1.55:9000/ranked/#{match.patch_number}/#{match.match_id}.json"
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
HTTPoison.get(match_url),
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
process_game_data(decoded_match)
MatchRepo.update(match, %{fact_champion_picked_item_status: :processed})
:ok
else
_ ->
MatchRepo.update(match, fact_champion_picked_item_status: :error_match_not_found)
Logger.error("Could not process data from #{match_url} for ChampionPickedItem")
{:error, "Could not process data from #{match_url}"}
end
end
defp process_game_data(decoded_match) do
participants = decoded_match.info.participants
version = extract_game_version(decoded_match)
match =
MatchRepo.get_or_create(%{
match_id: decoded_match.metadata.matchId,
patch_number: decoded_match.info.gameVersion,
queue_id: decoded_match.info.queueId
})
Logger.info("Processing ChampionPickedItem for match #{decoded_match.metadata.matchId}")
participants
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
if participant.teamPosition != "" do
[:item0, :item1, :item2, :item3, :item4, :item5, :item6]
|> Enum.with_index()
|> Enum.each(fn {item_key, index} ->
item_key = Map.get(participant, item_key)
Repo.insert(%{
champion_id: participant.championId,
match_id: decoded_match.metadata.matchId,
is_win: participant.win,
item_id: item_key,
slot_number: index,
game_length_seconds: decoded_match.info.gameDuration,
queue_id: decoded_match.info.queueId,
puuid: participant.puuid,
team_position: participant.teamPosition,
patch_number: version
})
end)
end
end)
end
defp extract_game_version(game_data) do
game_data.info.gameVersion
|> String.split(".")
|> Enum.take(2)
|> Enum.join(".")
end
end
@@ -0,0 +1,89 @@
defmodule LolAnalytics.Facts.ChampionPickedItem.Repo do
import Ecto.Query
alias LolAnalytics.Dimensions.Patch.PatchRepo
alias LolAnalytics.Dimensions.Item.ItemSchema
alias LolAnalytics.Dimensions.Champion.ChampionSchema
alias LolAnalytics.Facts.ChampionPickedItem.Schema
alias LolAnalytics.Dimensions.Item.ItemRepo
alias LolAnalytics.Dimensions.Player.PlayerRepo
alias LolAnalytics.Dimensions.Champion.ChampionRepo
alias LolAnalytics.Dimensions.Match.MatchRepo
alias LoLAnalytics.Repo
@doc """
Inserts a new entry for the campion_picked_item fact.
Example:
iex> item_picked = %{champion_id: 90, item_id: 1, match_id: "123", patch_number: "14.9", puuid: "1223", slot_number: 1, queue_id: 420, team_position: "JUNGLE", slot_number: 1, is_win: true}
iex> LolAnalytics.Facts.ChampionPickedItem.Repo.insert(item_picked)
"""
@spec insert(%{
:champion_id => integer(),
:item_id => integer(),
:match_id => binary(),
:patch_number => String.t(),
:puuid => String.t(),
:is_win => boolean(),
:queue_id => integer(),
:team_position => String.t(),
:slot_number => integer()
}) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def insert(attrs) do
_champion = ChampionRepo.get_or_create(attrs.champion_id)
_player = PlayerRepo.get_or_create(attrs.puuid)
_patch = PatchRepo.get_or_create(attrs.patch_number)
_item_id = ItemRepo.get_or_create(attrs.item_id)
prev =
from(f in Schema,
where:
f.match_id == ^attrs.match_id and
f.puuid == ^attrs.puuid and
f.item_id == ^attrs.item_id and
f.slot_number == ^attrs.slot_number
)
|> Repo.one()
Schema.changeset(prev || %Schema{}, attrs)
|> Repo.insert_or_update()
end
@spec get_champion_picked_items(String.t(), String.t(), String.t()) :: list()
def get_champion_picked_items(champion_id, team_position, patch_number) do
query =
from f in Schema,
where:
f.champion_id == ^champion_id and
f.team_position == ^team_position and
f.item_id != 0 and f.patch_number == ^patch_number,
join: c in ChampionSchema,
on: c.champion_id == f.champion_id,
join: i in ItemSchema,
on: i.item_id == f.item_id,
select: %{
wins: fragment("count(CASE WHEN ? THEN 1 END)", f.is_win),
win_rate:
fragment(
"
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
)",
f.is_win
),
metadata: i.metadata,
item_id: i.item_id,
champion_id: c.champion_id,
team_position: f.team_position,
total_games: count("*")
},
group_by: [
i.metadata,
i.item_id,
c.champion_id,
f.team_position
]
Repo.all(query)
end
end
@@ -0,0 +1,34 @@
defmodule LolAnalytics.Facts.ChampionPickedItem.Schema do
use Ecto.Schema
import Ecto.Changeset
@casting_args [
:champion_id,
:item_id,
:match_id,
:is_win,
:queue_id,
:patch_number,
:team_position,
:puuid,
:slot_number
]
schema "fact_champion_picked_item" do
field :champion_id, :integer
field :item_id, :integer
field :match_id, :string
field :is_win, :boolean
field :queue_id, :integer
field :patch_number, :string
field :team_position, :string
field :puuid, :string
field :slot_number, :integer
end
def changeset(fact, attrs) do
fact
|> cast(attrs, @casting_args)
|> validate_required(@casting_args)
end
end
@@ -1,20 +1,44 @@
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.FactProcessor do
alias LolAnalytics.Facts.ChampionPickedSummonerSpell
require Logger
def process_game_at_url(url) do
data = HTTPoison.get!(url)
process_game_data(data.body)
alias LolAnalytics.Dimensions.Match.MatchSchema
alias LolAnalytics.Dimensions.Match.MatchRepo
alias LolAnalytics.Facts.ChampionPickedSummonerSpell
@spec process_match(%MatchSchema{}) :: :ok | {:error, String.t()}
def process_match(match) do
match_url = "http://192.168.1.55:9000/ranked/#{match.patch_number}/#{match.match_id}.json"
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
HTTPoison.get(match_url),
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
process_game_data(decoded_match)
MatchRepo.update(match, %{fact_champion_picked_summoner_spell_status: :processed})
:ok
else
_ ->
MatchRepo.update(match, fact_champion_picked_summoner_spell_status: :error_match_not_found)
Logger.error("Could not process data from #{match_url} for ChampionPickedItem")
{:error, "Could not process data from #{match_url}"}
end
end
defp process_game_data(data) do
decoded_match = Poison.decode!(data, as: %LoLAPI.Model.MatchResponse{})
defp process_game_data(decoded_match) do
participants = decoded_match.info.participants
version = extract_game_version(decoded_match)
match =
MatchRepo.get_or_create(%{
match_id: decoded_match.metadata.matchId,
patch_number: decoded_match.info.gameVersion,
queue_id: decoded_match.info.queueId
})
Logger.info("Processing ChampionPickedSummoner for match #{decoded_match.metadata.matchId}")
participants
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
if participant.teamPosition != "" do
attrs_spell_1 = %{
champion_id: participant.championId,
match_id: decoded_match.metadata.matchId,
@@ -26,6 +50,7 @@ alias LolAnalytics.Facts.ChampionPickedSummonerSpell
team_position: participant.teamPosition,
patch_number: version
}
attrs_spell_2 = %{
champion_id: participant.championId,
match_id: decoded_match.metadata.matchId,
@@ -37,6 +62,7 @@ alias LolAnalytics.Facts.ChampionPickedSummonerSpell
team_position: participant.teamPosition,
patch_number: version
}
ChampionPickedSummonerSpell.Repo.insert(attrs_spell_1)
ChampionPickedSummonerSpell.Repo.insert(attrs_spell_2)
end
@@ -1,6 +1,7 @@
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
import Ecto.Query
alias LolAnalytics.Dimensions.Patch.PatchRepo
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellSchema
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellRepo
alias LolAnalytics.Dimensions.Champion.ChampionSchema
@@ -13,19 +14,18 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
alias LoLAnalytics.Repo
@type insert_attrs :: %{
match_id: String.t(),
champion_id: String.t(),
puuid: String.t(),
summoner_spell_id: :integer
}
@spec insert(insert_attrs()) :: any()
@spec insert(%{
:champion_id => String.t(),
:match_id => String.t(),
:patch_number => String.t(),
:puuid => any(),
:summoner_spell_id => String.t()
}) :: any()
def insert(attrs) do
_match = MatchRepo.get_or_create(attrs.match_id)
_champion = ChampionRepo.get_or_create(attrs.champion_id)
_player = PlayerRepo.get_or_create(attrs.puuid)
_spell = SummonerSpellRepo.get_or_create(attrs.summoner_spell_id)
_patch = PatchRepo.get_or_create(attrs.patch_number)
prev =
from(f in Schema,
@@ -37,18 +37,18 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
)
|> Repo.one()
changeset = Schema.changeset(prev || %Schema{}, attrs)
IO.inspect(attrs)
Repo.insert_or_update(changeset)
|> IO.inspect()
Schema.changeset(prev || %Schema{}, attrs)
|> Repo.insert_or_update()
end
def get_champion_picked_summoners(championId, team_position \\ "MIDDLE") do
@spec get_champion_picked_summoners(String.t(), String.t(), String.t()) :: list()
def get_champion_picked_summoners(champion_id, team_position, patch_number) do
query =
from f in Schema,
where: f.champion_id == ^championId and f.team_position == ^team_position,
where:
f.champion_id == ^champion_id and
f.team_position == ^team_position and
f.patch_number == ^patch_number,
join: c in ChampionSchema,
on: c.champion_id == f.champion_id,
join: s in SummonerSpellSchema,
@@ -62,10 +62,8 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
)",
f.is_win
),
id: f.champion_id,
spell_id: f.summoner_spell_id,
metadata: s.metadata,
champion_name: c.name,
champion_id: c.champion_id,
team_position: f.team_position,
total_games: count("*")
@@ -74,16 +72,10 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
f.champion_id,
f.summoner_spell_id,
s.metadata,
c.image,
c.name,
c.champion_id,
f.team_position
]
Repo.all(query)
end
def list_facts() do
Repo.all(Schema)
end
end
@@ -7,6 +7,7 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Schema do
:champion_id,
:summoner_spell_id,
:match_id,
:patch_number,
:is_win,
:game_length_seconds,
:queue_id,
@@ -18,6 +19,7 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Schema do
field :champion_id, :integer
field :summoner_spell_id, :integer
field :match_id, :string
field :patch_number, :string
field :is_win, :boolean
field :game_length_seconds, :integer
field :queue_id, :integer
@@ -1,26 +1,56 @@
defmodule LolAnalytics.Facts.ChampionPlayedGame.Repo do
import Ecto.Query
alias LolAnalytics.Dimensions.Patch.PatchRepo
alias LolAnalytics.Dimensions.Champion.ChampionSchema
alias LolAnalytics.Dimensions.Player.PlayerRepo
alias LolAnalytics.Dimensions.Champion.ChampionRepo
alias LolAnalytics.Dimensions.Match.MatchRepo
alias LolAnalytics.Facts.ChampionPlayedGame.Schema
alias LoLAnalytics.Repo
def insert(attrs) do
_match = MatchRepo.get_or_create(attrs.match_id)
_champion = ChampionRepo.get_or_create(attrs.champion_id)
_player = PlayerRepo.get_or_create(attrs.puuid)
changeset = Schema.changeset(%Schema{}, attrs)
Repo.insert(changeset)
_patch = PatchRepo.get_or_create(attrs.patch_number)
prev =
from(f in Schema,
where:
f.match_id == ^attrs.match_id and
f.champion_id == ^attrs.champion_id and f.queue_id == ^attrs.queue_id
)
|> Repo.one()
changeset = Schema.changeset(prev || %Schema{}, attrs)
Repo.insert_or_update(changeset)
end
def list_played_matches() do
Repo.all(Schema)
end
def get_win_rates do
def get_win_rates(opts \\ []) do
team_position = Keyword.get(opts, :team_position)
patch = Keyword.get(opts, :patch_number, "14.12")
case {team_position, patch} do
{nil, nil} ->
get_win_rates()
{"all", patch} ->
get_win_rates_for_patch(patch)
{nil, patch} ->
get_win_rates_for_patch(patch)
{team_position, patch} ->
get_win_rates_for_patch_and_team_position(patch, team_position)
end
end
@spec champion_win_rates_by_patch(String.t(), String.t()) :: [map()]
def champion_win_rates_by_patch(champion_id, team_position) do
query =
from m in Schema,
join: c in ChampionSchema,
@@ -35,16 +65,72 @@ defmodule LolAnalytics.Facts.ChampionPlayedGame.Repo do
m.is_win
),
id: m.champion_id,
patch_number: m.patch_number,
name: c.name,
image: c.image,
team_position: m.team_position,
total_games: count("*")
},
group_by: [m.champion_id, c.image, c.name, m.team_position]
group_by: [m.champion_id, c.image, c.name, m.patch_number, m.team_position],
where: m.team_position == ^team_position and m.champion_id == ^champion_id,
having: count("*") > 50
Repo.all(query)
end
def get_win_rates_by_roles() do
defp get_win_rates_for_patch(patch_number) do
query =
from m in Schema,
join: c in ChampionSchema,
on: c.champion_id == m.champion_id,
select: %{
wins: fragment("count(CASE WHEN ? THEN 1 END)", m.is_win),
win_rate:
fragment(
"
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
)",
m.is_win
),
id: m.champion_id,
patch_number: m.patch_number,
name: c.name,
image: c.image,
team_position: m.team_position,
total_games: count("*")
},
group_by: [m.champion_id, c.image, c.name, m.team_position, m.patch_number],
having: m.patch_number == ^patch_number and count("*") > 100
Repo.all(query)
end
defp get_win_rates_for_patch_and_team_position(patch_number, team_position) do
query =
from m in Schema,
join: c in ChampionSchema,
on: c.champion_id == m.champion_id,
select: %{
wins: fragment("count(CASE WHEN ? THEN 1 END)", m.is_win),
win_rate:
fragment(
"
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
)",
m.is_win
),
id: m.champion_id,
patch_number: m.patch_number,
name: c.name,
image: c.image,
team_position: m.team_position,
total_games: count("*")
},
group_by: [m.champion_id, c.image, c.name, m.team_position, m.patch_number],
having:
m.team_position == ^team_position and m.patch_number == ^patch_number and
count("*") > 100
Repo.all(query)
end
end
@@ -6,6 +6,7 @@ defmodule LolAnalytics.Facts.ChampionPlayedGame.Schema do
@casting_attrs [
:champion_id,
:match_id,
:patch_number,
:is_win,
:game_length_seconds,
:team_position,
@@ -16,6 +17,7 @@ defmodule LolAnalytics.Facts.ChampionPlayedGame.Schema do
schema "fact_champion_played_game" do
field :champion_id, :integer
field :match_id, :string
field :patch_number, :string
field :is_win, :boolean
field :game_length_seconds, :integer
field :team_position, :string
@@ -1,14 +1,40 @@
defmodule LolAnalytics.Facts.ChampionPlayedGame.FactProcessor do
def process_game_at_url(path) do
data = HTTPoison.get!(path)
process_game_data(data.body)
require Logger
alias LolAnalytics.Dimensions.Match.MatchSchema
alias LolAnalytics.Dimensions.Match.MatchRepo
@spec process_match(%MatchSchema{}) :: :ok | {:error, String.t()}
def process_match(match) do
match_url = "http://192.168.1.55:9000/ranked/#{match.patch_number}/#{match.match_id}.json"
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
HTTPoison.get(match_url),
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
process_game_data(decoded_match)
MatchRepo.update(match, %{fact_champion_played_game_status: :processed})
:ok
else
_ ->
MatchRepo.update(match, fact_champion_played_game_status: :error_match_not_found)
Logger.error("Could not process data from #{match_url} for ChampionPickedItem")
{:error, "Could not process data from #{match_url}"}
end
end
def process_game_data(data) do
decoded_match = Poison.decode!(data, as: %LoLAPI.Model.MatchResponse{})
def process_game_data(decoded_match) do
participants = decoded_match.info.participants
version = extract_game_version(decoded_match)
match =
MatchRepo.get_or_create(%{
match_id: decoded_match.metadata.matchId,
patch_number: decoded_match.info.gameVersion,
queue_id: decoded_match.info.queueId
})
Logger.info("Processing ChampionPlayedMatch for #{decoded_match.metadata.matchId}")
participants
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
if participant.teamPosition != "" do
@@ -1,22 +1,24 @@
defmodule LolAnalytics.Facts.FactsRunner do
alias LolAnalytics.Facts
def analyze_all_matches do
Storage.MatchStorage.S3MatchStorage.stream_files("ranked")
|> Enum.each(fn %{key: path} ->
get_facts()
|> Enum.each(fn fact_runner ->
apply(fact_runner, ["http://192.168.1.55:9000/ranked/#{path}"])
end)
def analyze_match(match) do
get_facts()
|> Enum.each(fn fact_runner ->
apply(fact_runner, [match])
end)
end
def analyze_match() do
end
def get_facts() do
[
&Facts.ChampionPickedSummonerSpell.FactProcessor.process_game_at_url/1
&Facts.ChampionPickedSummonerSpell.FactProcessor.process_match/1,
&Facts.ChampionPlayedGame.FactProcessor.process_match/1,
&Facts.ChampionPickedItem.FactProcessor.process_match/1
]
end
def peach(enum, fun, concurrency \\ System.schedulers_online(), timeout \\ :infinity) do
Task.async_stream(enum, &fun.(&1), max_concurrency: concurrency, timeout: timeout)
|> Stream.each(fn {:ok, val} -> val end)
|> Enum.to_list()
end
end
@@ -1,35 +0,0 @@
defmodule LolAnalytics.Match.MatchRepo do
alias LolAnalytics.Match.MatchSchema
import Ecto.Query
def list_matches do
query = from m in MatchSchema, order_by: [desc: m.match_id]
LoLAnalytics.Repo.all(query)
end
def number_of_matches do
query = from m in MatchSchema, select: count(m.match_id)
LoLAnalytics.Repo.one(query)
end
@spec get_match(String.t()) :: %LolAnalytics.Match.MatchSchema{} | nil
def get_match(match_id) do
query = from m in MatchSchema, where: m.match_id == ^match_id
LoLAnalytics.Repo.one(query)
end
@spec insert_match(String.t()) :: %LolAnalytics.Match.MatchSchema{}
def insert_match(match_id) do
MatchSchema.changeset(%MatchSchema{}, %{:match_id => match_id, :processed => false})
|> LoLAnalytics.Repo.insert()
end
@spec update_match(%LolAnalytics.Match.MatchSchema{}, term()) ::
%LolAnalytics.Match.MatchSchema{}
def update_match(match, attrs) do
match = MatchSchema.changeset(match, attrs)
LoLAnalytics.Repo.update(match)
end
end
@@ -1,19 +0,0 @@
defmodule LolAnalytics.Match.MatchSchema do
use Ecto.Schema
import Ecto.Changeset
schema "match" do
field :match_id, :string
field :processed, :boolean, default: false
field :match_url, :string
timestamps()
end
def changeset(%__MODULE__{} = match, params \\ %{}) do
match
|> cast(params, [:match_id, :processed, :match_url])
|> validate_required([:match_id, :processed])
end
end
@@ -0,0 +1,34 @@
defmodule LolAnalytics.MatchProcessor.MatchesBroadwayProcessor do
alias LolAnalytics.Facts.FactsRunner
use Broadway
def start_link(opts) do
Broadway.start_link(__MODULE__,
name: __MODULE__,
processors: [default: []],
producer: [
module: {LolAnalytics.MatchProcessor.MatchesProducer, []},
rate_limiting: [
interval: 1000,
allowed_messages: 40
]
]
)
end
@impl Broadway
def handle_message(_processor, message, _context) do
message.data
# build_match_url(message.data.queue_id, message.data.patch_number, message.data.match_id)
|> FactsRunner.analyze_match()
message
end
defp build_match_url(queue, patch_id, match_id) do
"http://192.168.1.55:9000/#{queue_to_dir(queue)}/#{patch_id}/#{match_id}.json"
end
defp queue_to_dir(420), do: "ranked"
defp queue_to_dir(_), do: "ranked"
end
@@ -0,0 +1,33 @@
defmodule LolAnalytics.MatchProcessor.MatchesProducer do
use GenStage
@impl GenStage
def init(opts) do
{:producer, opts}
end
def start_link(opts) do
GenStage.start_link(__MODULE__, :ok)
end
@impl GenStage
def handle_demand(demand, state) do
matches = query_unprocessed_matches(demand)
{:noreply, matches, state}
end
defp query_unprocessed_matches(demand) when demand <= 0, do: []
defp query_unprocessed_matches(demand) do
LolAnalytics.Dimensions.Match.MatchRepo.list_unprocessed_matches(demand)
|> Enum.map(&broadway_transform/1)
end
defp broadway_transform(match) do
%Broadway.Message{
data: match,
acknowledger: Broadway.NoopAcknowledger.init()
}
end
end
+3 -1
View File
@@ -44,7 +44,9 @@ defmodule LoLAnalytics.MixProject do
{:lol_api, in_umbrella: true},
{:storage, in_umbrella: true},
{:httpoison, "~> 2.2"},
{:poison, "~> 5.0"}
{:poison, "~> 5.0"},
{:gen_stage, "~> 1.2.1"},
{:broadway, "~> 1.1"}
]
end
@@ -16,13 +16,6 @@ defmodule LoLAnalytics.Repo.Migrations.AnalyticsTables do
create index("dim_item", [:item_id], unique: true)
create table("dim_match") do
add :match_id, :string, primary_key: true, null: false
timestamps()
end
create index("dim_match", [:match_id], unique: true)
create table("dim_patch") do
add :patch_number, :string, primary_key: true, null: false
timestamps()
@@ -30,6 +23,15 @@ defmodule LoLAnalytics.Repo.Migrations.AnalyticsTables do
create index("dim_patch", [:patch_number], unique: true)
create table("dim_match") do
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
add :match_id, :string, primary_key: true, null: false
timestamps()
end
create index("dim_match", [:patch_number])
create index("dim_match", [:match_id], unique: true)
create table("dim_player") do
add :puuid, :string, primary_key: true, null: false
timestamps()
@@ -0,0 +1,28 @@
defmodule LoLAnalytics.Repo.Migrations.ChampionPickedItem do
use Ecto.Migration
def change do
create table("fact_champion_picked_item") do
add :champion_id, references("dim_champion", column: :champion_id, type: :integer)
add :item_id, references("dim_item", column: :item_id, type: :integer)
add :slot_number, :integer
add :match_id, references("dim_match", column: :match_id, type: :string)
add :is_win, :boolean
add :game_length_seconds, :integer
add :queue_id, :integer
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
add :team_position, :string
add :puuid, references("dim_player", column: :puuid, type: :string)
end
create index("fact_champion_picked_item", [:champion_id])
create index("fact_champion_picked_item", [:puuid, :match_id, :slot_number, :item_id], unique: true)
create index("fact_champion_picked_item", [
:item_id,
:queue_id,
:is_win,
:patch_number,
:team_position
])
end
end
@@ -0,0 +1,9 @@
defmodule LoLAnalytics.Repo.Migrations.ItemMetadata do
use Ecto.Migration
def change do
alter table("dim_item") do
add :metadata, :map
end
end
end
@@ -0,0 +1,11 @@
defmodule LoLAnalytics.Repo.Migrations.ItemMetadataIndex do
use Ecto.Migration
def up do
execute("CREATE INDEX dim_item_metadata ON dim_item USING GIN(metadata)")
end
def down do
execute("DROP INDEX dim_item_metadata")
end
end
@@ -0,0 +1,15 @@
defmodule LoLAnalytics.Repo.Migrations.DimMatchFactProcessingStatus do
use Ecto.Migration
def change do
alter table("dim_match") do
add :fact_champion_played_game_status, :integer, default: 0
add :fact_champion_picked_item_status, :integer, default: 0
add :fact_champion_picked_summoner_spell_status, :integer, default: 0
end
create index("dim_match", [:fact_champion_played_game_status])
create index("dim_match", [:fact_champion_picked_item_status])
create index("dim_match", [:fact_champion_picked_summoner_spell_status])
end
end
@@ -0,0 +1,9 @@
defmodule LoLAnalytics.Repo.Migrations.DimMatchQueueId do
use Ecto.Migration
def change do
alter table "dim_match" do
add :queue_id, :integer
end
end
end
@@ -0,0 +1,9 @@
defmodule LoLAnalytics.Repo.Migrations.DimPlayerLastProcessed do
use Ecto.Migration
def change do
alter table "dim_player" do
add :last_processed_at, :utc_datetime
end
end
end
+13
View File
@@ -3,3 +3,16 @@
@import "tailwindcss/utilities";
/* This file is for your main application CSS */
.tooltip {
visibility: hidden;
position: absolute;
}
.has-tooltip:hover .tooltip {
visibility: visible;
z-index: 100;
border: 1px solid #333;
background-color: whitesmoke;
border-radius: 12px;
}
+8 -4
View File
@@ -18,18 +18,22 @@
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import topbar from "../vendor/topbar"
import { ChampionWinRate } from "./hooks/champion_win_rate_patch"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}
params: { _csrf_token: csrfToken },
hooks: {
ChampionWinRate,
}
})
// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
@@ -0,0 +1,50 @@
import Chart from "chart.js/auto"
const ChampionWinRate = {
mounted() {
this.handleEvent("win-rate", ({ winRates }) => {
this.sortedWinRates = winRates.sort((a, b) => {
let [p1Major, p1Minor] = a.patch_number.split(".").map(Number);
let [p2Major, p2Minor] = b.patch_number.split(".").map(Number);
if (p1Major > p2Major || (p1Major == p2Major && p1Minor > p2Minor)) return 1;
return -1
})
this.patches = this.sortedWinRates.map((winRate) => {
return winRate.patch_number
})
this.winRateValues = this.sortedWinRates.map((winRate) => winRate.win_rate)
// TODO: it breaks on liveview updates, should apply a better fix...
setInterval(() => {
const data = {
labels: this.patches,
datasets: [{
data: this.winRateValues,
fill: false,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
};
this.chart = new Chart(document.getElementById("win-rate"), {
type: 'line',
data: data,
options: {
plugins: {
legend: {
display: false
}
}
}
})
this.chart.canvas.parentNode.style.height = '250px';
this.chart.canvas.parentNode.style.width = '400px';
this.chart.labels.display = false;
this.chart.options.legend.display = false
this.chart.options.legend.display = false
}, 1000)
});
}
}
export { ChampionWinRate };
+28
View File
@@ -0,0 +1,28 @@
{
"name": "assets",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"chart.js": "^4.4.3"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
},
"node_modules/chart.js": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz",
"integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
}
}
}
@@ -0,0 +1,5 @@
{
"dependencies": {
"chart.js": "^4.4.3"
}
}
@@ -4,12 +4,19 @@ defmodule LolAnalyticsWeb.ChampionComponents.ChampionAvatar do
attr :id, :integer, required: true
attr :image, :string, required: true
attr :name, :string, required: true
attr :width, :integer, default: 100
attr :height, :integer, default: 100
def champion_avatar(assigns) do
~H"""
<style>
.champion-avatar {
width: 100px;
height: 100px;
}
</style>
<div class="flex flex-col w-40">
<img src={@image} alt="champion-icon" />
<p class="w-full text-center"><%= @name %></p>
<img src={@image} class="champion-avatar rounded-xl" alt="champion-icon" />
</div>
"""
end
@@ -1,5 +1,5 @@
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard.Props do
defstruct [:id, :win_rate, :image, :name, :team_position, :wins, :total_games]
defstruct [:id, :win_rate, :image, :name, :team_position, :patch_number, :wins, :total_games]
end
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard do
@@ -11,15 +11,21 @@ defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard do
def champion_card(assigns) do
~H"""
<.link patch={"/champions/#{@props.id}"}>
<.link patch={"/champions/#{@props.id}?team-position=#{@props.team_position}&patch=#{@props.patch_number}"}>
<div class="flex flex-col rounded-xl bg-clip-border overflow-hidden bg-gray-200">
<img src={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@props.image}"} />
<div class="py-2" />
<div class="flex mx-auto">
<img src={team_position_image(@props.team_position)} class="w-5 h-5" />
<h3><%= @props.name %></h3>
<div class="flex flex-col flex-col-reverse">
<div class="flex w-auto px-4 py-1 opacity-80 gap-2 absolute z-10 align-bottom bg-black">
<img src={team_position_image(@props.team_position)} class="w-5 h-5" />
<h3 class="text-white"><%= @props.name %></h3>
</div>
<img src={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@props.image}"} />
</div>
<div class="py-2" />
<div class="py-1" />
<div class="pl-2">
<h3>Win rate: <%= @props.win_rate %></h3>
<p class="text-xs">Wins: <%= @props.wins %> / <%= @props.total_games %></p>
</div>
<div class="py-1" />
</div>
</.link>
"""
@@ -0,0 +1,23 @@
defmodule LolAnalyticsWeb.ChampionComponents.Items do
use Phoenix.Component
def items(assigns) do
image = ""
~H"""
<div class="flex flex-wrap flex-wrap gap-4">
<%= for item <- assigns.items do %>
<div class="has-tooltip">
<div clas="flex flex-col gap-1 p-4">
<img class="rounded-xl" src={item.image} />
<%!-- <p><%= item.name %></p> --%>
<p><%= item.win_rate %>%</p>
<p class="text-xs"><%= item.wins %>/<%= item.total_games %></p>
</div>
<span class="tooltip -mt-8 py-2 px-4 rounded-xl"><%= item.name %></span>
</div>
<% end %>
</div>
"""
end
end
@@ -1,42 +0,0 @@
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell do
defstruct [:id, :name, :win_rate, :wins, :total_games, :image]
@type t :: %{
id: integer(),
win_rate: float(),
wins: integer(),
total_games: integer(),
image: String.t(),
name: String.t()
}
end
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell
defstruct spell: %SummonerSpell{}
@type t :: %{spell: SummonerSpell.t()}
end
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpell do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props
use Phoenix.Component
attr :spells, Props, default: %Props{}
def summoner_spells(assigns) do
~H"""
<div class="flex flex-wrap flex-wrap gap-4">
<%= for spell <- assigns.spells.summoner_spells do %>
<div clas="flex flex-col gap-1">
<img src={spell.spell.image} />
<p><%= spell.spell.name %></p>
<p><%= spell.spell.win_rate %>%</p>
<p><%= spell.spell.wins %>/<%= spell.spell.total_games %></p>
</div>
<% end %>
</div>
"""
end
end
@@ -0,0 +1,23 @@
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props
use Phoenix.Component
def summoner_spells(assigns) do
~H"""
<div class="flex flex-wrap flex-wrap gap-4">
<%= for spell <- assigns.spells do %>
<div class="has-tooltip">
<div clas="flex flex-col gap-1">
<img class="rounded-xl" src={spell.image} />
<p><%= spell.win_rate %>%</p>
<p class="text-xs"><%= spell.wins %>/<%= spell.total_games %></p>
</div>
<div class="tooltip -my-8 px-4 py-2 rounded-xl">
<p><%= spell.name %></p>
</div>
</div>
<% end %>
</div>
"""
end
end
@@ -302,9 +302,6 @@ defmodule LoLAnalyticsWeb.CoreComponents do
end
def input(%{type: "checkbox"} = assigns) do
IO.puts(">>>>")
IO.inspect(assigns)
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
@@ -4,9 +4,6 @@
<a href="/">
LoL Analytics
</a>
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
v<%= Application.spec(:phoenix, :vsn) %>
</p>
</div>
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
<.link patch="/champions">
@@ -0,0 +1,28 @@
defmodule LolAnalyticsWeb.Loader do
use Phoenix.Component
def loader(assigns) do
~H"""
<div role="status" class="flex flex-col mx-auto w-max justify-center items-center gap-4">
<span class="text-3xl">Loading</span>
<svg
aria-hidden="true"
class="w-16 h-16 text-gray-300 animate-spin dark:text-gray-300 fill-orange-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span class="sr-only">Loading...</span>
</div>
"""
end
end
@@ -0,0 +1,66 @@
defmodule LolAnalyticsWeb.PatchSelector do
use LoLAnalyticsWeb, :live_component
def mount(socket) do
patches =
LolAnalytics.Dimensions.Patch.PatchRepo.list_patches()
|> Enum.map(fn patch ->
%{patch_number: String.split(patch.patch_number, ".") |> Enum.take(2) |> Enum.join(".")}
end)
|> MapSet.new()
|> Enum.to_list()
|> Enum.sort(fn %{patch_number: p1}, %{patch_number: p2} ->
[major_1, minor_1] = String.split(p1, ".") |> Enum.map(&String.to_integer/1)
[major_2, minor_2] = String.split(p2, ".") |> Enum.map(&String.to_integer/1)
major_1 > major_2 || (major_1 == major_2 && minor_1 > minor_2)
end)
patch_numbers = Enum.map(patches, & &1.patch_number)
[last_patch | _] = patch_numbers
send(self(), %{patch: last_patch})
socket =
assign(socket, :patch_numbers, patch_numbers)
|> assign(:patch_form, to_form(%{"selected_patch" => last_patch}))
{:ok, socket}
end
@impl true
def handle_event("selected_patch", %{"patch" => patch} = unsigned_params, socket) do
send(self(), %{patch: patch})
{:noreply, assign(socket, :selected_patch, patch)}
end
def render(assigns) do
~H"""
<div phx-feedback-for={@id}>
<style>
.patch-selector {
width: 100px;
}
</style>
<.form for={@patch_form} phx-change="validate" phx-target={@myself} phx-submit="save">
<div class="flex gap-4">
<p class="my-auto">Patch</p>
<select
phx-change="selected_patch"
id="patch"
name="patch"
class="patch-selector cursor-pointer block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
>
<%= for patch <- @patch_numbers do %>
<option key={patch} phx-click="select-patch" name={patch} value={patch}>
<%= patch %>
</option>
<% end %>
</select>
</div>
</.form>
</div>
"""
end
end
@@ -17,67 +17,30 @@ defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionFilters do
def mount(socket) do
socket =
assign(socket, :roles, @roles_definition)
|> assign(:form, build_roles_form())
{:ok, socket}
end
defp build_roles() do
@roles_definition
|> Enum.reduce(%{}, fn role, acc ->
Map.merge(acc, %{"#{role.value}" => false})
end)
|> Map.merge(%{"all" => true})
end
defp build_roles_form() do
@roles_definition
|> Enum.reduce(%{}, fn role, acc ->
Map.merge(acc, %{"#{role.value}" => false})
end)
|> Map.merge(%{"all" => true})
|> to_form
end
def handle_event("filter", unsigned_params, socket) do
# updated_form =
# unsigned_params
# |> Enum.map(fn {key, val} -> {key, key == unsigned_params["_target"]} end)
IO.inspect(unsigned_params)
# # assign()
# {:noreply, socket |> assign(:form, to_form(updated_form))}
end
attr :selectedrole, :string, required: true
attr :roles, :list, default: []
def render(assigns) do
IO.puts(">>>>1")
# IO.inspect(assigns)
IO.inspect(assigns.selectedrole)
IO.inspect(">>>>>2")
selected_class =
"px-8 py-2 flex flex-row gap-2 align-middle rounded-md border-gray-200 border border-sky-500"
"px-8 py-2 flex flex-row gap-2 align-middle rounded-full border-gray-200 border border-orange-600 cursor-pointer"
~H"""
<div>
<div class="flex flex-row justify-between p-10">
<div class="flex flex-row overflow-x-scroll sm:overflow-x-auto justify-between">
<%= for role <- @roles do %>
<%!-- <%= IO.inspect(role) %> --%>
<%= if (assigns.selectedrole == role.value) do %>
<div phx-click="filter" phx-value-role={role.title} class={selected_class}>
<%!-- <.input enabled={false} type="checkbox" field={@form[role.value]} /> --%>
<p><%= role.title %></p>
</div>
<% else %>
<div
phx-click="filter"
phx-value-role={role.value}
class="px-8 py-2 flex flex-row gap-2 align-middle"
class="px-8 py-2 flex flex-row gap-2 align-middle cursor-pointer"
>
<%!-- <.input enabled={false} type="checkbox" field={@form[role.value]} /> --%>
<p><%= role.title %></p>
</div>
<% end %>
@@ -0,0 +1,79 @@
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionsList do
use Phoenix.Component
attr :champion, :map
defp render_champion(assigns) do
detail_url =
"/champions/#{assigns.champion.id}?team-position=#{assigns.champion.team_position}&patch=#{assigns.champion.patch_number}"
~H"""
<tr>
<td>
<.link patch={detail_url}>
<div class="flex cursor-pointer items-center gap-2">
<img
class="champion_image rounded-md"
src={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{assigns.champion.image}"}
/>
<%= @champion.name %>
</div>
</.link>
</td>
<td><%= @champion.win_rate %>%</td>
<td><%= @champion.total_games %></td>
<td>
<img src={team_position_image(@champion.team_position)} class="w-5 h-5" />
</td>
</tr>
"""
end
attr :champions, :list
attr :patch_number, :integer
attr :position, :string
def champions_list(assigns) do
~H"""
<style>
.champion_image {
width: 50px;
}
</style>
<table class="table w-full">
<thead>
<tr>
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
Champion
</th>
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
Win rate
</th>
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
Total games
</th>
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
Position
</th>
</tr>
</thead>
<tbody>
<%= for champion <- assigns.champions do %>
<div class="cursor-pointer">
<.link patch={"/champions/#{champion.id}?team-position=#{champion.team_position}&patch=#{champion.patch_number}"}>
<.render_champion champion={champion} ) />
</.link>
</div>
<% end %>
<!-- table rows and cells go here -->
</tbody>
</table>
"""
end
defp team_position_image("BOTTOM"), do: "/images/lanes/bot.png"
defp team_position_image("MIDDLE"), do: "/images/lanes/mid.png"
defp team_position_image("TOP"), do: "/images/lanes/top.png"
defp team_position_image("JUNGLE"), do: "/images/lanes/jungle.png"
defp team_position_image("UTILITY"), do: "/images/lanes/utility.png"
end
@@ -2,59 +2,174 @@ defmodule LoLAnalyticsWeb.ChampionLive.Index do
use LoLAnalyticsWeb, :live_view
import LolAnalyticsWeb.ChampionComponents.ChampionCard
import LolAnalyticsWeb.ChampionLive.Components.ChampionsList
import LolAnalyticsWeb.Loader
import Phoenix.VerifiedRoutes
alias LolAnalyticsWeb.ChampionLive.Mapper
alias LolAnalyticsWeb.ChampionLive.Components.ChampionFilters
alias LolAnalyticsWeb.PatchSelector
@behaviour LolAnalyticsWeb.ChampionFilters.EventHandler
@roles [
%{title: "All", value: "all"},
%{title: "Top", value: "TOP"},
%{title: "Jungle", value: "JUNGLE"},
%{title: "Mid", value: "MIDDLE"},
%{title: "Bot", value: "BOTTOM"},
%{title: "Support", value: "UTILITY"}
]
@impl true
def mount(_params, _session, socket) do
champs = LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates()
mapped =
champs
|> Mapper.map_champs()
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
def mount(params, _session, socket) do
role = params["role"] || "all"
patch = params["patch"] || "14.12"
display_mode = params["display_mode"] || "grid"
socket =
socket
|> stream(:champions, mapped)
|> assign(:selected_role, "all")
|> assign(:selected_role, role)
|> assign(:selected_patch, patch)
|> assign(:champions, %{status: :loading})
|> assign(:display_mode, display_mode)
|> load_champs(role, patch)
{:ok, socket}
end
def handle_params(params, _uri, socket) do
role = params["role"] || "all"
patch = params["patch"] || "14.10"
display_mode = params["display_mode"] || "grid"
{
:noreply,
assign(socket, selected_role: role)
|> assign(selected_patch: patch)
|> assign(display_mode: display_mode)
}
end
@impl true
def handle_event("filter", %{"role" => selected_role} = params, socket) do
champs =
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates()
|> filter_champs(selected_role)
query =
get_query_params(socket)
|> Map.merge(%{role: selected_role})
{:noreply,
{:reply, %{},
socket
|> stream(:champions, champs)
|> push_patch(to: ~p"/champions?#{query}")
|> assign(:champions, %{status: :loading})
|> load_champs(selected_role, socket.assigns.selected_patch)
|> assign(:selected_role, selected_role)}
end
def handle_event("filter_champs", params, socket) do
def handle_event("set-display-mode", %{"mode" => mode} = params, socket) do
query_params =
get_query_params(socket)
|> Map.merge(%{display_mode: mode})
{:noreply,
assign(socket, :display_mode, mode)
|> push_patch(to: ~p"/champions?#{query_params}")}
end
defp filter_champs(champs, selected_role) do
champs =
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates()
|> Enum.filter((&filter_role/1).(selected_role))
|> Mapper.map_champs()
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
def handle_info(%{patch: patch}, socket) do
selected_role = socket.assigns.selected_role
query_params =
get_query_params(socket)
|> Map.merge(%{patch: patch})
socket =
assign(socket, :champions, %{status: :loading})
|> assign(:selected_patch, patch)
|> push_patch(to: ~p"/champions?#{query_params}")
|> load_champs(selected_role, patch)
{:noreply, socket}
end
defp load_champs(socket, selected_role, "all") do
socket
|> start_async(
:get_champs,
fn ->
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates(team_position: selected_role)
|> Mapper.map_champs()
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
end
)
end
defp load_champs(socket, selected_role, patch) do
socket
|> start_async(
:get_champs,
fn ->
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates(
team_position: selected_role,
patch_number: patch
)
|> Mapper.map_champs()
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
end
)
end
def handle_async(:get_champs, {:ok, champs}, socket) do
{:noreply, assign(socket, :champions, %{status: :data, data: champs})}
end
def render_display_mode_selector_selector(assigns) do
~H"""
<div class="flex">
<div phx-click="set-display-mode" phx-value-mode="grid" class="cursor-pointer">
<.icon name={grid_icon(assigns.display_mode)} alt="table" />
</div>
<div phx-click="set-display-mode" phx-value-mode="list" class="cursor-pointer">
<.icon name={list_icon(assigns.display_mode)} alt="table" />
</div>
</div>
"""
end
defp grid_icon(selected_display_mode) do
case selected_display_mode do
"grid" -> "hero-squares-2x2-solid"
"list" -> "hero-squares-2x2"
end
end
def list_icon(selected_display_mode) do
case selected_display_mode do
"grid" -> "hero-table-cells"
"list" -> "hero-table-cells-solid"
end
end
def render_champions_list(assigns) do
case assigns.champions do
%{status: :loading} ->
~H"""
<.loader />
"""
%{status: :data, data: champions} ->
~H"""
<.champions_list champions={champions} />
"""
end
end
def render_champions_grid(%{:champions => champions_state} = assigns) do
case champions_state do
%{status: :loading} ->
~H"""
<.loader />
"""
%{status: :data, data: champions} ->
~H"""
<div id="champions" class="grid grid-cols-2 sm:grid-cols-4 gap-4">
<%= for champion <- champions do %>
<.champion_card props={champion} />
<% end %>
</div>
"""
end
end
defp filter_role(role) do
@@ -72,4 +187,12 @@ defmodule LoLAnalyticsWeb.ChampionLive.Index do
socket
|> assign(:page_title, "Listing Champions")
end
defp get_query_params(socket) do
%{
patch: socket.assigns.selected_patch,
role: socket.assigns.selected_role,
display_mode: socket.assigns.display_mode
}
end
end
@@ -1,20 +1,32 @@
<.header>
Champions
<p class="text-3xl">
Champions
</p>
</.header>
<h1>Filters</h1>
<div class="h-4" />
<div class="px-2">
<h1 class="text-l font-semibold">Filters</h1>
<div class="flex flex-col py-2 gap-2">
<p>Roles</p>
<.live_component module={ChampionFilters} id="role-filters" selectedrole={@selected_role || "all" } />
</div>
<div class="flex justify-between items-center">
<.live_component module={PatchSelector} id="patch-selector" initial_patch={@selected_patch} />
<.render_display_mode_selector_selector display_mode={@display_mode} />
</div>
<.live_component module={ChampionFilters} id="role-filters" selectedrole={@selected_role} />
<div class="h-4"></div>
<div id="champions" class="grid grid-cols-4 gap-4">
<%= for {_, champion} <- @streams.champions do %>
<.champion_card props={champion} />
<% end %>
</div>
<.modal :if={@live_action in [:new, :edit]} id="champion-modal" show on_cancel={JS.patch(~p"/champions")}>
<.live_component module={LoLAnalyticsWeb.ChampionLive.FormComponent} id={@champion.champion_id || :new}
title={@page_title} action={@live_action} champion={@champion} patch={~p"/champions"} />
</.modal>
<%= if @display_mode=="grid" do %>
<.render_champions_grid champions={@champions} />
<% else %>
<.render_champions_list id="champions-list" champions={@champions} />
<% end %>
</div>
@@ -1,8 +1,10 @@
defmodule LoLAnalyticsWeb.ChampionLive.Show do
use LoLAnalyticsWeb, :live_view
import LolAnalyticsWeb.ChampionComponents.SummonerSpell
import LolAnalyticsWeb.ChampionComponents.SummonerSpells
import LolAnalyticsWeb.ChampionComponents.ChampionAvatar
import LolAnalyticsWeb.ChampionComponents.Items
import LolAnalyticsWeb.Loader
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper
@@ -12,28 +14,154 @@ defmodule LoLAnalyticsWeb.ChampionLive.Show do
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
def handle_params(%{"id" => id, "team-position" => team_position, "patch" => patch}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:champion, load_champion_info(id) |> ShowMapper.map_champion())
|> assign(:summoner_spells, %{
summoner_spells: load_summoner_spells(id) |> ShowMapper.map_spells()
})}
|> assign(:champion, load_champion_info(id))
|> load_win_rates(id, team_position)
|> load_summoner_spells(id, team_position, patch)
|> load_items(id, team_position, patch)}
end
defp load_summoner_spells(champion_id) do
LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo.get_champion_picked_summoners(champion_id)
def load_win_rates(socket, champion_id, team_position) do
socket
|> start_async(:get_win_rates, fn ->
LolAnalytics.Facts.ChampionPlayedGame.Repo.champion_win_rates_by_patch(
champion_id,
team_position
)
|> Enum.sort(fn p1, p2 ->
[_, minor_1] = String.split(p1.patch_number, ".") |> Enum.map(&String.to_integer/1)
[_, minor_2] = String.split(p2.patch_number, ".") |> Enum.map(&String.to_integer/1)
p1 < p2 && minor_1 < minor_2
end)
end)
end
defp load_summoner_spells(socket, champion_id, team_position, patch_number) do
socket
|> assign(:summoner_spells, %{status: :loading})
|> start_async(:get_summoners, fn ->
LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo.get_champion_picked_summoners(
champion_id,
team_position,
patch_number
)
|> ShowMapper.map_spells()
end)
end
defp load_items(socket, champion_id, team_position, patch) do
socket
|> assign(:items, %{status: :loading})
|> assign(:boots, %{status: :loading})
|> start_async(
:get_items,
fn ->
items =
LolAnalytics.Facts.ChampionPickedItem.Repo.get_champion_picked_items(
champion_id,
team_position,
patch
)
popular_items = items |> ShowMapper.map_items() |> Enum.take(30)
boots = items |> ShowMapper.extract_boots()
%{boots: boots, popular: popular_items}
end
)
end
@impl true
@spec handle_async(:get_items | :get_summoners | :get_win_rates, {:ok, any()}, map()) ::
{:noreply, map()}
def handle_async(:get_win_rates, {:ok, result}, socket) do
{:noreply, push_event(socket, "win-rate", %{winRates: result})}
end
def handle_async(:get_items, {:ok, %{popular: popular, boots: boots}} = result, socket) do
socket =
socket
|> assign(:items, %{
status: :data,
data: %{
popular: popular,
boots: boots
}
})
{:noreply, socket}
end
def handle_async(:get_summoners, {:ok, summoner_spells}, socket) do
socket =
assign(socket, :summoner_spells, %{
status: :data,
data: summoner_spells
})
{:noreply, socket}
end
defp load_champion_info(champion_id) do
champion = LolAnalytics.Dimensions.Champion.ChampionRepo.get_or_create(champion_id)
IO.inspect(champion)
champion
LolAnalytics.Dimensions.Champion.ChampionRepo.get_or_create(champion_id)
|> ShowMapper.map_champion()
end
defp page_title(:show), do: "Show Champion"
defp page_title(:edit), do: "Edit Champion"
def render_items(assigns) do
case assigns.items do
%{status: :loading} ->
~H"""
<.loader />
"""
%{status: :data, data: data} ->
~H"""
<%= if Enum.count(data.boots) > 0 do %>
<h2 class="text-2xl">Items</h2>
<h2 class="text-xl">Boots</h2>
<div class="my-2" />
<.items items={data.boots} />
<div class="my-4" />
<% end %>
<h2 class="text-xl">Popular items</h2>
<div class="my-2" />
<.items items={data.popular} />
"""
end
end
def render_summoners(assigns) do
case assigns.summoner_pells do
%{status: :loading} ->
~H"""
<h2 class="text-2xl">Summoner spells</h2>
<div class="my-2" />
<.loader />
"""
%{status: :data, data: data} ->
~H"""
<h2 class="text-2xl">Summoner spells</h2>
<div class="my-2" />
<.summoner_spells spells={data} />
"""
end
end
end
@@ -1,15 +1,36 @@
<style>
.win-rate {
height: 250px;
}
.win-rate-container {
width: 100%;
height: 250px;
}
</style>
<.header>
Champion <%= @champion.id %>
<:actions></:actions>
<h1 class="text-4xl">
<%= @champion.name %>
</h1>
</.header>
<.champion_avatar id={@champion.id} name={@champion.name}
image={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@champion.image}"} />
<div class="my-6" />
<div class="flex flex-row gap-4">
<.champion_avatar class="w-20" id={@champion.id} name={@champion.name}
image={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@champion.image}"} />
<div>
<p class="text-lg">Win rate by patch</p>
<div class="win-rate-container" style="position: relative; height:250px; width:400px">
<canvas class="w-full win-rate" height="250" class="win-rate" id="win-rate" phx-hook="ChampionWinRate" />
</div>
</div>
</div>
<div class="my-4" />
<.render_summoners summoner_pells={@summoner_spells} />
<div class="my-4" />
<.summoner_spells spells={@summoner_spells} />
<.back navigate={~p"/champions"}>
Back to champions
</.back>
<.render_items items={@items} />
@@ -13,21 +13,48 @@ defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper do
def map_spells(items) do
items
|> Enum.map(&map_spell/1)
|> Enum.sort(&(&1.spell.total_games > &2.spell.total_games))
|> Enum.sort(&(&1.total_games > &2.total_games))
end
def map_spell(spell) do
image = spell.metadata["image"]["full"]
%LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props{
spell: %LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell{
id: spell.id,
win_rate: :erlang.float_to_binary(spell.win_rate, decimals: 2),
total_games: spell.total_games,
image: "https://ddragon.leagueoflegends.com/cdn/14.11.1/img/spell/#{image}",
name: spell.metadata["name"],
wins: spell.wins
}
%{
id: spell["id"],
win_rate: :erlang.float_to_binary(spell.win_rate, decimals: 2),
total_games: spell.total_games,
image: "https://ddragon.leagueoflegends.com/cdn/14.11.1/img/spell/#{image}",
name: spell.metadata["name"],
wins: spell.wins
}
end
def map_items(items) do
items
|> Enum.map(&map_item/1)
|> Enum.sort(&(&1.total_games > &2.total_games))
end
def map_item(item) do
image = item.metadata["image"]["full"]
%{
id: item["id"],
win_rate: :erlang.float_to_binary(item.win_rate, decimals: 2),
total_games: item.total_games,
image: "https://ddragon.leagueoflegends.com/cdn/14.11.1/img/item/#{image}",
name: item.metadata["name"],
wins: item.wins
}
end
def extract_boots(items) do
items
|> Enum.filter(fn item ->
tags = item.metadata["tags"]
tags |> Enum.any?(&String.equivalent?(&1, "Boots"))
end)
|> Enum.map(&map_item/1)
|> Enum.sort(&(&1.total_games > &2.total_games))
end
end
@@ -17,7 +17,7 @@ defmodule LoLAnalyticsWeb.Router do
scope "/", LoLAnalyticsWeb do
pipe_through :browser
get "/", PageController, :home
live "/", ChampionLive.Index, :index
live "/champions", ChampionLive.Index, :index
live "/champions/new", ChampionLive.Index, :new
live "/champions/:id/edit", ChampionLive.Index, :edit
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
defmodule LoLAPI.MatchApi do
require Logger
@match_base_endpoint "https://europe.api.riotgames.com/lol/match/v5/matches/%{matchid}"
@puuid_matches_base_endpoint "https://europe.api.riotgames.com/lol/match/v5/matches/by-puuid/%{puuid}/ids"
@puuid_matches_base_endpoint "https://europe.api.riotgames.com/lol/match/v5/matches/by-puuid/%{puuid}/ids?queue=420"
@doc """
Get match by id
+2 -2
View File
@@ -10,8 +10,8 @@ defmodule Scrapper.Application do
children = [
Scrapper.Queue.MatchQueue,
Scrapper.Queue.PlayerQueue,
{Scrapper.Processor.MatchProcessor, []},
{Scrapper.Processor.PlayerProcessor, []}
{Scrapper.Consumer.MatchConsumer, []},
{Scrapper.Consumer.PlayerConsumer, []}
# Starts a worker by calling: Scrapper.Worker.start_link(arg)
# {Scrapper.Worker, arg}
]
@@ -0,0 +1,83 @@
defmodule Scrapper.Consumer.MatchConsumer do
require Logger
use Broadway
def start_link(_opts) do
Broadway.start_link(
__MODULE__,
name: __MODULE__,
producer: [
module:
{BroadwayRabbitMQ.Producer,
queue: "match",
connection: [
username: "guest",
password: "guest",
host: "localhost"
],
on_failure: :reject_and_requeue,
qos: [
prefetch_count: 1
]},
concurrency: 1,
rate_limiting: [
interval: 300,
allowed_messages: 1
]
],
processors: [
default: [
concurrency: 1
]
]
)
end
@impl true
def handle_message(_, message = %Broadway.Message{}, _) do
match_id = message.data
resp = LoLAPI.MatchApi.get_match_by_id(match_id)
process_resp(resp, match_id)
message
end
def process_resp({:ok, raw_match}, match_id) do
Task.start_link(fn ->
decoded_match = Poison.decode!(raw_match, as: %LoLAPI.Model.MatchResponse{})
match_url =
case decoded_match.info.queueId do
420 ->
Logger.info("#{match_id} #{decoded_match.info.gameVersion}")
Storage.MatchStorage.S3MatchStorage.store_match(
match_id,
raw_match,
"ranked",
"#{decoded_match.info.gameVersion}"
)
LolAnalytics.Dimensions.Match.MatchRepo.get_or_create(%{
match_id: decoded_match.metadata.matchId,
patch_number: decoded_match.info.gameVersion,
queue_id: 420
})
_queue_id ->
Storage.MatchStorage.S3MatchStorage.store_match(match_id, raw_match, "matches")
end
decoded_match.metadata.participants
# |> Enum.shuffle()
# |> Enum.take(2)
|> Enum.each(fn participant_puuid ->
Scrapper.Queue.PlayerQueue.enqueue_puuid(participant_puuid)
end)
end)
end
def process_resp({:err, _code}, _match_id) do
end
end
@@ -1,4 +1,4 @@
defmodule Scrapper.Processor.PlayerProcessor do
defmodule Scrapper.Consumer.PlayerConsumer do
use Broadway
def start_link(_opts) do
@@ -20,7 +20,7 @@ defmodule Scrapper.Processor.PlayerProcessor do
]},
concurrency: 1,
rate_limiting: [
interval: 1000 * 10,
interval: 6700,
allowed_messages: 1
]
],
@@ -52,7 +52,7 @@ defmodule Scrapper.Processor.PlayerProcessor do
{
matches
|> Enum.each(fn match_id ->
Scrapper.Queue.MatchQueue.queue_match(match_id)
Scrapper.Queue.MatchQueue.enqueue_match(match_id)
end)
}
@@ -1,71 +0,0 @@
defmodule Scrapper.Processor.MatchProcessor do
use Broadway
def start_link(_opts) do
Broadway.start_link(
__MODULE__,
name: __MODULE__,
producer: [
module:
{BroadwayRabbitMQ.Producer,
queue: "match",
connection: [
username: "guest",
password: "guest",
host: "localhost"
],
on_failure: :reject_and_requeue,
qos: [
prefetch_count: 1
]},
concurrency: 1,
rate_limiting: [
interval: 333 * 1,
allowed_messages: 1
]
],
processors: [
default: [
concurrency: 1
]
]
)
end
@impl true
def handle_message(_, message = %Broadway.Message{}, _) do
match_id = message.data
resp = LoLAPI.MatchApi.get_match_by_id(match_id)
process_resp(resp, match_id)
message
end
def process_resp({:ok, raw_match}, match_id) do
decoded_match = Poison.decode!(raw_match, as: %LoLAPI.Model.MatchResponse{})
match_url = Storage.MatchStorage.S3MatchStorage.store_match(match_id, raw_match, "matches")
match = LolAnalytics.Match.MatchRepo.get_match(match_id)
case match do
nil ->
LolAnalytics.Match.MatchRepo.insert_match(match_id)
_ ->
LolAnalytics.Match.MatchRepo.update_match(match, %{
:processed => true,
:match_url => match_url
})
end
decoded_match.metadata.participants
|> Enum.shuffle()
|> Enum.take(2)
|> Enum.each(fn participant_puuid ->
Scrapper.Queue.PlayerQueue.queue_puuid(participant_puuid)
end)
end
def process_resp({:err, _code}, _match_id) do
end
end
@@ -14,16 +14,16 @@ defmodule Scrapper.Queue.MatchQueue do
{:ok, %{:channel => channel, :connection => connection}}
end
@spec queue_match(String.t()) :: any()
def queue_match(match_id) do
LolAnalytics.Match.MatchRepo.get_match(match_id)
@spec enqueue_match(String.t()) :: any()
def enqueue_match(match_id) do
LolAnalytics.Dimensions.Match.MatchRepo.get(match_id)
|> case do
nil -> GenServer.call(__MODULE__, {:queue_match, match_id})
nil -> GenServer.call(__MODULE__, {:enqueue_match, match_id})
_match -> :already_processed
end
end
def handle_call({:queue_match, match_id}, _from, %{:channel => channel} = state) do
def handle_call({:enqueue_match, match_id}, _from, %{:channel => channel} = state) do
AMQP.Basic.publish(channel, "", "match", match_id)
{:reply, nil, state}
end
@@ -12,31 +12,36 @@ defmodule Scrapper.Queue.PlayerQueue do
def init(_opts) do
{:ok, connection} = AMQP.Connection.open()
{:ok, channel} = AMQP.Channel.open(connection)
AMQP.Queue.declare(channel, "player", durable: true)
AMQP.Queue.declare(channel, "player",
durable: true,
arguments: [{"x-max-length", :long, 1000}]
)
{:ok, {channel, connection}}
end
@spec queue_puuid(String.t()) :: nil
def queue_puuid(puuid) do
@spec enqueue_puuid(String.t()) :: nil
def enqueue_puuid(puuid) do
case LolAnalytics.Player.PlayerRepo.get_player(puuid) do
nil -> GenServer.call(__MODULE__, {:queue_player, puuid})
player_entry -> queue_if_not_updated?(player_entry)
nil -> GenServer.call(__MODULE__, {:enqueue_player, puuid})
player_entry -> enqueue_if_not_updated?(player_entry)
end
end
@spec queue_if_not_updated?(%LolAnalytics.Player.PlayerSchema{}) :: nil
defp queue_if_not_updated?(player = %LolAnalytics.Player.PlayerSchema{}) do
@spec enqueue_if_not_updated?(%LolAnalytics.Player.PlayerSchema{}) :: nil
defp enqueue_if_not_updated?(player = %LolAnalytics.Player.PlayerSchema{}) do
now = DateTime.utc_now() |> DateTime.truncate(:second)
case player.last_processed_at do
nil ->
GenServer.call(__MODULE__, {:queue_player, player.puuid})
GenServer.call(__MODULE__, {:enqueue_player, player.puuid})
processed_at ->
diff_in_hours = DateTime.diff(now, processed_at, :hour)
if diff_in_hours > @hours_to_update_player do
GenServer.call(__MODULE__, {:queue_player, player.puuid})
GenServer.call(__MODULE__, {:enqueue_player, player.puuid})
else
Logger.info(
"Player #{player.puuid} already processed at #{player.last_processed_at}, diff was #{diff_in_hours}"
@@ -45,7 +50,7 @@ defmodule Scrapper.Queue.PlayerQueue do
end
end
def handle_call({:queue_player, puuid}, _from, {channel, _} = state) do
def handle_call({:enqueue_player, puuid}, _from, {channel, _} = state) do
LolAnalytics.Player.PlayerRepo.insert_player(puuid)
AMQP.Basic.publish(channel, "", "player", puuid)
{:reply, nil, state}
@@ -7,8 +7,10 @@ defmodule Storage.MatchStorage.S3MatchStorage do
end
@impl true
def stream_files(path) do
ExAws.S3.list_objects_v2(path)
def stream_files(path, opts \\ []) do
patch = Keyword.get(opts, :patch, "")
ExAws.S3.list_objects_v2(path, prefix: patch)
|> ExAws.stream!()
end
+1 -1
View File
@@ -8,7 +8,7 @@ import Config
config :lol_analytics_web, LoLAnalyticsWeb.Endpoint,
server: true,
http: [ip: {0, 0, 0, 0}, port: 4000],
url: [host: "localhost", port: 80],
url: [host: "lol-analytics.kaizer.cloud", port: 443],
cache_static_manifest: "priv/static/cache_manifest.json"
# Do not print debug messages in production
+1 -1
View File
@@ -11,7 +11,7 @@ config :lol_analytics, LoLAnalytics.Repo,
hostname: "localhost",
database: "lol_analytics_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: System.schedulers_online() * 2
pool_size: System.schedulers_online() * 4
# We don't run a server during test. If one is required,
# you can enable the server option below.
+18 -2
View File
@@ -26,11 +26,27 @@ services:
- ~/.docker-conf/rabbitmq/data/:/var/lib/rabbitmq/
- ~/.docker-conf/rabbitmq/log/:/var/log/rabbitmq
networks:
- rabbitmq_go_net
- rabbitmq_net
minio:
image: bitnami/minio:latest
ports:
- '9000:9000'
- '9001:9001'
networks:
- minionetwork
volumes:
- 'minio_data:/data'
environment:
- MINIO_ROOT_USER=your_username
- MINIO_ROOT_PASSWORD=your_pasword
- MINIO_DEFAULT_BUCKETS=your_bucket_name
volumes:
postgres-data:
minio_data:
networks:
rabbitmq_go_net:
rabbitmq_net:
driver: bridge
minionetwork:
driver: bridge
+3 -3
View File
@@ -2,7 +2,7 @@
"amqp": {:hex, :amqp, "3.3.0", "056d9f4bac96c3ab5a904b321e70e78b91ba594766a1fc2f32afd9c016d9f43b", [:mix], [{:amqp_client, "~> 3.9", [hex: :amqp_client, repo: "hexpm", optional: false]}], "hexpm", "8d3ae139d2646c630d674a1b8d68c7f85134f9e8b2a1c3dd5621616994b10a8b"},
"amqp_client": {:hex, :amqp_client, "3.12.13", "6fc6a7c681e53fed4cbd3f5bcdda342a2b46976345e460ef85414c63698cfe70", [:make, :rebar3], [{:credentials_obfuscation, "3.4.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:rabbit_common, "3.12.13", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm", "76f41bff0792193f00e0062128db51eb68bcee0eb8236139247a7d1866438d03"},
"bandit": {:hex, :bandit, "1.5.0", "3bc864a0da7f013ad3713a7f550c6a6ec0e19b8d8715ec678256a0dc197d5539", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "92d18d9a7228a597e0d4661ef69a874ea82d63ff49c7d801a5c68cb18ebbbd72"},
"broadway": {:hex, :broadway, "1.0.7", "7808f9e3eb6f53ca6d060f0f9d61012dd8feb0d7a82e62d087dd517b9b66fa53", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.7 or ~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e76cfb0a7d64176c387b8b1ddbfb023e2ee8a63e92f43664d78e6d5d0b1177c6"},
"broadway": {:hex, :broadway, "1.1.0", "8ed3aea01fd6f5640b3e1515b90eca51c4fc1fac15fb954cdcf75dc054ae719c", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.7 or ~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25e315ef1afe823129485d981dcc6d9b221cea30e625fd5439e9b05f44fb60e4"},
"broadway_rabbitmq": {:hex, :broadway_rabbitmq, "0.8.1", "6d68a480b2e49694e4f3836dcbbf8e621bb97b34e84787a2093d5cc3078a4d87", [:mix], [{:amqp, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :amqp, repo: "hexpm", optional: false]}, {:broadway, "~> 1.0", [hex: :broadway, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5 or ~> 0.4.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eebe9b03c9673cbda790430389c47e4ca867f9904418cff1b71a74a59c2a986"},
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
@@ -21,7 +21,7 @@
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
@@ -29,7 +29,7 @@
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.5.1", "6fdbc334ea53620e71655664df6f33f670747b3a7a6c4041cdda3e2c32df6257", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ebe43aa580db129e54408e719fb9659b7f9e0d52b965c5be26cdca416ecead28"},
-22
View File
@@ -1,22 +0,0 @@
```
SELECT
(cast(count(CASE WHEN is_win THEN 1 END) as float) / cast(count(*) as float)) * 100.0 as win_rate,
count(CASE WHEN is_win THEN 1 END) as games_won,
count(*) as total_games,
champion_id
FROM fact_champion_played_game
GROUP BY champion_id
ORDER BY win_rate desc;
```
```
SELECT
(cast(count(CASE WHEN is_win THEN 1 END) as float) / cast(count(*) as float)) * 100.0 as win_rate,
count(CASE WHEN is_win THEN 1 END) as games_won,
count(*) as total_games,
champion_id,
team_position
FROM fact_champion_played_game
GROUP BY champion_id, team_position
ORDER BY win_rate desc;
```