Compare commits
71 Commits
d9dee3af7b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f6b4a211b8 | |||
| 4c8364328c | |||
| 5192978917 | |||
| 4dfae60875 | |||
| b7723c91df | |||
| 3993f97de1 | |||
| f0a7fc1303 | |||
| 8ed22bebf0 | |||
| e364b20b1b | |||
| 305b9237c8 | |||
| 39c729b420 | |||
| d290a2c457 | |||
| 078682fd48 | |||
| 56ba989ad8 | |||
| 667bf76591 | |||
| 2768e4434a | |||
| caf0e44f3e | |||
| 11c8aade4c | |||
| 71cb4c8868 | |||
| 3c817b910a | |||
| be2f02560c | |||
| e0aa1b2476 | |||
| 90d54f85d9 | |||
| fdca3e37d1 | |||
| d2463fd1f3 | |||
| 854756c5dd | |||
| 50837411d3 | |||
| 9e7bd07ae0 | |||
| c09c4a0664 | |||
| eb5e1cf41d | |||
| 3e443617e9 | |||
| 648dacdd1a | |||
| 7a613ac191 | |||
| 1414439ceb | |||
| 036c0cdd5b | |||
| ccac1fec15 | |||
| bba1913d65 | |||
| d24dc4726b | |||
| 0c0c7f1230 | |||
| 1c06ffb8a8 | |||
| 8cfc39e381 | |||
| 6d89ed657c | |||
| 3e3017f31a | |||
| 37dcd67bdc | |||
| ffb31bf2b1 | |||
| 4c8f7fd11d | |||
| d07e604cbe | |||
| 75ec6a49e9 | |||
| c3de834f57 | |||
| a5cb6dc299 | |||
| 56af93b14e | |||
| 45f72cb836 | |||
| 656edf30a7 | |||
| aafb805194 | |||
| bd8148f3e2 | |||
| 4823e68700 | |||
| 4371701ca9 | |||
| 60bf927c67 | |||
| f79f0db862 | |||
| 7e30db3864 | |||
| 0987290dca | |||
| 4e9d98afe2 | |||
| ce825b75b1 | |||
| 0c72f96e16 | |||
| fb4fe9d487 | |||
| b5e58c4c25 | |||
| b611b296c3 | |||
| 16437b5deb | |||
| 33bcbece88 | |||
| 74a086c855 | |||
| 192748bb1d |
@@ -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
|
||||
+33
-7
@@ -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
|
||||
|
||||
+93
-7
@@ -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
|
||||
|
||||
+2
@@ -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
|
||||
@@ -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
|
||||
+15
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -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
|
||||
|
||||
+14
-8
@@ -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
|
||||
-42
@@ -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
|
||||
+23
@@ -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
|
||||
+3
-40
@@ -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 %>
|
||||
|
||||
+79
@@ -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
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
+3
-3
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
```
|
||||
Reference in New Issue
Block a user