add graph to range page
This commit is contained in:
parent
1f017ced4a
commit
ba8d7988b3
@ -10,6 +10,7 @@
|
||||
- Make container show page a bit more compact
|
||||
- Make container show page filter used-up ammo
|
||||
- Forgot to add the logo as the favicon whoops
|
||||
- Add graph to range page
|
||||
- Update project dependencies
|
||||
|
||||
# v0.5.4
|
||||
|
@ -26,6 +26,7 @@ import { Socket } from 'phoenix'
|
||||
import { LiveSocket } from 'phoenix_live_view'
|
||||
import topbar from '../vendor/topbar'
|
||||
import MaintainAttrs from './maintain_attrs'
|
||||
import ShotLogChart from './shot_log_chart'
|
||||
import Alpine from 'alpinejs'
|
||||
|
||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
|
||||
@ -36,7 +37,7 @@ const liveSocket = new LiveSocket('/live', Socket, {
|
||||
}
|
||||
},
|
||||
params: { _csrf_token: csrfToken },
|
||||
hooks: { MaintainAttrs }
|
||||
hooks: { MaintainAttrs, ShotLogChart }
|
||||
})
|
||||
|
||||
// alpine.js
|
||||
|
83
assets/js/shot_log_chart.js
Normal file
83
assets/js/shot_log_chart.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { Chart, Title, Tooltip, Legend, LineController, LineElement, PointElement, TimeScale, LinearScale } from 'chart.js'
|
||||
import 'chartjs-adapter-date-fns'
|
||||
Chart.register(Title, Tooltip, Legend, LineController, LineElement, PointElement, TimeScale, LinearScale)
|
||||
|
||||
export default {
|
||||
initalizeChart (el) {
|
||||
const data = JSON.parse(el.dataset.chartData)
|
||||
|
||||
this.el.chart = new Chart(el, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: el.dataset.label,
|
||||
data: data.map(({ date, count, labels }) => ({
|
||||
labels,
|
||||
x: date,
|
||||
y: count
|
||||
})),
|
||||
backgroundColor: el.dataset.color,
|
||||
borderColor: el.dataset.color,
|
||||
fill: true,
|
||||
borderWidth: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
elements: {
|
||||
point: {
|
||||
radius: 7,
|
||||
hoverRadius: 10
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
padding: 20
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
label: ({ raw: { labels } }) => labels
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
stacked: true,
|
||||
grace: '15%',
|
||||
ticks: {
|
||||
padding: 15
|
||||
}
|
||||
},
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'day'
|
||||
}
|
||||
}
|
||||
},
|
||||
transitions: {
|
||||
show: {
|
||||
animations: {
|
||||
x: {
|
||||
from: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
hide: {
|
||||
animations: {
|
||||
x: {
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
mounted () { this.initalizeChart(this.el) },
|
||||
updated () { this.initalizeChart(this.el) }
|
||||
}
|
44
assets/package-lock.json
generated
44
assets/package-lock.json
generated
@ -8,6 +8,9 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"alpinejs": "^3.10.2",
|
||||
"chart.js": "^3.9.1",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
@ -3166,6 +3169,19 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz",
|
||||
"integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w=="
|
||||
},
|
||||
"node_modules/chartjs-adapter-date-fns": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz",
|
||||
"integrity": "sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
@ -3850,6 +3866,18 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@ -12537,6 +12565,17 @@
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz",
|
||||
"integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w=="
|
||||
},
|
||||
"chartjs-adapter-date-fns": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz",
|
||||
"integrity": "sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==",
|
||||
"requires": {}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
@ -13025,6 +13064,11 @@
|
||||
"css-tree": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"repository": {},
|
||||
"description": " ",
|
||||
"license": "MIT",
|
||||
"engines":{
|
||||
"engines": {
|
||||
"node": "18.12.1",
|
||||
"npm": "8.19.2"
|
||||
},
|
||||
@ -15,6 +15,9 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"alpinejs": "^3.10.2",
|
||||
"chart.js": "^3.9.1",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
|
@ -58,7 +58,7 @@ defmodule Cannery.ActivityLog.ShotGroup do
|
||||
|> cast(attrs, [:count, :notes, :date])
|
||||
|> validate_number(:count, greater_than: 0)
|
||||
|> validate_create_shot_group_count(ammo_group)
|
||||
|> validate_required([:count, :ammo_group_id, :user_id])
|
||||
|> validate_required([:count, :date, :ammo_group_id, :user_id])
|
||||
end
|
||||
|
||||
def create_changeset(shot_group, _invalid_user, _invalid_ammo_group, attrs) do
|
||||
@ -90,7 +90,7 @@ defmodule Cannery.ActivityLog.ShotGroup do
|
||||
shot_group
|
||||
|> cast(attrs, [:count, :notes, :date])
|
||||
|> validate_number(:count, greater_than: 0)
|
||||
|> validate_required([:count])
|
||||
|> validate_required([:count, :date])
|
||||
|> validate_update_shot_group_count(shot_group, user)
|
||||
end
|
||||
|
||||
|
@ -120,18 +120,4 @@ defmodule Cannery.Tags do
|
||||
"""
|
||||
@spec delete_tag!(Tag.t(), User.t()) :: Tag.t()
|
||||
def delete_tag!(%Tag{user_id: user_id} = tag, %User{id: user_id}), do: tag |> Repo.delete!()
|
||||
|
||||
@doc """
|
||||
Get a random tag bg_color in `#ffffff` hex format
|
||||
|
||||
## Examples
|
||||
|
||||
iex> random_color()
|
||||
"#cc0066"
|
||||
"""
|
||||
@spec random_bg_color() :: <<_::7>>
|
||||
def random_bg_color do
|
||||
["#cc0066", "#ff6699", "#6666ff", "#0066cc", "#00cc66", "#669900", "#ff9900", "#996633"]
|
||||
|> Enum.random()
|
||||
end
|
||||
end
|
||||
|
@ -88,17 +88,67 @@ defmodule CanneryWeb.RangeLive.Index do
|
||||
shot_groups
|
||||
|> Enum.map(fn shot_group -> shot_group |> get_row_data_for_shot_group(columns) end)
|
||||
|
||||
chart_data =
|
||||
shot_groups
|
||||
|> Enum.map(fn shot_group ->
|
||||
shot_group
|
||||
|> get_chart_data_for_shot_group([:name, :count, :notes, :date])
|
||||
end)
|
||||
|> Enum.sort_by(fn %{date: date} -> date end, Date)
|
||||
|
||||
socket
|
||||
|> assign(ammo_groups: ammo_groups, columns: columns, rows: rows, shot_groups: shot_groups)
|
||||
|> assign(
|
||||
ammo_groups: ammo_groups,
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
chart_data: chart_data,
|
||||
shot_groups: shot_groups
|
||||
)
|
||||
end
|
||||
|
||||
@spec get_row_data_for_shot_group(ShotGroup.t(), [map()]) :: [map()]
|
||||
@spec get_chart_data_for_shot_group(ShotGroup.t(), keys :: [atom()]) :: map()
|
||||
defp get_chart_data_for_shot_group(shot_group, keys) do
|
||||
shot_group = shot_group |> Repo.preload(ammo_group: :ammo_type)
|
||||
|
||||
labels =
|
||||
if shot_group.notes do
|
||||
[gettext("Notes: %{notes}", notes: shot_group.notes)]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
labels = [
|
||||
gettext(
|
||||
"Name: %{name}",
|
||||
name: shot_group.ammo_group.ammo_type.name
|
||||
),
|
||||
gettext(
|
||||
"Rounds shot: %{count}",
|
||||
count: shot_group.count
|
||||
)
|
||||
| labels
|
||||
]
|
||||
|
||||
keys
|
||||
|> Map.new(fn key ->
|
||||
value =
|
||||
case key do
|
||||
:name -> shot_group.ammo_group.ammo_type.name
|
||||
key -> shot_group |> Map.get(key)
|
||||
end
|
||||
|
||||
{key, value}
|
||||
end)
|
||||
|> Map.put(:labels, labels)
|
||||
end
|
||||
|
||||
@spec get_row_data_for_shot_group(ShotGroup.t(), [map()]) :: map()
|
||||
defp get_row_data_for_shot_group(%{date: date} = shot_group, columns) do
|
||||
shot_group = shot_group |> Repo.preload(ammo_group: :ammo_type)
|
||||
assigns = %{shot_group: shot_group}
|
||||
|
||||
columns
|
||||
|> Enum.into(%{}, fn %{key: key} ->
|
||||
|> Map.new(fn %{key: key} ->
|
||||
value =
|
||||
case key do
|
||||
:name ->
|
||||
|
@ -53,11 +53,27 @@
|
||||
<%= gettext("Shot log") %>
|
||||
</h1>
|
||||
|
||||
<canvas
|
||||
id="shot-log-chart"
|
||||
phx-hook="ShotLogChart"
|
||||
phx-update="ignore"
|
||||
class="max-h-72"
|
||||
data-chart-data={Jason.encode!(@chart_data)}
|
||||
data-label={gettext("Rounds fired")}
|
||||
data-color={random_color()}
|
||||
aria-label={gettext("Rounds fired chart")}
|
||||
role="img"
|
||||
>
|
||||
<%= dgettext("errors", "Your browser does not support the canvas element.") %>
|
||||
</canvas>
|
||||
|
||||
<.live_component
|
||||
module={CanneryWeb.Components.TableComponent}
|
||||
id="shot_groups_index_table"
|
||||
columns={@columns}
|
||||
rows={@rows}
|
||||
initial_key={:date}
|
||||
initial_sort_mode={:desc}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@ defmodule CanneryWeb.TagLive.Index do
|
||||
use CanneryWeb, :live_view
|
||||
import CanneryWeb.Components.TagCard
|
||||
alias Cannery.{Tags, Tags.Tag}
|
||||
alias CanneryWeb.Endpoint
|
||||
alias CanneryWeb.{Endpoint, ViewHelpers}
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket), do: {:ok, socket |> display_tags()}
|
||||
@ -25,7 +25,7 @@ defmodule CanneryWeb.TagLive.Index do
|
||||
defp apply_action(socket, :new, _params) do
|
||||
socket
|
||||
|> assign(:page_title, gettext("New Tag"))
|
||||
|> assign(:tag, %Tag{bg_color: Tags.random_bg_color(), text_color: "#ffffff"})
|
||||
|> assign(:tag, %Tag{bg_color: ViewHelpers.random_color(), text_color: "#ffffff"})
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
|
@ -115,4 +115,18 @@ defmodule CanneryWeb.ViewHelpers do
|
||||
</label>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a random color in `#ffffff` hex format
|
||||
|
||||
## Examples
|
||||
|
||||
iex> random_color()
|
||||
"#cc0066"
|
||||
"""
|
||||
@spec random_color() :: <<_::7>>
|
||||
def random_color do
|
||||
["#cc0066", "#ff6699", "#6666ff", "#0066cc", "#00cc66", "#669900", "#ff9900", "#996633"]
|
||||
|> Enum.random()
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user