diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 83610cf..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: -- package-ecosystem: bundler - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 10 -- package-ecosystem: github-actions - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 10 diff --git a/Gemfile b/Gemfile index 82fbb48..e2cf0d4 100644 --- a/Gemfile +++ b/Gemfile @@ -41,6 +41,8 @@ gem "kamal", require: false # Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] gem "thruster", require: false +gem "chartkick" + # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] gem "image_processing", "~> 1.2" diff --git a/Gemfile.lock b/Gemfile.lock index 223645a..3d3fe7b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,31 +1,31 @@ GEM remote: https://rubygems.org/ specs: - action_text-trix (2.1.15) + action_text-trix (2.1.16) railties - actioncable (8.1.1) - actionpack (= 8.1.1) - activesupport (= 8.1.1) + actioncable (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.1) - actionpack (= 8.1.1) - activejob (= 8.1.1) - activerecord (= 8.1.1) - activestorage (= 8.1.1) - activesupport (= 8.1.1) + actionmailbox (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) mail (>= 2.8.0) - actionmailer (8.1.1) - actionpack (= 8.1.1) - actionview (= 8.1.1) - activejob (= 8.1.1) - activesupport (= 8.1.1) + actionmailer (8.1.2) + actionpack (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activesupport (= 8.1.2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.1) - actionview (= 8.1.1) - activesupport (= 8.1.1) + actionpack (8.1.2) + actionview (= 8.1.2) + activesupport (= 8.1.2) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -33,36 +33,36 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.1) + actiontext (8.1.2) action_text-trix (~> 2.1.15) - actionpack (= 8.1.1) - activerecord (= 8.1.1) - activestorage (= 8.1.1) - activesupport (= 8.1.1) + actionpack (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.1) - activesupport (= 8.1.1) + actionview (8.1.2) + activesupport (= 8.1.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.1.1) - activesupport (= 8.1.1) + activejob (8.1.2) + activesupport (= 8.1.2) globalid (>= 0.3.6) - activemodel (8.1.1) - activesupport (= 8.1.1) - activerecord (8.1.1) - activemodel (= 8.1.1) - activesupport (= 8.1.1) + activemodel (8.1.2) + activesupport (= 8.1.2) + activerecord (8.1.2) + activemodel (= 8.1.2) + activesupport (= 8.1.2) timeout (>= 0.4.0) - activestorage (8.1.1) - actionpack (= 8.1.1) - activejob (= 8.1.1) - activerecord (= 8.1.1) - activesupport (= 8.1.1) + activestorage (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activesupport (= 8.1.2) marcel (~> 1.0) - activesupport (8.1.1) + activesupport (8.1.2) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -77,11 +77,11 @@ GEM uri (>= 0.13.1) ast (2.4.3) base64 (0.3.0) - bcrypt (3.1.20) + bcrypt (3.1.21) bcrypt_pbkdf (1.1.2) bigdecimal (4.0.1) bindex (0.8.1) - bootsnap (1.20.0) + bootsnap (1.21.1) msgpack (~> 1.2) brakeman (7.1.2) racc @@ -92,6 +92,7 @@ GEM bundler-audit (0.9.3) bundler (>= 1.2.0) thor (~> 1.0) + chartkick (5.2.1) concurrent-ruby (1.3.6) connection_pool (3.0.2) crass (1.0.6) @@ -119,9 +120,9 @@ GEM factory_bot_rails (6.5.1) factory_bot (~> 6.5) railties (>= 6.1.0) - faker (3.5.3) + faker (3.6.0) i18n (>= 1.8.11, < 2) - ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.3-x86_64-linux-gnu) fugit (1.12.1) et-orbi (~> 1.4) raabro (~> 1.4) @@ -132,7 +133,7 @@ GEM image_processing (1.14.0) mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) - importmap-rails (2.2.2) + importmap-rails (2.2.3) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) @@ -172,7 +173,7 @@ GEM mini_magick (5.3.1) logger mini_mime (1.1.5) - minitest (6.0.0) + minitest (6.0.1) prism (~> 1.5) msgpack (1.8.0) net-imap (0.6.2) @@ -190,19 +191,19 @@ GEM net-protocol net-ssh (7.3.0) nio4r (2.7.5) - nokogiri (1.18.10-x86_64-linux-gnu) + nokogiri (1.19.0-x86_64-linux-gnu) racc (~> 1.4) orm_adapter (0.5.0) ostruct (0.6.3) parallel (1.27.0) - parser (3.3.10.0) + parser (3.3.10.1) ast (~> 2.4.1) racc - pg (1.6.2-x86_64-linux) + pg (1.6.3-x86_64-linux) pp (0.6.3) prettyprint prettyprint (0.2.0) - prism (1.7.0) + prism (1.8.0) propshaft (1.3.1) actionpack (>= 7.0.0) activesupport (>= 7.0.0) @@ -210,7 +211,7 @@ GEM psych (5.3.1) date stringio - puma (7.1.0) + puma (7.2.0) nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) @@ -222,20 +223,20 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.1) - actioncable (= 8.1.1) - actionmailbox (= 8.1.1) - actionmailer (= 8.1.1) - actionpack (= 8.1.1) - actiontext (= 8.1.1) - actionview (= 8.1.1) - activejob (= 8.1.1) - activemodel (= 8.1.1) - activerecord (= 8.1.1) - activestorage (= 8.1.1) - activesupport (= 8.1.1) + rails (8.1.2) + actioncable (= 8.1.2) + actionmailbox (= 8.1.2) + actionmailer (= 8.1.2) + actionpack (= 8.1.2) + actiontext (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activemodel (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) bundler (>= 1.15.0) - railties (= 8.1.1) + railties (= 8.1.2) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -243,9 +244,9 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.1.1) - actionpack (= 8.1.1) - activesupport (= 8.1.1) + railties (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -254,9 +255,9 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.1) - rbs (3.10.0) + rbs (3.10.2) logger - rdoc (7.0.3) + rdoc (7.1.0) erb psych (>= 4.0.0) tsort @@ -294,14 +295,14 @@ GEM rubocop-ast (>= 1.48.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.48.0) + rubocop-ast (1.49.0) parser (>= 3.3.7.2) - prism (~> 1.4) + prism (~> 1.7) rubocop-performance (1.26.1) lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.47.1, < 2.0) - rubocop-rails (2.34.2) + rubocop-rails (2.34.3) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) @@ -311,10 +312,10 @@ GEM rubocop (>= 1.72) rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) - rubocop-rspec (3.8.0) + rubocop-rspec (3.9.0) lint_roller (~> 1.1) rubocop (~> 1.81) - ruby-lsp (0.26.4) + ruby-lsp (0.26.5) language_server-protocol (~> 3.17.0) prism (>= 1.2, < 2.0) rbs (>= 3, < 5) @@ -340,7 +341,7 @@ GEM activejob (>= 7.2) activerecord (>= 7.2) railties (>= 7.2) - solid_queue (1.2.4) + solid_queue (1.3.1) activejob (>= 7.1) activerecord (>= 7.1) concurrent-ruby (>= 1.3.1) @@ -361,11 +362,11 @@ GEM railties (>= 7.0.0) tailwindcss-ruby (~> 3.0) tailwindcss-ruby (3.4.19-x86_64-linux) - thor (1.4.0) + thor (1.5.0) thruster (0.1.17-x86_64-linux) timeout (0.6.0) tsort (0.2.0) - turbo-rails (2.0.20) + turbo-rails (2.0.21) actionpack (>= 7.1.0) railties (>= 7.1.0) tzinfo (2.0.6) @@ -397,6 +398,7 @@ DEPENDENCIES brakeman bullet (~> 8.0, >= 8.0.8) bundler-audit + chartkick debug devise (~> 4.9) factory_bot_rails (~> 6.4, >= 6.4.4) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 016af88..9e471ad 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,5 +1,89 @@ - class DashboardController < ApplicationController def index + @wallets = current_user.wallets.includes(:holdings, :transactions, :strategy) + + @total_wallets = @wallets.count + @active_wallets = @wallets.active.count + @inactive_wallets = @wallets.inactive.count + + @wallet_summaries = @wallets.map do |wallet| + { + wallet: wallet, + total_invested: calculate_total_invested(wallet), + current_value: calculate_current_value(wallet), + holdings_count: wallet.holdings.count, + transactions_count: wallet.transactions.count, + has_strategy: wallet.strategy.present? + } + end + + @total_invested = @wallet_summaries.sum { |ws| ws[:total_invested] } + @total_current_value = @wallet_summaries.sum { |ws| ws[:current_value] } + @total_profit_loss = @total_current_value - @total_invested + @total_profit_loss_percentage = @total_invested.zero? ? 0 : ((@total_profit_loss / @total_invested) * 100) + + @recent_transactions = current_user.wallets + .joins(:transactions) + .includes(transactions: :instrument) + .merge(Transaction.order(occurred_at: :desc)) + .limit(10) + .flat_map(&:transactions) + .first(10) + + + @wallet_distribution = @wallet_summaries.map do |summary| + [summary[:wallet].name, summary[:current_value].to_f] + end.to_h + + transactions_last_6_months = current_user.wallets + .joins(:transactions) + .where("transactions.occurred_at >= ?", 6.months.ago) + .pluck("transactions.occurred_at") + + @transactions_by_month = transactions_last_6_months + .compact + .group_by { |date| date.beginning_of_month.strftime("%b %Y") } + .transform_values(&:count) + .sort_by { |month, _| Date.parse("01 #{month}") } + .to_h + + @buy_vs_sell = { + "Buys" => current_user.wallets.joins(:transactions).merge(Transaction.buy).count, + "Sells" => current_user.wallets.joins(:transactions).merge(Transaction.sell).count + } + + holdings_with_instruments = current_user.wallets + .joins(holdings: :instrument) + .select("instruments.ticker, holdings.average_price, holdings.quantity") + + @top_instruments = holdings_with_instruments + .group_by(&:ticker) + .map { |ticker, holdings| + total = holdings.sum { |h| (h.average_price || 0) * (h.quantity || 0) } + [ticker, total.to_f] + } + .sort_by { |_, value| -value } + .first(5) + .to_h + + @wallet_comparison_data = @wallet_summaries.map do |summary| + [ + summary[:wallet].name, + { + "Invested" => summary[:total_invested].to_f, + "Current" => summary[:current_value].to_f + } + ] + end.to_h end -end + + private + + def calculate_total_invested(wallet) + wallet.transactions.buy.sum('price * quantity') + end + + def calculate_current_value(wallet) + wallet.holdings.sum('average_price * quantity') + end +end \ No newline at end of file diff --git a/app/javascript/application.js b/app/javascript/application.js index 0d7b494..3b51970 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,3 +1,4 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" import "controllers" +import "chartkick/chart.js" diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 464f78b..391f798 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -1 +1,327 @@ -
Wallets
+<%= @total_wallets %>
+Invested
++ <%= number_to_currency(@total_invested, unit: "R$ ", separator: ",", delimiter: ".") %> +
+Allocated capital
+Portfolio
++ <%= number_to_currency(@total_current_value, unit: "R$ ", separator: ",", delimiter: ".") %> +
+Market value
+Return
++ <%= number_to_currency(@total_profit_loss, unit: "R$ ", separator: ",", delimiter: ".") %> +
+ + <%= @total_profit_loss >= 0 ? "↑" : "↓" %> + <%= number_to_percentage(@total_profit_loss_percentage.abs, precision: 2, separator: ",", delimiter: ".") %> + +| Name | +Status | +Holdings | +Transactions | +Invested | +Current Value | +Strategy | +Actions | +
|---|---|---|---|---|---|---|---|
|
+
+
+
+ <%= wallet.name[0].upcase %>
+
+ <%= wallet.name %>
+ |
+ + + <%= wallet.status.titleize %> + + | ++ <%= summary[:holdings_count] %> + | ++ <%= summary[:transactions_count] %> + | ++ <%= number_to_currency(summary[:total_invested], unit: "R$ ", separator: ",", delimiter: ".") %> + | +
+
+ <%= number_to_currency(summary[:current_value], unit: "R$ ", separator: ",", delimiter: ".") %>
+
+ <% if profit_loss != 0 %>
+
+ <%= profit_loss >= 0 ? "+" : "" %><%= number_to_percentage(profit_percentage, precision: 1) %>
+
+ <% end %>
+ |
+ + <% if summary[:has_strategy] %> + + ✓ Yes + + <% else %> + + ✗ No + + <% end %> + | ++ <%= link_to wallet_path(wallet), class: "inline-flex items-center gap-1 bg-yellow-500 hover:bg-yellow-600 text-white text-xs font-bold py-2 px-4 rounded-lg shadow hover:shadow-lg transition-all duration-200" do %> + + View + <% end %> + | +
Get started by creating your first investment portfolio!
+ <%= link_to new_wallet_path, class: "inline-flex items-center gap-2 bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-xl transition-all duration-200" do %> + + Create first wallet + <% end %> +m&&(m=s);h=(d*h+e)/++d}else{_();t.lineTo(e,s);g=i;d=0;p=m=s}x=s}_()}function Zi(t){const e=t.options;const s=e.borderDash&&e.borderDash.length;const i=!t._decimated&&!t._loop&&!e.tension&&e.cubicInterpolationMode!=="monotone"&&!e.stepped&&!s;return i?qi:Ji}function Qi(t){return t.stepped?St:t.tension||t.cubicInterpolationMode==="monotone"?Dt:Pt}function tn(t,e,s,i){let n=e._path;if(!n){n=e._path=new Path2D;e.path(n,s,i)&&n.closePath()}Yi(t,e.options);t.stroke(n)}function en(t,e,s,i){const{segments:n,options:o}=e;const a=Zi(e);for(const r of n){Yi(t,o,r.style);t.beginPath();a(t,e,r,{start:s,end:s+i-1})&&t.closePath();t.stroke()}}const sn=typeof Path2D==="function";function nn(t,e,s,i){sn&&!e.options.segment?tn(t,e,s,i):en(t,e,s,i)}class LineElement extends Element{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:true,cubicInterpolationMode:"default",fill:false,spanGaps:false,stepped:false,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:true,_indexable:t=>t!=="borderDash"&&t!=="fill"};constructor(t){super();this.animated=true;this.options=void 0;this._chart=void 0;this._loop=void 0;this._fullLoop=void 0;this._path=void 0;this._points=void 0;this._segments=void 0;this._decimated=false;this._pointsUpdated=false;this._datasetIndex=void 0;t&&Object.assign(this,t)}updateControlPoints(t,e){const s=this.options;if((s.tension||s.cubicInterpolationMode==="monotone")&&!s.stepped&&!this._pointsUpdated){const i=s.spanGaps?this._loop:this._fullLoop;Ct(this._points,s,t,i,e);this._pointsUpdated=true}}set points(t){this._points=t;delete this._segments;delete this._path;this._pointsUpdated=false}get points(){return this._points}get segments(){return this._segments||(this._segments=At(this,this.options.segment))}first(){const t=this.segments;const e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments;const e=this.points;const s=t.length;return s&&e[t[s-1].end]}interpolate(t,e){const s=this.options;const i=t[e];const n=this.points;const o=Lt(this,{property:e,start:i,end:i});if(!o.length)return;const a=[];const r=Qi(s);let l,c;for(l=0,c=o.length;lt+e))/i.size;return{x:a,y:n/o}},nearest(t,e){if(!t.length)return false;let s=e.x;let i=e.y;let n=Number.POSITIVE_INFINITY;let o,a,r;for(o=0,a=t.length;o-1?t.split("\n"):t}function Do(t,e){const{element:s,datasetIndex:i,index:n}=e;const o=t.getDatasetMeta(i).controller;const{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[i].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:i,element:s}}function Po(t,e){const s=t.chart.ctx;const{body:i,footer:n,title:o}=t;const{boxWidth:a,boxHeight:r}=e;const l=j(e.bodyFont);const c=j(e.titleFont);const h=j(e.footerFont);const d=o.length;const u=n.length;const f=i.length;const g=R(e.padding);let p=g.height;let m=0;let x=i.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);x+=t.beforeBody.length+t.afterBody.length;d&&(p+=d*c.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom);if(x){const t=e.displayColors?Math.max(r,l.lineHeight):l.lineHeight;p+=f*t+(x-f)*l.lineHeight+(x-1)*e.bodySpacing}u&&(p+=e.footerMarginTop+u*h.lineHeight+(u-1)*e.footerSpacing);let b=0;const _=function(t){m=Math.max(m,s.measureText(t).width+b)};s.save();s.font=c.string;I(t.title,_);s.font=l.string;I(t.beforeBody.concat(t.afterBody),_);b=e.displayColors?a+2+e.boxPadding:0;I(i,(t=>{I(t.before,_);I(t.lines,_);I(t.after,_)}));b=0;s.font=h.string;I(t.footer,_);s.restore();m+=g.width;return{width:m,height:p}}function Co(t,e){const{y:s,height:i}=e;return st.height-i/2?"bottom":"center"}function Ao(t,e,s,i){const{x:n,width:o}=i;const a=s.caretSize+s.caretPadding;return t==="left"&&n+o+a>e.width||(t==="right"&&n-o-a<0||void 0)}function Lo(t,e,s,i){const{x:n,width:o}=s;const{width:a,chartArea:{left:r,right:l}}=t;let c="center";i==="center"?c=n<=(r+l)/2?"left":"right":n<=o/2?c="left":n>=a-o/2&&(c="right");Ao(c,t,e,s)&&(c="center");return c}function Eo(t,e,s){const i=s.yAlign||e.yAlign||Co(t,s);return{xAlign:s.xAlign||e.xAlign||Lo(t,e,s,i),yAlign:i}}function To(t,e){let{x:s,width:i}=t;e==="right"?s-=i:e==="center"&&(s-=i/2);return s}function Oo(t,e,s){let{y:i,height:n}=t;e==="top"?i+=s:i-=e==="bottom"?n+s:n/2;return i}function Ro(t,e,s,i){const{caretSize:n,caretPadding:o,cornerRadius:a}=t;const{xAlign:r,yAlign:l}=s;const c=n+o;const{topLeft:h,topRight:d,bottomLeft:u,bottomRight:f}=Ot(a);let g=To(e,r);const p=Oo(e,l,c);l==="center"?r==="left"?g+=c:r==="right"&&(g-=c):r==="left"?g-=Math.max(h,u)+n:r==="right"&&(g+=Math.max(d,f)+n);return{x:K(g,0,i.width-e.width),y:K(p,0,i.height-e.height)}}function Io(t,e,s){const i=R(s.padding);return e==="center"?t.x+t.width/2:e==="right"?t.x+t.width-i.right:t.x+i.left}function zo(t){return ko([],So(t))}function Fo(t,e,s){return c(t,{tooltip:e,tooltipItems:s,type:"tooltip"})}function Vo(t,e){const s=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return s?t.override(s):t}const Bo={beforeTitle:Ht,title(t){if(t.length>0){const e=t[0];const s=e.chart.data.labels;const i=s?s.length:0;if(this&&this.options&&this.options.mode==="dataset")return e.dataset.label||"";if(e.label)return e.label;if(i>0&&e.dataIndex{const e={before:[],lines:[],after:[]};const n=Vo(s,t);ko(e.before,So(No(n,"beforeLabel",this,t)));ko(e.lines,No(n,"label",this,t));ko(e.after,So(No(n,"afterLabel",this,t)));i.push(e)}));return i}getAfterBody(t,e){return zo(No(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:s}=e;const i=No(s,"beforeFooter",this,t);const n=No(s,"footer",this,t);const o=No(s,"afterFooter",this,t);let a=[];a=ko(a,So(i));a=ko(a,So(n));a=ko(a,So(o));return a}_createItems(t){const e=this._active;const s=this.chart.data;const i=[];const n=[];const o=[];let a=[];let r,l;for(r=0,l=e.length;r=10?d=d<15?15:20:d++;if(d>=20){o++;d=2;a=o>=0?1:a}u=Math.round((l+h+d*Math.pow(10,o))*a)/a}const f=Y(t.max,u);i.push({value:f,major:qo(f),significand:d});return i}class LogarithmicScale extends Scale{static id="logarithmic";static defaults={ticks:{callback:Gt.formatters.logarithmic,major:{enabled:true}}};constructor(t){super(t);this.start=void 0;this.end=void 0;this._startValue=void 0;this._valueRange=0}parse(t,e){const s=LinearScaleBase.prototype.parse.apply(this,[t,e]);if(s!==0)return r(s)&&s>0?s:null;this._zero=true}determineDataLimits(){const{min:t,max:e}=this.getMinMax(true);this.min=r(t)?Math.max(0,t):null;this.max=r(e)?Math.max(0,e):null;this.options.beginAtZero&&(this._zero=true);this._zero&&this.min!==this._suggestedMin&&!r(this._userMin)&&(this.min=t===Jo(this.min,0)?Jo(this.min,-1):Jo(this.min,0));this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let s=this.min;let i=this.max;const n=e=>s=t?s:e;const o=t=>i=e?i:t;if(s===i)if(s<=0){n(1);o(10)}else{n(Jo(s,-1));o(Jo(i,1))}s<=0&&n(Jo(i,-1));i<=0&&o(Jo(s,1));this.min=s;this.max=i}buildTicks(){const t=this.options;const e={min:this._userMin,max:this._userMax};const s=ta(e,this);t.bounds==="ticks"&&Xt(s,this,"value");if(t.reverse){s.reverse();this.start=this.max;this.end=this.min}else{this.start=this.min;this.end=this.max}return s}getLabelForValue(t){return t===void 0?"0":k(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure();this._startValue=Kt(t);this._valueRange=Kt(this.max)-Kt(t)}getPixelForValue(t){t!==void 0&&t!==0||(t=this.min);return t===null||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(Kt(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function ea(t){const e=t.ticks;if(e.display&&t.display){const t=R(e.backdropPadding);return h(e.font&&e.font.size,o.font.size)+t.height}return 0}function sa(t,e,s){s=a(s)?s:[s];return{w:Jt(t,e.string,s),h:s.length*e.lineHeight}}function ia(t,e,s,i,n){return t===i||t===n?{start:e-s/2,end:e+s/2}:tn?{start:e-s,end:e}:{start:e,end:e+s}}function na(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom};const s=Object.assign({},e);const i=[];const n=[];const o=t._pointLabels.length;const a=t.options.pointLabels;const r=a.centerPointLabels?y/o:0;for(let l=0;l=e?s[i]:s[n];t[o]=true}}else t[e]=true}function Sa(t,e,s,i){const n=t._adapter;const o=+n.startOf(e[0].value,i);const a=e[e.length-1].value;let r,l;for(r=o;r<=a;r=+n.add(r,1,i)){l=s[r];l>=0&&(e[l].major=true)}return e}function Da(t,e,s){const i=[];const n={};const o=e.length;let a,r;for(a=0;a+t))}getLabelForValue(t){const e=this._adapter;const s=this.options.time;return s.tooltipFormat?e.format(t,s.tooltipFormat):e.format(t,s.displayFormats.datetime)}format(t,e){const s=this.options;const i=s.time.displayFormats;const n=this._unit;const o=e||i[n];return this._adapter.format(t,o)}_tickFormatFunction(t,e,s,i){const n=this.options;const o=n.ticks.callback;if(o)return X(o,[t,e,s],this);const a=n.time.displayFormats;const r=this._unit;const l=this._majorUnit;const c=r&&a[r];const h=l&&a[l];const d=s[e];const u=l&&h&&d&&d.major;return this._adapter.format(t,i||(u?h:c))}generateTickLabels(t){let e,s,i;for(e=0,s=t.length;e0?a:1}getDataTimestamps(){let t=this._cache.data||[];let e,s;if(t.length)return t;const i=this.getMatchingVisibleMetas();if(this._normalized&&i.length)return this._cache.data=i[0].controller.getAllParsedValues(this);for(e=0,s=i.length;e=t[i].pos&&e<=t[n].pos&&({lo:i,hi:n}=L(t,"pos",e));({pos:o,time:r}=t[i]);({pos:a,time:l}=t[n])}else{e>=t[i].time&&e<=t[n].time&&({lo:i,hi:n}=L(t,"time",e));({time:o,pos:r}=t[i]);({time:a,pos:l}=t[n])}const c=a-o;return c?r+(l-r)*(e-o)/c:r}class TimeSeriesScale extends TimeScale{static id="timeseries";static defaults=TimeScale.defaults;constructor(t){super(t);this._table=[];this._minPos=void 0;this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable();const e=this._table=this.buildLookupTable(t);this._minPos=Pa(e,this.min);this._tableRange=Pa(e,this.max)-this._minPos;super.initOffsets(t)}buildLookupTable(t){const{min:e,max:s}=this;const i=[];const n=[];let o,a,r,l,c;for(o=0,a=t.length;o=e&&l<=s&&i.push(l)}if(i.length<2)return[{time:e,pos:0},{time:s,pos:1}];for(o=0,a=i.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps();const s=this.getLabelTimestamps();t=e.length&&s.length?this.normalize(e.concat(s)):e.length?e:s;t=this._cache.all=t;return t}getDecimalForValue(t){return(Pa(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets;const s=this.getDecimalForPixel(t)/e.factor-e.end;return Pa(this._table,s*this._tableRange+this._minPos,true)}}var Ca=Object.freeze({__proto__:null,CategoryScale:CategoryScale,LinearScale:LinearScale,LogarithmicScale:LogarithmicScale,RadialLinearScale:RadialLinearScale,TimeScale:TimeScale,TimeSeriesScale:TimeSeriesScale});const Aa=[Be,pn,Ho,Ca];export{Animation,Animations,ArcElement,BarController,BarElement,BasePlatform,BasicPlatform,BubbleController,CategoryScale,Chart,Pn as Colors,DatasetController,On as Decimation,DomPlatform,DoughnutController,Element,co as Filler,Je as Interaction,bo as Legend,LineController,LineElement,LinearScale,LogarithmicScale,PieController,PointElement,PolarAreaController,RadarController,RadialLinearScale,Scale,ScatterController,Mo as SubTitle,Gt as Ticks,TimeScale,TimeSeriesScale,yo as Title,Wo as Tooltip,We as _adapters,Os as _detectPlatform,Qt as animator,Be as controllers,o as defaults,pn as elements,fs as layouts,Ho as plugins,Aa as registerables,si as registry,Ca as scales};
+
diff --git a/vendor/javascript/chartkick.js b/vendor/javascript/chartkick.js
new file mode 100644
index 0000000..fc487cd
--- /dev/null
+++ b/vendor/javascript/chartkick.js
@@ -0,0 +1,4 @@
+// chartkick@5.0.1 downloaded from https://ga.jspm.io/npm:chartkick@5.0.1/dist/chartkick.esm.js
+
+function isArray(t){return"[object Array]"===Object.prototype.toString.call(t)}function isFunction(t){return t instanceof Function}function isPlainObject(t){return"[object Object]"===Object.prototype.toString.call(t)&&!isFunction(t)&&t instanceof Object}function extend(t,e){for(var r in e)if("__proto__"!==r)if(isPlainObject(e[r])||isArray(e[r])){isPlainObject(e[r])&&!isPlainObject(t[r])&&(t[r]={});isArray(e[r])&&!isArray(t[r])&&(t[r]=[]);extend(t[r],e[r])}else void 0!==e[r]&&(t[r]=e[r])}function merge(t,e){var r={};extend(r,t);extend(r,e);return r}var t=/^(\d\d\d\d)(?:-)?(\d\d)(?:-)?(\d\d)$/i;function negativeValues(t){for(var e=0;e
"})}var c=function defaultExport(t){this.name="highcharts";this.library=t};c.prototype.renderLineChart=function renderLineChart(t,e){e=e||"spline";var r={};"areaspline"===e&&(r={plotOptions:{areaspline:{stacking:"normal"},area:{stacking:"normal"},series:{marker:{enabled:false}}}});false===t.options.curve&&("areaspline"===e?e="area":"spline"===e&&(e="line"));var a=l(t,t.options,r);"number"===t.xtype?a.xAxis.type=a.xAxis.type||"linear":a.xAxis.type="string"===t.xtype?"category":"datetime";a.chart.type||(a.chart.type=e);setFormatOptions(t,a,e);var o=t.data;for(var n=0;n