mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Add nice charts to the stat cards
This commit is contained in:
parent
23805a6ef4
commit
29ac8c1136
15 changed files with 86 additions and 20 deletions
1
Gemfile
1
Gemfile
|
|
@ -17,6 +17,7 @@ gem 'turbo-rails'
|
||||||
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
|
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
|
||||||
gem "importmap-rails"
|
gem "importmap-rails"
|
||||||
gem "mapkick-rb"
|
gem "mapkick-rb"
|
||||||
|
gem "chartkick"
|
||||||
gem 'geocoder'
|
gem 'geocoder'
|
||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ GEM
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
byebug (11.1.3)
|
byebug (11.1.3)
|
||||||
|
chartkick (5.0.6)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.2.3)
|
concurrent-ruby (1.2.3)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
|
|
@ -323,6 +324,7 @@ PLATFORMS
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
bootsnap
|
bootsnap
|
||||||
|
chartkick
|
||||||
debug
|
debug
|
||||||
devise
|
devise
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class ImportsController < ApplicationController
|
||||||
files.each do |file|
|
files.each do |file|
|
||||||
import = current_user.imports.create(
|
import = current_user.imports.create(
|
||||||
name: file.original_filename,
|
name: file.original_filename,
|
||||||
source: params[:import][:source],
|
source: params[:import][:source]
|
||||||
)
|
)
|
||||||
|
|
||||||
import.file.attach(file)
|
import.file.attach(file)
|
||||||
|
|
@ -27,7 +27,7 @@ class ImportsController < ApplicationController
|
||||||
redirect_to imports_url, notice: "#{files.size} files are queued to be imported in background", status: :see_other
|
redirect_to imports_url, notice: "#{files.size} files are queued to be imported in background", status: :see_other
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Import.where(user: current_user, name: files.map(&:original_filename)).destroy_all
|
Import.where(user: current_user, name: files.map(&:original_filename)).destroy_all
|
||||||
Rails.logger.debug e.message
|
|
||||||
flash.now[:error] = e.message
|
flash.now[:error] = e.message
|
||||||
|
|
||||||
redirect_to new_import_path, notice: e.message, status: :unprocessable_entity
|
redirect_to new_import_path, notice: e.message, status: :unprocessable_entity
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ class PointsController < ApplicationController
|
||||||
|
|
||||||
@countries_and_cities = CountriesAndCities.new(@points).call
|
@countries_and_cities = CountriesAndCities.new(@points).call
|
||||||
@coordinates =
|
@coordinates =
|
||||||
@points
|
@points.pluck(:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id)
|
||||||
.pluck(:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id)
|
.map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7] }
|
||||||
.map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7] }
|
|
||||||
@distance = distance
|
@distance = distance
|
||||||
@start_at = Time.at(start_at)
|
@start_at = Time.zone.at(start_at)
|
||||||
@end_at = Time.at(end_at)
|
@end_at = Time.zone.at(end_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
@ -23,7 +22,7 @@ class PointsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def end_at
|
def end_at
|
||||||
return Date.today.end_of_day.to_i if params[:end_at].nil?
|
return Time.zone.today.end_of_day.to_i if params[:end_at].nil?
|
||||||
|
|
||||||
params[:end_at].to_datetime.to_i
|
params[:end_at].to_datetime.to_i
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,8 @@ module ApplicationHelper
|
||||||
|
|
||||||
{ start_at: start_at, end_at: end_at }
|
{ start_at: start_at, end_at: end_at }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def header_colors
|
||||||
|
%w[info success warning error accent secondary primary]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,5 @@ import "@hotwired/turbo-rails"
|
||||||
import "mapkick/bundle"
|
import "mapkick/bundle"
|
||||||
import "leaflet"
|
import "leaflet"
|
||||||
import "leaflet-providers"
|
import "leaflet-providers"
|
||||||
|
import "chartkick"
|
||||||
|
import "Chart.bundle"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Stat < ApplicationRecord
|
class Stat < ApplicationRecord
|
||||||
validates :year, :month, presence: true
|
validates :year, :month, presence: true
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
def timespan
|
||||||
|
DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month
|
||||||
|
end
|
||||||
|
|
||||||
|
def distance_by_day
|
||||||
|
timespan.to_a.map.with_index(1) do |day, index|
|
||||||
|
beginning_of_day = day.beginning_of_day.to_i
|
||||||
|
end_of_day = day.end_of_day.to_i
|
||||||
|
|
||||||
|
data = { day: index, distance: 0 }
|
||||||
|
|
||||||
|
# We have to filter by user as well
|
||||||
|
points = Point.where(timestamp: beginning_of_day..end_of_day)
|
||||||
|
|
||||||
|
points.each_cons(2) do |point1, point2|
|
||||||
|
distance = Geocoder::Calculations.distance_between(
|
||||||
|
[point1.latitude, point1.longitude], [point2.latitude, point2.longitude]
|
||||||
|
)
|
||||||
|
|
||||||
|
data[:distance] += distance
|
||||||
|
end
|
||||||
|
|
||||||
|
[data[:day], data[:distance].round(2)]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,8 @@ class User < ApplicationRecord
|
||||||
def total_cities
|
def total_cities
|
||||||
Stat.where(user: self).pluck(:toponyms).flatten.size
|
Stat.where(user: self).pluck(:toponyms).flatten.size
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def total_reverse_geocoded
|
||||||
|
points.where.not(country: nil, city: nil).count
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,13 @@ class CreateStats
|
||||||
end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i
|
end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i
|
||||||
|
|
||||||
points = points(beginning_of_month_timestamp, end_of_month_timestamp)
|
points = points(beginning_of_month_timestamp, end_of_month_timestamp)
|
||||||
|
|
||||||
next if points.empty?
|
next if points.empty?
|
||||||
|
|
||||||
Stat.create(
|
stat = Stat.create(year:, month:, user:, distance: distance(points), toponyms: toponyms(points))
|
||||||
year: year,
|
|
||||||
month: month,
|
stat.update(daily_distance: stat.distance_by_day) if stat.persisted?
|
||||||
distance: distance(points),
|
|
||||||
toponyms: toponyms(points),
|
stat
|
||||||
user: user
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end.compact
|
end.compact
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
</h2>
|
</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<% country[:cities].each do |city| %>
|
<% country[:cities].each do |city| %>
|
||||||
<li><%= city[:city] %> (<%= Time.at(city[:timestamp]).strftime("%d.%m.%Y") %>)</li>
|
<li><%= city[:city] %> (<%= Time.zone.at(city[:timestamp]).strftime("%d.%m.%Y") %>)</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<div id="<%= dom_id stat %>" class="card w-full bg-base-200 shadow-xl">
|
<div id="<%= dom_id stat %>" class="card w-full bg-base-200 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">
|
<h2 class="card-title">
|
||||||
<%= link_to points_url(month_timespan(stat)), class: 'underline hover:no-underline' do %>
|
<%= link_to points_url(month_timespan(stat)), class: "underline hover:no-underline text-#{header_colors.sample}" do %>
|
||||||
<%= "#{Date::MONTHNAMES[stat.month]} of #{stat.year}" %>
|
<%= "#{Date::MONTHNAMES[stat.month]} of #{stat.year}" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
@ -10,6 +10,13 @@
|
||||||
<div class="card-actions justify-end">
|
<div class="card-actions justify-end">
|
||||||
<%= stat.toponyms.count %> countries, <%= stat.toponyms.sum { _1['cities'].count } %> cities
|
<%= stat.toponyms.count %> countries, <%= stat.toponyms.sum { _1['cities'].count } %> cities
|
||||||
</div>
|
</div>
|
||||||
|
<%= column_chart(
|
||||||
|
stat.daily_distance,
|
||||||
|
height: '100px',
|
||||||
|
suffix: ' km',
|
||||||
|
xtitle: 'Days',
|
||||||
|
ytitle: 'Distance'
|
||||||
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,24 @@
|
||||||
<div class="stat-title">Total distance</div>
|
<div class="stat-title">Total distance</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="stat text-center">
|
||||||
|
<div class="stat-value text-success">
|
||||||
|
<%= number_with_delimiter current_user.points.count %>
|
||||||
|
</div>
|
||||||
|
<div class="stat-title">Geopoints tracked</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% if REVERSE_GEOCODING_ENABLED %>
|
<% if REVERSE_GEOCODING_ENABLED %>
|
||||||
<div class="stat text-center">
|
<div class="stat text-center">
|
||||||
<div class="stat-value text-secondary">
|
<div class="stat-value text-secondary">
|
||||||
<%= current_user.total_countries %>
|
<%= current_user.total_reverse_geocoded %>
|
||||||
|
</div>
|
||||||
|
<div class="stat-title">Reverse geocoded points</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat text-center">
|
||||||
|
<div class="stat-value text-warning">
|
||||||
|
<%= number_with_delimiter current_user.total_countries %>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title">Countries visited</div>
|
<div class="stat-title">Countries visited</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,5 @@ pin_all_from "app/javascript/controllers", under: "controllers"
|
||||||
pin "mapkick/bundle", to: "mapkick.bundle.js"
|
pin "mapkick/bundle", to: "mapkick.bundle.js"
|
||||||
pin "leaflet" # @1.9.4
|
pin "leaflet" # @1.9.4
|
||||||
pin "leaflet-providers" # @2.0.0
|
pin "leaflet-providers" # @2.0.0
|
||||||
|
pin "chartkick", to: "chartkick.js"
|
||||||
|
pin "Chart.bundle", to: "Chart.bundle.js"
|
||||||
|
|
|
||||||
5
db/migrate/20240324173315_add_daily_distance_to_stat.rb
Normal file
5
db/migrate/20240324173315_add_daily_distance_to_stat.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddDailyDistanceToStat < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :stats, :daily_distance, :jsonb, default: {}
|
||||||
|
end
|
||||||
|
end
|
||||||
3
db/schema.rb
generated
3
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_03_24_161800) do
|
ActiveRecord::Schema[7.1].define(version: 2024_03_24_173315) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
|
@ -101,6 +101,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_24_161800) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
|
t.jsonb "daily_distance", default: {}
|
||||||
t.index ["distance"], name: "index_stats_on_distance"
|
t.index ["distance"], name: "index_stats_on_distance"
|
||||||
t.index ["month"], name: "index_stats_on_month"
|
t.index ["month"], name: "index_stats_on_month"
|
||||||
t.index ["user_id"], name: "index_stats_on_user_id"
|
t.index ["user_id"], name: "index_stats_on_user_id"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue