From 81a8508e140a39ad9eede68b4fff7e0650e686e9 Mon Sep 17 00:00:00 2001 From: Yevhenii Hurin Date: Sat, 7 Oct 2023 19:04:13 +0300 Subject: [PATCH] 16 highcharts (#21) * Tweet metric methods * Add TweetsController with views * Linting fix * Add charts * Sort Gemfile * Align side title with vh * Linting fix * Renove redundand highsharts script from the app template --- Gemfile | 3 + Gemfile.lock | 19 +++ app/assets/stylesheets/style.scss | 21 +++ app/controllers/tweets_controller.rb | 21 +++ app/decorators/application_decorator.rb | 8 ++ app/decorators/tweet_decorator.rb | 120 ++++++++++++++++++ app/javascript/application.js | 1 + app/models/tweet.rb | 42 ++++++ app/views/layouts/application.html.slim | 2 +- app/views/shared/_navbar.html.slim | 16 +++ app/views/tweets/index.html.slim | 32 +++++ app/views/tweets/show.html.slim | 65 ++++++++++ app/views/welcome/index.html.slim | 18 +-- config/routes.rb | 2 + lib/tasks/auto_annotate_models.rake | 59 +++++++++ lib/tasks/tweet_metrics_importer.rb | 2 +- package.json | 2 + spec/lib/tasks/tweet_metrics_importer_spec.rb | 4 +- spec/models/tweet_spec.rb | 104 +++++++++++++-- spec/requests/tweets_spec.rb | 33 +++++ yarn.lock | 50 ++++++++ 21 files changed, 593 insertions(+), 31 deletions(-) create mode 100644 app/controllers/tweets_controller.rb create mode 100644 app/decorators/application_decorator.rb create mode 100644 app/decorators/tweet_decorator.rb create mode 100644 app/views/shared/_navbar.html.slim create mode 100644 app/views/tweets/index.html.slim create mode 100644 app/views/tweets/show.html.slim create mode 100644 lib/tasks/auto_annotate_models.rake create mode 100644 spec/requests/tweets_spec.rb diff --git a/Gemfile b/Gemfile index 29eec1e..08ca54a 100644 --- a/Gemfile +++ b/Gemfile @@ -31,7 +31,10 @@ gem "cssbundling-rails" gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] gem "awesome_print", "~> 1.9" +gem "chartkick", "~> 5.0" gem "data_migrate", "~> 9.2" +gem "draper", "~> 4.0" +gem "groupdate", "~> 6.4" gem "pry", "~> 0.14.2" gem "slim", "~> 5.1" gem "sqlite3", "~> 1.6" diff --git a/Gemfile.lock b/Gemfile.lock index 431e884..e48fe87 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,6 +54,10 @@ GEM globalid (>= 0.3.6) activemodel (7.1.0) activesupport (= 7.1.0) + activemodel-serializers-xml (1.0.2) + activemodel (> 5.x) + activesupport (> 5.x) + builder (~> 3.1) activerecord (7.1.0) activemodel (= 7.1.0) activesupport (= 7.1.0) @@ -89,6 +93,7 @@ GEM binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) builder (3.2.4) + chartkick (5.0.4) coderay (1.1.3) concurrent-ruby (1.2.2) connection_pool (2.4.1) @@ -104,6 +109,13 @@ GEM reline (>= 0.3.1) debug_inspector (1.1.0) diff-lcs (1.5.0) + draper (4.0.2) + actionpack (>= 5.0) + activemodel (>= 5.0) + activemodel-serializers-xml (>= 1.0) + activesupport (>= 5.0) + request_store (>= 1.0) + ruby2_keywords drb (2.1.1) ruby2_keywords erubi (1.12.0) @@ -118,6 +130,8 @@ GEM ruby-progressbar (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) + groupdate (6.4.0) + activesupport (>= 6.1) i18n (1.14.1) concurrent-ruby (~> 1.0) io-console (0.6.0) @@ -218,6 +232,8 @@ GEM regexp_parser (2.8.1) reline (0.3.9) io-console (~> 0.5) + request_store (1.5.1) + rack (>= 1.4) rexml (3.2.6) rouge (4.1.3) rspec-core (3.12.2) @@ -318,12 +334,15 @@ DEPENDENCIES awesome_print (~> 1.9) better_errors (~> 2.10) binding_of_caller (~> 1.0) + chartkick (~> 5.0) cssbundling-rails data_migrate (~> 9.2) debug + draper (~> 4.0) factory_bot_rails (~> 6.2) ffaker (~> 2.23) fuubar (~> 2.5) + groupdate (~> 6.4) jsbundling-rails ordinare (~> 0.4.0) pg (~> 1.1) diff --git a/app/assets/stylesheets/style.scss b/app/assets/stylesheets/style.scss index 00f8a95..b00e657 100644 --- a/app/assets/stylesheets/style.scss +++ b/app/assets/stylesheets/style.scss @@ -6,3 +6,24 @@ .title { font-family: Source Code Pro; } + +h1.rotated { + transform: rotate(-90deg); + transform-origin: bottom left; + white-space: nowrap; + position: absolute; + bottom: 30vh; + left: 9vw; + font-family: Source Code Pro; + font-size: 3rem; +} + +@media (max-width: 768px) { + h1.rotated { + transform: none; + position: static; + bottom: auto; + left: auto; + font-size: 2rem; + } +} diff --git a/app/controllers/tweets_controller.rb b/app/controllers/tweets_controller.rb new file mode 100644 index 0000000..744e0fa --- /dev/null +++ b/app/controllers/tweets_controller.rb @@ -0,0 +1,21 @@ +class TweetsController < ApplicationController + def index + @tweets = Tweet.first(10).map(&:decorate) + + respond_to do |format| + format.html # renders index.html.slim + end + end + + def show + @tweet = Tweet.find(params[:id]).decorate + + respond_to do |format| + format.html # renders show.html.slim + format.json do + # TODO: add filtering by date range and by metrics subset + render json: @tweet.tweet_metrics.to_json + end + end + end +end diff --git a/app/decorators/application_decorator.rb b/app/decorators/application_decorator.rb new file mode 100644 index 0000000..5caf08b --- /dev/null +++ b/app/decorators/application_decorator.rb @@ -0,0 +1,8 @@ +class ApplicationDecorator < Draper::Decorator + # Define methods for all decorated objects. + # Helpers are accessed through `helpers` (aka `h`). For example: + # + # def percent_amount + # h.number_to_percentage object.amount, precision: 2 + # end +end diff --git a/app/decorators/tweet_decorator.rb b/app/decorators/tweet_decorator.rb new file mode 100644 index 0000000..f4bee72 --- /dev/null +++ b/app/decorators/tweet_decorator.rb @@ -0,0 +1,120 @@ +class TweetDecorator < Draper::Decorator + delegate_all + + def combined_chart + h.area_chart combined_metrics_series, height: "20vh", library: chart_options + end + + def likes_chart + h.line_chart( + likes_metric[:data], + min: min_y(likes_metric), + max: max_y(likes_metric), + height: "70vh", + library: single_metric_chart_options("Likes") + ) + end + + def replies_chart + h.line_chart( + replies_metric[:data], + min: min_y(replies_metric), + max: max_y(replies_metric), + height: "70vh", + library: single_metric_chart_options("Replies") + ) + end + + def reposts_chart + h.line_chart( + reposts_metric[:data], + min: min_y(reposts_metric), + max: max_y(reposts_metric), + height: "70vh", + library: single_metric_chart_options("Reposts") + ) + end + + def bookmarks_chart + h.line_chart( + bookmarks_metric[:data], + min: min_y(bookmarks_metric), + max: max_y(bookmarks_metric), + height: "70vh", + library: single_metric_chart_options("Bookmarks") + ) + end + + def views_chart + h.line_chart( + views_metric[:data], + min: min_y(views_metric), + max: max_y(views_metric), + height: "70vh", + library: single_metric_chart_options("Views") + ) + end + + private + + def min_y(metric) + metric[:data].min_by { |k, v| k.to_i }&.last + end + + def max_y(metric) + metric[:data].max_by { |k, v| k.to_i }&.last + end + + def single_metric_chart_options(title) + chart_options.merge( + yAxis: { + title: { + text: title + } + } + ) + end + + def chart_options + { + chart: { + zoomType: "xy" + }, + resetZoomButton: { + theme: { + display: "flex" + } + } + } + end + + def combined_metrics_series + [ + likes_metric, + replies_metric, + reposts_metric, + bookmarks_metric, + views_metric + ] + end + + def likes_metric + @likes_metric ||= {name: "Likes", data: object.tweet_metrics.group_by_minute(:created_at).maximum(:likes)} + end + + def replies_metric + @replies_metric ||= {name: "Replies", data: object.tweet_metrics.group_by_minute(:created_at).maximum(:replies)} + end + + def reposts_metric + @reposts_metric ||= {name: "Reposts", data: object.tweet_metrics.group_by_minute(:created_at).maximum(:reposts)} + end + + def bookmarks_metric + @bookmarks_metric ||= {name: "Bookmarks", data: object.tweet_metrics.group_by_minute(:created_at).maximum(:bookmarks)} + end + + def views_metric + @views_metric ||= {name: "Views", data: object.tweet_metrics.group_by_minute(:created_at).maximum(:views)} + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index d933293..b9f57c5 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,3 +1,4 @@ // Entry point for the build script in your package.json import "@hotwired/turbo-rails" import "./controllers" +import "chartkick/highcharts" diff --git a/app/models/tweet.rb b/app/models/tweet.rb index f32832b..af9b7d1 100644 --- a/app/models/tweet.rb +++ b/app/models/tweet.rb @@ -15,8 +15,50 @@ class Tweet < ApplicationRecord before_validation :set_author + def author_avatar_url + # TODO: Implement a way to get the avatar url from the tweet + "https://pbs.twimg.com/profile_images/1678305940481753089/g751T5c__400x400.jpg" + end + + def author_url + "https://twitter.com/#{author}" + end + + def author_name + "@#{author}" + end + + def likes + last_metric&.likes + end + + def reposts + last_metric&.reposts + end + + def replies + last_metric&.replies + end + + # TODO: Add quotes to the tweet metrics + # def quotes + # last_metric&.quotes + # end + + def bookmarks + last_metric&.bookmarks + end + + def views + last_metric&.views + end + private + def last_metric + @last_metric ||= tweet_metrics.last + end + def set_author self.author = url&.split("/")&.fetch(3) end diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index a361ba6..325385c 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -8,9 +8,9 @@ html = stylesheet_link_tag 'application', 'data-turbo-track': 'reload' = javascript_include_tag 'application', 'data-turbo-track': 'reload', defer: true link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css" - script src="https://kit.fontawesome.com/08b1825a15.js" crossorigin="anonymous" link rel="preconnect" href="https://fonts.googleapis.com" link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin" link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap" rel="stylesheet" + script src="https://kit.fontawesome.com/08b1825a15.js" crossorigin="anonymous" body == yield diff --git a/app/views/shared/_navbar.html.slim b/app/views/shared/_navbar.html.slim new file mode 100644 index 0000000..d2cc1a4 --- /dev/null +++ b/app/views/shared/_navbar.html.slim @@ -0,0 +1,16 @@ +header.navbar + .container + .navbar-brand + a.navbar-item href="/" + | X-Tracker + span.navbar-burger.burger data-target="navbarMenuHeroA" + span + span + span + .navbar-menu id="navbarMenuHeroA" + .navbar-end + a.navbar-item href="/" + | Home + = link_to "Tweets", tweets_path, class: "navbar-item" + / a.navbar-item href="/contact" + / | Contact diff --git a/app/views/tweets/index.html.slim b/app/views/tweets/index.html.slim new file mode 100644 index 0000000..626dbd1 --- /dev/null +++ b/app/views/tweets/index.html.slim @@ -0,0 +1,32 @@ +section.hero.is-info + .hero-head + = render "shared/navbar" +.section + .columns.is-centered + .column.is-1 + h1.rotated.has-text-grey Tracked Tweets + .column.is-10 + - @tweets.each do |tweet| + .card + .card-content + .media + .media-left + figure class="image is-48x48" + img src="#{tweet.author_avatar_url}" class="is-rounded" + .media-content + p class="title is-4" + = link_to tweet.author_name, tweet.author_url, target: "_blank" + p class="subtitle is-6" + = tweet.body.truncate(280) + .content + = tweet.combined_chart + footer.card-footer + = link_to tweet_path(tweet), class: "card-footer-item primary" do + span View Metrics + span.icon + i class="fa fa-chart-line" + = link_to tweet.url, class: "card-footer-item", target: "_blank" do + span View Tweet + span.icon + i class="fa fa-twitter" + .column.is-1 diff --git a/app/views/tweets/show.html.slim b/app/views/tweets/show.html.slim new file mode 100644 index 0000000..746ed84 --- /dev/null +++ b/app/views/tweets/show.html.slim @@ -0,0 +1,65 @@ +section.hero.is-info + .hero-head + = render "shared/navbar" +.section + .columns.is-centered + .column.is-1 + h1.rotated.has-text-grey= @tweet.author_name + .column.is-10 + .card + .card-content + .content + .columns.is-centered + .column.is-6 + h2.title.is-4.has-text-dark + | Tweet + .content + p= @tweet.body + .column.is-6 + h2.title.is-4.has-text-dark + | Metrics + .content + p + | Likes: #{number_with_delimiter(@tweet.likes)} + p + | Retweets: #{number_with_delimiter(@tweet.reposts)} + p + | Comments: #{number_with_delimiter(@tweet.replies)} + p + | Views: #{number_with_delimiter(@tweet.views)} + hr + .content + .card + .card-content + .content + h4.title.is-4.has-text-dark + | Likes + = @tweet.likes_chart + .content + .card + .card-content + .content + h4.title.is-4.has-text-dark + | Retweets + = @tweet.reposts_chart + .content + .card + .card-content + .content + h4.title.is-4.has-text-dark + | Comments + = @tweet.replies_chart + .content + .card + .card-content + .content + h4.title.is-4.has-text-dark + | Bookmarks + = @tweet.bookmarks_chart + .content + .card + .card-content + .content + h4.title.is-4.has-text-dark + | Views + = @tweet.views_chart diff --git a/app/views/welcome/index.html.slim b/app/views/welcome/index.html.slim index 57e65ee..bd97868 100644 --- a/app/views/welcome/index.html.slim +++ b/app/views/welcome/index.html.slim @@ -1,22 +1,6 @@ section.hero.is-fullheight.is-info .hero-head - header.navbar - .container - .navbar-brand - a.navbar-item href="/" - | X-Tracker - span.navbar-burger.burger data-target="navbarMenuHeroA" - span - span - span - .navbar-menu id="navbarMenuHeroA" - .navbar-end - a.navbar-item href="/" - | Home - / a.navbar-item href="/about" - / | About - / a.navbar-item href="/contact" - / | Contact + = render "shared/navbar" .hero-body .container diff --git a/config/routes.rb b/config/routes.rb index a8216b4..0386a9c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,5 @@ Rails.application.routes.draw do + resources :tweets, only: %i[index show] + root "welcome#index" end diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rake new file mode 100644 index 0000000..60381ab --- /dev/null +++ b/lib/tasks/auto_annotate_models.rake @@ -0,0 +1,59 @@ +# NOTE: only doing this in development as some production environments (Heroku) +# NOTE: are sensitive to local FS writes, and besides -- it's just not proper +# NOTE: to have a dev-mode tool do its thing in production. +if Rails.env.development? + require "annotate" + task :set_annotation_options do + # You can override any of these by setting an environment variable of the + # same name. + Annotate.set_defaults( + "active_admin" => "false", + "additional_file_patterns" => [], + "routes" => "false", + "models" => "true", + "position_in_routes" => "before", + "position_in_class" => "before", + "position_in_test" => "before", + "position_in_fixture" => "before", + "position_in_factory" => "before", + "position_in_serializer" => "before", + "show_foreign_keys" => "true", + "show_complete_foreign_keys" => "false", + "show_indexes" => "true", + "simple_indexes" => "false", + "model_dir" => "app/models", + "root_dir" => "", + "include_version" => "false", + "require" => "", + "exclude_tests" => "false", + "exclude_fixtures" => "false", + "exclude_factories" => "false", + "exclude_serializers" => "false", + "exclude_scaffolds" => "true", + "exclude_controllers" => "true", + "exclude_helpers" => "true", + "exclude_sti_subclasses" => "false", + "ignore_model_sub_dir" => "false", + "ignore_columns" => nil, + "ignore_routes" => nil, + "ignore_unknown_models" => "false", + "hide_limit_column_types" => "integer,bigint,boolean", + "hide_default_column_types" => "json,jsonb,hstore", + "skip_on_db_migrate" => "false", + "format_bare" => "true", + "format_rdoc" => "false", + "format_yard" => "false", + "format_markdown" => "false", + "sort" => "false", + "force" => "false", + "frozen" => "false", + "classified_sort" => "true", + "trace" => "false", + "wrapper_open" => nil, + "wrapper_close" => nil, + "with_comment" => "true" + ) + end + + Annotate.load_tasks +end diff --git a/lib/tasks/tweet_metrics_importer.rb b/lib/tasks/tweet_metrics_importer.rb index a578140..de4ae0f 100644 --- a/lib/tasks/tweet_metrics_importer.rb +++ b/lib/tasks/tweet_metrics_importer.rb @@ -22,7 +22,7 @@ def self.import_batch(batch) def self.transform_row(row) { tweet_id: tweet.id, - likes: row["likes"], + likes: row["count"], created_at: row["created_at"], updated_at: row["updated_at"] } diff --git a/package.json b/package.json index 2031e7c..4b586ea 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^7.3.0", "bulma": "^0.9.4", + "chartkick": "^5.0.1", + "highcharts": "^11.1.0", "sass": "^1.68.0", "webpack": "^5.88.2", "webpack-cli": "^5.1.4" diff --git a/spec/lib/tasks/tweet_metrics_importer_spec.rb b/spec/lib/tasks/tweet_metrics_importer_spec.rb index abf98d5..561650b 100644 --- a/spec/lib/tasks/tweet_metrics_importer_spec.rb +++ b/spec/lib/tasks/tweet_metrics_importer_spec.rb @@ -52,7 +52,7 @@ let(:batch) do [ { - "likes" => 42, + "count" => 42, "created_at" => time, "updated_at" => time } @@ -104,7 +104,7 @@ describe ".transform_row" do it "transforms a row" do row = { - "likes" => 42, + "count" => 42, "created_at" => time, "updated_at" => time } diff --git a/spec/models/tweet_spec.rb b/spec/models/tweet_spec.rb index 94b5dd0..d410028 100644 --- a/spec/models/tweet_spec.rb +++ b/spec/models/tweet_spec.rb @@ -12,27 +12,111 @@ require "rails_helper" RSpec.describe Tweet, type: :model do + let(:tweet) { create(:tweet) } + describe "validations" do it "validates uniqueness of url" do - first_tweet = create(:tweet) - tweet = Tweet.new(url: first_tweet.url) - expect(tweet).to_not be_valid - expect(tweet.errors.messages[:url]).to eq(["has already been taken"]) + new_tweet = Tweet.new(url: tweet.url) + expect(new_tweet).to_not be_valid + expect(new_tweet.errors.messages[:url]).to eq(["has already been taken"]) end it "validates presence of url" do - tweet = Tweet.new(url: nil) - expect(tweet).to_not be_valid - expect(tweet.errors.messages[:url]).to eq(["can't be blank"]) + new_tweet = Tweet.new(url: nil) + expect(new_tweet).to_not be_valid + expect(new_tweet.errors.messages[:url]).to eq(["can't be blank"]) end end describe "callbacks" do describe "#get_author" do it "sets the author from the url" do - tweet = Tweet.new(url: "https://twitter.com/P_Kallioniemi/status/1674360288445964288", author: nil) - expect(tweet).to be_valid - expect(tweet.author).to eq("P_Kallioniemi") + new_tweet = Tweet.new(url: "https://twitter.com/P_Kallioniemi/status/1674360288445964288", author: nil) + expect(new_tweet).to be_valid + expect(new_tweet.author).to eq("P_Kallioniemi") + end + end + end + + describe "instance methods" do + describe "#author_url" do + subject { tweet.author_url } + + it { is_expected.to eq("https://twitter.com/P_Kallioniemi") } + end + + describe "#author_name" do + subject { tweet.author_name } + + it { is_expected.to eq("@P_Kallioniemi") } + end + + describe "metrics" do + context "when there are no metrics" do + describe "#likes" do + subject { tweet.likes } + + it { is_expected.to be_nil } + end + + describe "#reposts" do + subject { tweet.reposts } + + it { is_expected.to be_nil } + end + + describe "#replies" do + subject { tweet.replies } + + it { is_expected.to be_nil } + end + + describe "#bookmarks" do + subject { tweet.bookmarks } + + it { is_expected.to be_nil } + end + + describe "#views" do + subject { tweet.views } + + it { is_expected.to be_nil } + end + end + + context "when there are metrics" do + let!(:first_metric) { create(:tweet_metric, tweet: tweet, likes: 1, reposts: 2, replies: 3, bookmarks: 4, views: 5, created_at: 1.day.ago) } + let!(:last_metric) { create(:tweet_metric, tweet: tweet, likes: 6, reposts: 7, replies: 8, bookmarks: 9, views: 10) } + + describe "#likes" do + subject { tweet.likes } + + it { is_expected.to eq(6) } + end + + describe "#reposts" do + subject { tweet.reposts } + + it { is_expected.to eq(7) } + end + + describe "#replies" do + subject { tweet.replies } + + it { is_expected.to eq(8) } + end + + describe "#bookmarks" do + subject { tweet.bookmarks } + + it { is_expected.to eq(9) } + end + + describe "#views" do + subject { tweet.views } + + it { is_expected.to eq(10) } + end end end end diff --git a/spec/requests/tweets_spec.rb b/spec/requests/tweets_spec.rb new file mode 100644 index 0000000..4030527 --- /dev/null +++ b/spec/requests/tweets_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe "Tweets", type: :request do + let!(:tweet) { create(:tweet) } + + describe "GET index" do + subject { get "/tweets" } + + it "returns http success" do + subject + expect(response).to have_http_status(:success) + end + + it "returns a list of tweets" do + subject + expect(response.body).to include(tweet.author) + end + end + + describe "GET show" do + subject { get "/tweets/#{tweet.id}" } + + it "returns http success" do + subject + expect(response).to have_http_status(:success) + end + + it "returns a tweet" do + subject + expect(response.body).to include(tweet.author) + end + end +end diff --git a/yarn.lock b/yarn.lock index c4d23c7..bfeeefc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@babel/runtime@^7.21.0": + version "7.23.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d" + integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g== + dependencies: + regenerator-runtime "^0.14.0" + "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -65,6 +72,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@kurkle/color@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" + integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== + "@rails/actioncable@^7.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.1.0.tgz#9b1543b0abf5a0a7ef7088882ee3d6092b2745b3" @@ -317,6 +329,27 @@ caniuse-lite@^1.0.30001541: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz#10fdad03436cfe3cc632d3af7a99a0fb497407f0" integrity sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw== +chart.js@4: + version "4.4.0" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.0.tgz#df843fdd9ec6bd88d7f07e2b95348d221bd2698c" + integrity sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ== + dependencies: + "@kurkle/color" "^0.3.0" + +chartjs-adapter-date-fns@>=3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz#c25f63c7f317c1f96f9a7c44bd45eeedb8a478e5" + integrity sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg== + +chartkick@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/chartkick/-/chartkick-5.0.1.tgz#f557ff8560f974343dc65c7fc34ce1e8326d8ee7" + integrity sha512-4F3tWI3eBQgnjCYZIZ+fHOaJuNyxeyhDE2Tm+voOWB19hDjSJceys/spzN52DOn8bWepNESGXvPVTGU1jeFsbA== + optionalDependencies: + chart.js "4" + chartjs-adapter-date-fns ">=3" + date-fns ">=2" + "chokidar@>=3.0.0 <4.0.0": version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -370,6 +403,13 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +date-fns@>=2: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + electron-to-chromium@^1.4.535: version "1.4.543" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.543.tgz#51116ffc9fba1ee93514d6a40d34676aa6d7d1c4" @@ -490,6 +530,11 @@ has@^1.0.3: resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== +highcharts@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-11.1.0.tgz#715eb55fd081351b526e28cd89ac0e4e30b35c15" + integrity sha512-vhmqq6/frteWMx0GKYWwEFL25g4OYc7+m+9KQJb/notXbNtIb8KVy+ijOF7XAFqF165cq0pdLIePAmyFY5ph3g== + immutable@^4.0.0: version "4.3.4" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" @@ -701,6 +746,11 @@ rechoir@^0.8.0: dependencies: resolve "^1.20.0" +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"