forked from shibao/cannery
		
	add graph to range page
This commit is contained in:
		| @@ -10,6 +10,7 @@ | |||||||
| - Make container show page a bit more compact | - Make container show page a bit more compact | ||||||
| - Make container show page filter used-up ammo | - Make container show page filter used-up ammo | ||||||
| - Forgot to add the logo as the favicon whoops | - Forgot to add the logo as the favicon whoops | ||||||
|  | - Add graph to range page | ||||||
| - Update project dependencies | - Update project dependencies | ||||||
|  |  | ||||||
| # v0.5.4 | # v0.5.4 | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ import { Socket } from 'phoenix' | |||||||
| import { LiveSocket } from 'phoenix_live_view' | import { LiveSocket } from 'phoenix_live_view' | ||||||
| import topbar from '../vendor/topbar' | import topbar from '../vendor/topbar' | ||||||
| import MaintainAttrs from './maintain_attrs' | import MaintainAttrs from './maintain_attrs' | ||||||
|  | import ShotLogChart from './shot_log_chart' | ||||||
| import Alpine from 'alpinejs' | import Alpine from 'alpinejs' | ||||||
|  |  | ||||||
| const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content') | const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content') | ||||||
| @@ -36,7 +37,7 @@ const liveSocket = new LiveSocket('/live', Socket, { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   params: { _csrf_token: csrfToken }, |   params: { _csrf_token: csrfToken }, | ||||||
|   hooks: { MaintainAttrs } |   hooks: { MaintainAttrs, ShotLogChart } | ||||||
| }) | }) | ||||||
|  |  | ||||||
| // alpine.js | // 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": { |       "dependencies": { | ||||||
|         "@fortawesome/fontawesome-free": "^6.1.1", |         "@fortawesome/fontawesome-free": "^6.1.1", | ||||||
|         "alpinejs": "^3.10.2", |         "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": "file:../deps/phoenix", | ||||||
|         "phoenix_html": "file:../deps/phoenix_html", |         "phoenix_html": "file:../deps/phoenix_html", | ||||||
|         "phoenix_live_view": "file:../deps/phoenix_live_view", |         "phoenix_live_view": "file:../deps/phoenix_live_view", | ||||||
| @@ -3166,6 +3169,19 @@ | |||||||
|         "node": ">=4" |         "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": { |     "node_modules/chokidar": { | ||||||
|       "version": "3.5.3", |       "version": "3.5.3", | ||||||
|       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", |       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", | ||||||
| @@ -3850,6 +3866,18 @@ | |||||||
|         "node": ">=8.0.0" |         "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": { |     "node_modules/debug": { | ||||||
|       "version": "4.3.4", |       "version": "4.3.4", | ||||||
|       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", |       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", | ||||||
| @@ -12537,6 +12565,17 @@ | |||||||
|         "supports-color": "^5.3.0" |         "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": { |     "chokidar": { | ||||||
|       "version": "3.5.3", |       "version": "3.5.3", | ||||||
|       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", |       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", | ||||||
| @@ -13025,6 +13064,11 @@ | |||||||
|         "css-tree": "^1.1.2" |         "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": { |     "debug": { | ||||||
|       "version": "4.3.4", |       "version": "4.3.4", | ||||||
|       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", |       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|   "repository": {}, |   "repository": {}, | ||||||
|   "description": " ", |   "description": " ", | ||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "engines":{ |   "engines": { | ||||||
|     "node": "18.12.1", |     "node": "18.12.1", | ||||||
|     "npm": "8.19.2" |     "npm": "8.19.2" | ||||||
|   }, |   }, | ||||||
| @@ -15,6 +15,9 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@fortawesome/fontawesome-free": "^6.1.1", |     "@fortawesome/fontawesome-free": "^6.1.1", | ||||||
|     "alpinejs": "^3.10.2", |     "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": "file:../deps/phoenix", | ||||||
|     "phoenix_html": "file:../deps/phoenix_html", |     "phoenix_html": "file:../deps/phoenix_html", | ||||||
|     "phoenix_live_view": "file:../deps/phoenix_live_view", |     "phoenix_live_view": "file:../deps/phoenix_live_view", | ||||||
|   | |||||||
| @@ -58,7 +58,7 @@ defmodule Cannery.ActivityLog.ShotGroup do | |||||||
|     |> cast(attrs, [:count, :notes, :date]) |     |> cast(attrs, [:count, :notes, :date]) | ||||||
|     |> validate_number(:count, greater_than: 0) |     |> validate_number(:count, greater_than: 0) | ||||||
|     |> validate_create_shot_group_count(ammo_group) |     |> 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 |   end | ||||||
|  |  | ||||||
|   def create_changeset(shot_group, _invalid_user, _invalid_ammo_group, attrs) do |   def create_changeset(shot_group, _invalid_user, _invalid_ammo_group, attrs) do | ||||||
| @@ -90,7 +90,7 @@ defmodule Cannery.ActivityLog.ShotGroup do | |||||||
|     shot_group |     shot_group | ||||||
|     |> cast(attrs, [:count, :notes, :date]) |     |> cast(attrs, [:count, :notes, :date]) | ||||||
|     |> validate_number(:count, greater_than: 0) |     |> validate_number(:count, greater_than: 0) | ||||||
|     |> validate_required([:count]) |     |> validate_required([:count, :date]) | ||||||
|     |> validate_update_shot_group_count(shot_group, user) |     |> validate_update_shot_group_count(shot_group, user) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -120,18 +120,4 @@ defmodule Cannery.Tags do | |||||||
|   """ |   """ | ||||||
|   @spec delete_tag!(Tag.t(), User.t()) :: Tag.t() |   @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!() |   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 | end | ||||||
|   | |||||||
| @@ -88,17 +88,67 @@ defmodule CanneryWeb.RangeLive.Index do | |||||||
|       shot_groups |       shot_groups | ||||||
|       |> Enum.map(fn shot_group -> shot_group |> get_row_data_for_shot_group(columns) end) |       |> 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 |     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 |   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 |   defp get_row_data_for_shot_group(%{date: date} = shot_group, columns) do | ||||||
|     shot_group = shot_group |> Repo.preload(ammo_group: :ammo_type) |     shot_group = shot_group |> Repo.preload(ammo_group: :ammo_type) | ||||||
|     assigns = %{shot_group: shot_group} |     assigns = %{shot_group: shot_group} | ||||||
|  |  | ||||||
|     columns |     columns | ||||||
|     |> Enum.into(%{}, fn %{key: key} -> |     |> Map.new(fn %{key: key} -> | ||||||
|       value = |       value = | ||||||
|         case key do |         case key do | ||||||
|           :name -> |           :name -> | ||||||
|   | |||||||
| @@ -53,11 +53,27 @@ | |||||||
|       <%= gettext("Shot log") %> |       <%= gettext("Shot log") %> | ||||||
|     </h1> |     </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 |     <.live_component | ||||||
|       module={CanneryWeb.Components.TableComponent} |       module={CanneryWeb.Components.TableComponent} | ||||||
|       id="shot_groups_index_table" |       id="shot_groups_index_table" | ||||||
|       columns={@columns} |       columns={@columns} | ||||||
|       rows={@rows} |       rows={@rows} | ||||||
|  |       initial_key={:date} | ||||||
|  |       initial_sort_mode={:desc} | ||||||
|     /> |     /> | ||||||
|   <% end %> |   <% end %> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ defmodule CanneryWeb.TagLive.Index do | |||||||
|   use CanneryWeb, :live_view |   use CanneryWeb, :live_view | ||||||
|   import CanneryWeb.Components.TagCard |   import CanneryWeb.Components.TagCard | ||||||
|   alias Cannery.{Tags, Tags.Tag} |   alias Cannery.{Tags, Tags.Tag} | ||||||
|   alias CanneryWeb.Endpoint |   alias CanneryWeb.{Endpoint, ViewHelpers} | ||||||
|  |  | ||||||
|   @impl true |   @impl true | ||||||
|   def mount(_params, _session, socket), do: {:ok, socket |> display_tags()} |   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 |   defp apply_action(socket, :new, _params) do | ||||||
|     socket |     socket | ||||||
|     |> assign(:page_title, gettext("New Tag")) |     |> 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 |   end | ||||||
|  |  | ||||||
|   defp apply_action(socket, :index, _params) do |   defp apply_action(socket, :index, _params) do | ||||||
|   | |||||||
| @@ -115,4 +115,18 @@ defmodule CanneryWeb.ViewHelpers do | |||||||
|     </label> |     </label> | ||||||
|     """ |     """ | ||||||
|   end |   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 | end | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user