diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 949a9b9d..9ae237ff 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -ARG RUBY_VERSION=3.4.1 +ARG RUBY_VERSION=3.4.4 FROM ruby:${RUBY_VERSION}-slim-bullseye RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ @@ -10,6 +10,8 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ imagemagick \ iproute2 \ libpq-dev \ + libyaml-dev \ + libyaml-0-2 \ openssh-client \ postgresql-client \ vim diff --git a/.ruby-version b/.ruby-version index 47b322c9..f9892605 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.1 +3.4.4 diff --git a/Dockerfile b/Dockerfile index d98833b3..2092d0ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax = docker/dockerfile:1 # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile -ARG RUBY_VERSION=3.4.1 +ARG RUBY_VERSION=3.4.4 FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here @@ -9,7 +9,7 @@ WORKDIR /rails # Install base packages RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libvips postgresql-client + apt-get install --no-install-recommends -y curl libvips postgresql-client libyaml-0-2 # Set production environment ARG BUILD_COMMIT_SHA @@ -23,7 +23,7 @@ ENV RAILS_ENV="production" \ FROM base AS build # Install packages needed to build gems -RUN apt-get install --no-install-recommends -y build-essential libpq-dev git pkg-config +RUN apt-get install --no-install-recommends -y build-essential libpq-dev git pkg-config libyaml-dev # Install application gems COPY .ruby-version Gemfile Gemfile.lock ./ diff --git a/Gemfile b/Gemfile index 26b29fc9..c06ca145 100644 --- a/Gemfile +++ b/Gemfile @@ -24,11 +24,11 @@ gem "stimulus-rails" gem "turbo-rails" gem "view_component" gem "lookbook", ">= 2.3.7" - gem "hotwire_combobox" # Background Jobs gem "sidekiq" +gem "sidekiq-cron" # Monitoring gem "vernier" @@ -44,6 +44,7 @@ gem "aws-sdk-s3", "~> 1.177.0", require: false gem "image_processing", ">= 1.2" # Other +gem "ostruct" gem "bcrypt", "~> 3.1" gem "jwt" gem "faraday" @@ -63,6 +64,10 @@ gem "rotp", "~> 6.3" gem "rqrcode", "~> 3.0" gem "activerecord-import" +# State machines +gem "aasm" +gem "after_commit_everywhere", "~> 1.0" + # AI gem "ruby-openai" diff --git a/Gemfile.lock b/Gemfile.lock index 224f8f9c..6596db18 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,6 +8,8 @@ GIT GEM remote: https://rubygems.org/ specs: + aasm (5.5.0) + concurrent-ruby (~> 1.0) actioncable (7.2.2.1) actionpack (= 7.2.2.1) activesupport (= 7.2.2.1) @@ -83,17 +85,20 @@ GEM tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) + after_commit_everywhere (1.6.0) + activerecord (>= 4.2) + activesupport ast (2.4.3) aws-eventstream (1.3.2) - aws-partitions (1.1093.0) - aws-sdk-core (3.222.3) + aws-partitions (1.1105.0) + aws-sdk-core (3.224.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.99.0) + aws-sdk-kms (1.101.0) aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) aws-sdk-s3 (1.177.0) @@ -115,7 +120,7 @@ GEM smart_properties bigdecimal (3.1.9) bindex (0.8.1) - bootsnap (1.18.4) + bootsnap (1.18.6) msgpack (~> 1.2) brakeman (7.0.2) racc @@ -139,6 +144,9 @@ GEM bigdecimal rexml crass (1.0.6) + cronex (0.15.0) + tzinfo + unicode (>= 0.4.4.5) css_parser (1.21.1) addressable csv (3.3.4) @@ -152,6 +160,7 @@ GEM dotenv (= 3.1.8) railties (>= 6.1) drb (2.2.1) + erb (5.0.1) erb_lint (0.9.0) activesupport better_html (>= 2.0.1) @@ -160,6 +169,8 @@ GEM rubocop (>= 1) smart_properties erubi (1.13.1) + et-orbi (1.2.11) + tzinfo event_stream_parser (1.0.0) faker (3.5.1) i18n (>= 1.8.11, < 2) @@ -182,6 +193,9 @@ GEM ffi (1.17.2-x86_64-linux-gnu) ffi (1.17.2-x86_64-linux-musl) foreman (0.88.1) + fugit (1.11.1) + et-orbi (~> 1, >= 1.2.11) + raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) hashdiff (1.1.2) @@ -230,10 +244,10 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) - json (2.11.3) + json (2.12.0) jwt (2.10.1) base64 - language_server-protocol (3.17.0.4) + language_server-protocol (3.17.0.5) launchy (3.1.1) addressable (~> 2.8) childprocess (~> 5.0) @@ -256,7 +270,7 @@ GEM logtail (~> 0.1, >= 0.1.14) logtail-rack (~> 0.1) railties (>= 5.0.0) - loofah (2.24.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) lookbook (2.3.9) @@ -290,7 +304,7 @@ GEM multipart-post (2.4.1) net-http (0.6.0) uri - net-imap (0.5.7) + net-imap (0.5.8) date net-protocol net-pop (0.1.2) @@ -319,13 +333,14 @@ GEM octokit (10.0.0) faraday (>= 1, < 3) sawyer (~> 0.9) + ostruct (0.6.1) pagy (9.3.4) parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) racc pg (1.5.9) - plaid (38.0.0) + plaid (39.0.0) faraday (>= 1.0.1, < 3.0) faraday-multipart (>= 1.0.1, < 2.0) platform_agent (1.0.1) @@ -340,17 +355,18 @@ GEM activesupport (>= 7.0.0) rack railties (>= 7.0.0) - psych (5.2.3) + psych (5.2.6) date stringio - public_suffix (6.0.1) + public_suffix (6.0.2) puma (6.6.0) nio4r (~> 2.0) + raabro (1.4.0) racc (1.8.1) - rack (3.1.13) + rack (3.1.15) rack-mini-profiler (3.3.1) rack (>= 1.2.0) - rack-session (2.1.0) + rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) @@ -397,9 +413,10 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rbs (3.9.2) + rbs (3.9.4) logger - rdoc (6.13.1) + rdoc (6.14.0) + erb psych (>= 4.0.0) redcarpet (3.6.1) redis (5.4.0) @@ -416,7 +433,7 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 2.0) rqrcode_core (2.0.0) - rubocop (1.75.4) + rubocop (1.75.6) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -434,23 +451,23 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rails (2.31.0) + rubocop-rails (2.32.0) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) rubocop-rails-omakase (1.1.0) rubocop (>= 1.72) rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) - ruby-lsp (0.23.16) + ruby-lsp (0.23.20) language_server-protocol (~> 3.17.0) prism (>= 1.2, < 2.0) rbs (>= 3, < 4) sorbet-runtime (>= 0.5.10782) - ruby-lsp-rails (0.4.2) - ruby-lsp (>= 0.23.16, < 0.24.0) + ruby-lsp-rails (0.4.3) + ruby-lsp (>= 0.23.18, < 0.24.0) ruby-openai (8.1.0) event_stream_parser (>= 0.3.0, < 2.0.0) faraday (>= 1) @@ -471,14 +488,14 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sentry-rails (5.23.0) + sentry-rails (5.24.0) railties (>= 5.0) - sentry-ruby (~> 5.23.0) - sentry-ruby (5.23.0) + sentry-ruby (~> 5.24.0) + sentry-ruby (5.24.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.23.0) - sentry-ruby (~> 5.23.0) + sentry-sidekiq (5.24.0) + sentry-ruby (~> 5.24.0) sidekiq (>= 3.0) sidekiq (8.0.3) connection_pool (>= 2.5.0) @@ -486,6 +503,11 @@ GEM logger (>= 1.6.2) rack (>= 3.1.0) redis-client (>= 0.23.2) + sidekiq-cron (2.3.0) + cronex (>= 0.13.0) + fugit (~> 1.8, >= 1.11.1) + globalid (>= 1.0.1) + sidekiq (>= 6.5.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -495,21 +517,21 @@ GEM skylight (6.0.4) activesupport (>= 5.2.0) smart_properties (1.17.0) - sorbet-runtime (0.5.12060) + sorbet-runtime (0.5.12117) stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.7) stripe (15.1.0) - tailwindcss-rails (4.2.2) + tailwindcss-rails (4.2.3) railties (>= 7.0.0) tailwindcss-ruby (~> 4.0) - tailwindcss-ruby (4.1.4) - tailwindcss-ruby (4.1.4-aarch64-linux-gnu) - tailwindcss-ruby (4.1.4-aarch64-linux-musl) - tailwindcss-ruby (4.1.4-arm64-darwin) - tailwindcss-ruby (4.1.4-x86_64-darwin) - tailwindcss-ruby (4.1.4-x86_64-linux-gnu) - tailwindcss-ruby (4.1.4-x86_64-linux-musl) + tailwindcss-ruby (4.1.7) + tailwindcss-ruby (4.1.7-aarch64-linux-gnu) + tailwindcss-ruby (4.1.7-aarch64-linux-musl) + tailwindcss-ruby (4.1.7-arm64-darwin) + tailwindcss-ruby (4.1.7-x86_64-darwin) + tailwindcss-ruby (4.1.7-x86_64-linux-gnu) + tailwindcss-ruby (4.1.7-x86_64-linux-musl) terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) thor (1.3.2) @@ -519,6 +541,7 @@ GEM railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unicode (0.4.4.5) unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) @@ -526,7 +549,7 @@ GEM useragent (0.16.11) vcr (6.3.1) base64 - vernier (1.7.0) + vernier (1.7.1) view_component (3.22.0) activesupport (>= 5.2.0, < 8.1) concurrent-ruby (= 1.3.4) @@ -548,7 +571,7 @@ GEM xpath (3.2.0) nokogiri (~> 1.8) yard (0.9.37) - zeitwerk (2.7.2) + zeitwerk (2.7.3) PLATFORMS aarch64-linux-gnu @@ -561,7 +584,9 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + aasm activerecord-import + after_commit_everywhere (~> 1.0) aws-sdk-s3 (~> 1.177.0) bcrypt (~> 3.1) benchmark-ips @@ -592,6 +617,7 @@ DEPENDENCIES lucide-rails! mocha octokit + ostruct pagy pg (~> 1.5) plaid @@ -612,6 +638,7 @@ DEPENDENCIES sentry-ruby sentry-sidekiq sidekiq + sidekiq-cron simplecov skylight stimulus-rails @@ -626,7 +653,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.4.1p0 + ruby 3.4.4p34 BUNDLED WITH - 2.6.3 + 2.6.9 diff --git a/app/assets/tailwind/maybe-design-system.css b/app/assets/tailwind/maybe-design-system.css index 8bf9c6c8..f9dc6039 100644 --- a/app/assets/tailwind/maybe-design-system.css +++ b/app/assets/tailwind/maybe-design-system.css @@ -240,7 +240,7 @@ 100% { stroke-dashoffset: 0; } - } + } } /* Specific override for strong tags in prose under dark mode */ @@ -429,5 +429,3 @@ } } - - diff --git a/app/assets/tailwind/maybe-design-system/background-utils.css b/app/assets/tailwind/maybe-design-system/background-utils.css index 0e4b85b8..2c57f18f 100644 --- a/app/assets/tailwind/maybe-design-system/background-utils.css +++ b/app/assets/tailwind/maybe-design-system/background-utils.css @@ -85,3 +85,7 @@ background-color: var(--color-alpha-black-900); } } + +@utility bg-loader { + @apply bg-surface-inset animate-pulse; +} diff --git a/app/components/tabs_component.html.erb b/app/components/tabs_component.html.erb index 4ec901fa..bfceddad 100644 --- a/app/components/tabs_component.html.erb +++ b/app/components/tabs_component.html.erb @@ -1,6 +1,7 @@ <%= tag.div data: { controller: "tabs", testid: testid, + tabs_session_key_value: session_key, tabs_url_param_key_value: url_param_key, tabs_nav_btn_active_class: active_btn_classes, tabs_nav_btn_inactive_class: inactive_btn_classes diff --git a/app/components/tabs_component.rb b/app/components/tabs_component.rb index 4017b308..747a9420 100644 --- a/app/components/tabs_component.rb +++ b/app/components/tabs_component.rb @@ -27,11 +27,12 @@ class TabsComponent < ViewComponent::Base } } - attr_reader :active_tab, :url_param_key, :variant, :testid + attr_reader :active_tab, :url_param_key, :session_key, :variant, :testid - def initialize(active_tab:, url_param_key: nil, variant: :default, active_btn_classes: "", inactive_btn_classes: "", testid: nil) + def initialize(active_tab:, url_param_key: nil, session_key: nil, variant: :default, active_btn_classes: "", inactive_btn_classes: "", testid: nil) @active_tab = active_tab @url_param_key = url_param_key + @session_key = session_key @variant = variant.to_sym @active_btn_classes = active_btn_classes @inactive_btn_classes = inactive_btn_classes diff --git a/app/components/tabs_controller.js b/app/components/tabs_controller.js index 43a4b192..259765aa 100644 --- a/app/components/tabs_controller.js +++ b/app/components/tabs_controller.js @@ -4,7 +4,7 @@ import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static classes = ["navBtnActive", "navBtnInactive"]; static targets = ["panel", "navBtn"]; - static values = { urlParamKey: String }; + static values = { sessionKey: String, urlParamKey: String }; show(e) { const btn = e.target.closest("button"); @@ -28,11 +28,30 @@ export default class extends Controller { } }); - // Update URL with the selected tab if (this.urlParamKeyValue) { const url = new URL(window.location.href); url.searchParams.set(this.urlParamKeyValue, selectedTabId); window.history.replaceState({}, "", url); } + + // Update URL with the selected tab + if (this.sessionKeyValue) { + this.#updateSessionPreference(selectedTabId); + } } + + #updateSessionPreference(selectedTabId) { + fetch("/current_session", { + method: "PUT", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-CSRF-Token": document.querySelector('[name="csrf-token"]').content, + Accept: "application/json", + }, + body: new URLSearchParams({ + "current_session[tab_key]": this.sessionKeyValue, + "current_session[tab_value]": selectedTabId, + }).toString(), + }); + } } diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index f003ab31..904be2b5 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -26,14 +26,6 @@ class AccountsController < ApplicationController render layout: false end - def sync_all - unless family.syncing? - family.sync_later - end - - redirect_back_or_to accounts_path - end - private def family Current.family diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a54dc088..260579d1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,8 @@ class ApplicationController < ActionController::Base - include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable, Breadcrumbable, FeatureGuardable, Notifiable + include RestoreLayoutPreferences, Onboardable, Localize, AutoSync, Authentication, Invitable, + SelfHostable, StoreLocation, Impersonatable, Breadcrumbable, + FeatureGuardable, Notifiable + include Pagy::Backend before_action :detect_os diff --git a/app/controllers/concerns/auto_sync.rb b/app/controllers/concerns/auto_sync.rb index e6ced672..15cdc557 100644 --- a/app/controllers/concerns/auto_sync.rb +++ b/app/controllers/concerns/auto_sync.rb @@ -13,6 +13,7 @@ module AutoSync def family_needs_auto_sync? return false unless Current.family&.accounts&.active&.any? return false if (Current.family.last_sync_created_at&.to_date || 1.day.ago) >= Date.current + return false unless Current.family.auto_sync_on_login Rails.logger.info "Auto-syncing family #{Current.family.id}, last sync was #{Current.family.last_sync_created_at}" diff --git a/app/controllers/concerns/notifiable.rb b/app/controllers/concerns/notifiable.rb index 0d8ea384..b1689f67 100644 --- a/app/controllers/concerns/notifiable.rb +++ b/app/controllers/concerns/notifiable.rb @@ -46,8 +46,6 @@ module Notifiable [ { partial: "shared/notifications/alert", locals: { message: data } } ] when "cta" [ resolve_cta(data) ] - when "loading" - [ { partial: "shared/notifications/loading", locals: { message: data } } ] when "notice" messages = Array(data) messages.map { |message| { partial: "shared/notifications/notice", locals: { message: message } } } diff --git a/app/controllers/concerns/restore_layout_preferences.rb b/app/controllers/concerns/restore_layout_preferences.rb new file mode 100644 index 00000000..284df4cc --- /dev/null +++ b/app/controllers/concerns/restore_layout_preferences.rb @@ -0,0 +1,24 @@ +module RestoreLayoutPreferences + extend ActiveSupport::Concern + + included do + before_action :restore_active_tabs + end + + private + def restore_active_tabs + last_selected_tab = Current.session&.get_preferred_tab("account_sidebar_tab") || "asset" + + @account_group_tab = account_group_tab_param || last_selected_tab + end + + def valid_account_group_tabs + %w[asset liability all] + end + + def account_group_tab_param + param_value = params[:account_sidebar_tab] + return nil unless param_value.in?(valid_account_group_tabs) + param_value + end +end diff --git a/app/controllers/cookie_sessions_controller.rb b/app/controllers/cookie_sessions_controller.rb new file mode 100644 index 00000000..7e76636f --- /dev/null +++ b/app/controllers/cookie_sessions_controller.rb @@ -0,0 +1,22 @@ +class CookieSessionsController < ApplicationController + def update + save_kv_to_session( + cookie_session_params[:tab_key], + cookie_session_params[:tab_value] + ) + + redirect_back_or_to root_path + end + + private + def cookie_session_params + params.require(:cookie_session).permit(:tab_key, :tab_value) + end + + def save_kv_to_session(key, value) + raise "Key must be a string" unless key.is_a?(String) + raise "Value must be a string" unless value.is_a?(String) + + session["custom_#{key}"] = value + end +end diff --git a/app/controllers/current_sessions_controller.rb b/app/controllers/current_sessions_controller.rb new file mode 100644 index 00000000..303b51f0 --- /dev/null +++ b/app/controllers/current_sessions_controller.rb @@ -0,0 +1,14 @@ +class CurrentSessionsController < ApplicationController + def update + if session_params[:tab_key].present? && session_params[:tab_value].present? + Current.session.set_preferred_tab(session_params[:tab_key], session_params[:tab_value]) + end + + head :ok + end + + private + def session_params + params.require(:current_session).permit(:tab_key, :tab_value) + end +end diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb index c1b51c23..20e5f9c4 100644 --- a/app/controllers/imports_controller.rb +++ b/app/controllers/imports_controller.rb @@ -5,6 +5,8 @@ class ImportsController < ApplicationController @import.publish_later redirect_to import_path(@import), notice: "Your import has started in the background." + rescue Import::MaxRowCountExceededError + redirect_back_or_to import_path(@import), alert: "Your import exceeds the maximum row count of #{@import.max_row_count}." end def index diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index c566d30e..1162483a 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -6,6 +6,23 @@ class PagesController < ApplicationController @balance_sheet = Current.family.balance_sheet @accounts = Current.family.accounts.active.with_attached_logo + period_param = params[:cashflow_period] + @cashflow_period = if period_param.present? + begin + Period.from_key(period_param) + rescue Period::InvalidKeyError + Period.last_30_days + end + else + Period.last_30_days + end + + family_currency = Current.family.currency + income_totals = Current.family.income_statement.income_totals(period: @cashflow_period) + expense_totals = Current.family.income_statement.expense_totals(period: @cashflow_period) + + @cashflow_sankey_data = build_cashflow_sankey_data(income_totals, expense_totals, family_currency) + @breadcrumbs = [ [ "Home", root_path ], [ "Dashboard", nil ] ] end @@ -31,4 +48,98 @@ class PagesController < ApplicationController def github_provider Provider::Registry.get_provider(:github) end + + def build_cashflow_sankey_data(income_totals, expense_totals, currency_symbol) + nodes = [] + links = [] + node_indices = {} # Memoize node indices by a unique key: "type_categoryid" + + # Helper to add/find node and return its index + add_node = ->(unique_key, display_name, value, percentage, color) { + node_indices[unique_key] ||= begin + nodes << { name: display_name, value: value.to_f.round(2), percentage: percentage.to_f.round(1), color: color } + nodes.size - 1 + end + } + + total_income_val = income_totals.total.to_f.round(2) + total_expense_val = expense_totals.total.to_f.round(2) + + # --- Create Central Cash Flow Node --- + cash_flow_idx = add_node.call("cash_flow_node", "Cash Flow", total_income_val, 0, "var(--color-success)") + + # --- Process Income Side --- + income_category_values = Hash.new(0.0) + income_totals.category_totals.each do |ct| + val = ct.total.to_f.round(2) + next if val.zero? || !ct.category.parent_id + income_category_values[ct.category.parent_id] += val + end + + income_totals.category_totals.each do |ct| + val = ct.total.to_f.round(2) + percentage_of_total_income = total_income_val.zero? ? 0 : (val / total_income_val * 100).round(1) + next if val.zero? + + node_display_name = ct.category.name + node_value_for_label = val + income_category_values[ct.category.id] # This sum is for parent node display + node_percentage_for_label = total_income_val.zero? ? 0 : (node_value_for_label / total_income_val * 100).round(1) + + node_color = ct.category.color.presence || Category::COLORS.sample + current_cat_idx = add_node.call("income_#{ct.category.id}", node_display_name, node_value_for_label, node_percentage_for_label, node_color) + + if ct.category.parent_id + parent_cat_idx = node_indices["income_#{ct.category.parent_id}"] + parent_cat_idx ||= add_node.call("income_#{ct.category.parent.id}", ct.category.parent.name, income_category_values[ct.category.parent.id], 0, ct.category.parent.color || Category::COLORS.sample) # Parent percentage will be recalc based on its total flow + links << { source: current_cat_idx, target: parent_cat_idx, value: val, color: node_color, percentage: percentage_of_total_income } + else + links << { source: current_cat_idx, target: cash_flow_idx, value: val, color: node_color, percentage: percentage_of_total_income } + end + end + + # --- Process Expense Side --- + expense_category_values = Hash.new(0.0) + expense_totals.category_totals.each do |ct| + val = ct.total.to_f.round(2) + next if val.zero? || !ct.category.parent_id + expense_category_values[ct.category.parent_id] += val + end + + expense_totals.category_totals.each do |ct| + val = ct.total.to_f.round(2) + percentage_of_total_expense = total_expense_val.zero? ? 0 : (val / total_expense_val * 100).round(1) + next if val.zero? + + node_display_name = ct.category.name + node_value_for_label = val + expense_category_values[ct.category.id] + node_percentage_for_label = total_expense_val.zero? ? 0 : (node_value_for_label / total_expense_val * 100).round(1) # Percentage relative to total expenses for expense nodes + + node_color = ct.category.color.presence || Category::UNCATEGORIZED_COLOR + current_cat_idx = add_node.call("expense_#{ct.category.id}", node_display_name, node_value_for_label, node_percentage_for_label, node_color) + + if ct.category.parent_id + parent_cat_idx = node_indices["expense_#{ct.category.parent_id}"] + parent_cat_idx ||= add_node.call("expense_#{ct.category.parent.id}", ct.category.parent.name, expense_category_values[ct.category.parent.id], 0, ct.category.parent.color || Category::UNCATEGORIZED_COLOR) + links << { source: parent_cat_idx, target: current_cat_idx, value: val, color: nodes[parent_cat_idx][:color], percentage: percentage_of_total_expense } + else + links << { source: cash_flow_idx, target: current_cat_idx, value: val, color: node_color, percentage: percentage_of_total_expense } + end + end + + # --- Process Surplus --- + leftover = (total_income_val - total_expense_val).round(2) + if leftover.positive? + percentage_of_total_income_for_surplus = total_income_val.zero? ? 0 : (leftover / total_income_val * 100).round(1) + surplus_idx = add_node.call("surplus_node", "Surplus", leftover, percentage_of_total_income_for_surplus, "var(--color-success)") + links << { source: cash_flow_idx, target: surplus_idx, value: leftover, color: "var(--color-success)", percentage: percentage_of_total_income_for_surplus } + end + + # Update Cash Flow and Income node percentages (relative to total income) + if node_indices["cash_flow_node"] + nodes[node_indices["cash_flow_node"]][:percentage] = 100.0 + end + # No primary income node anymore, percentages are on individual income cats relative to total_income_val + + { nodes: nodes, links: links, currency_symbol: Money::Currency.new(currency_symbol).symbol } + end end diff --git a/app/controllers/plaid_items_controller.rb b/app/controllers/plaid_items_controller.rb index 37efd5e3..8812cf0f 100644 --- a/app/controllers/plaid_items_controller.rb +++ b/app/controllers/plaid_items_controller.rb @@ -2,8 +2,8 @@ class PlaidItemsController < ApplicationController before_action :set_plaid_item, only: %i[destroy sync] def create - Current.family.plaid_items.create_from_public_token( - plaid_item_params[:public_token], + Current.family.create_plaid_item!( + public_token: plaid_item_params[:public_token], item_name: item_name, region: plaid_item_params[:region] ) diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index 4d47c06b..e5382e73 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -61,7 +61,7 @@ class TransactionsController < ApplicationController if @entry.save @entry.sync_account_later @entry.lock_saved_attributes! - @entry.transaction.lock!(:tag_ids) if @entry.transaction.tags.any? + @entry.transaction.lock_attr!(:tag_ids) if @entry.transaction.tags.any? flash[:notice] = "Transaction created" @@ -88,7 +88,7 @@ class TransactionsController < ApplicationController @entry.sync_account_later @entry.lock_saved_attributes! - @entry.transaction.lock!(:tag_ids) if @entry.transaction.tags.any? + @entry.transaction.lock_attr!(:tag_ids) if @entry.transaction.tags.any? respond_to do |format| format.html { redirect_back_or_to account_path(@entry.account), notice: "Transaction updated" } diff --git a/app/javascript/controllers/rule/conditions_controller.js b/app/javascript/controllers/rule/conditions_controller.js index d0c12941..1a20d00d 100644 --- a/app/javascript/controllers/rule/conditions_controller.js +++ b/app/javascript/controllers/rule/conditions_controller.js @@ -13,7 +13,7 @@ export default class extends Controller { addSubCondition() { const html = this.subConditionTemplateTarget.innerHTML.replaceAll( - "IDX_PLACEHOLDER", + "IDX_CHILD_PLACEHOLDER", this.#uniqueKey(), ); @@ -110,6 +110,6 @@ export default class extends Controller { } #uniqueKey() { - return Math.random().toString(36).substring(2, 15); + return Date.now(); } } diff --git a/app/javascript/controllers/sankey_chart_controller.js b/app/javascript/controllers/sankey_chart_controller.js new file mode 100644 index 00000000..9601b088 --- /dev/null +++ b/app/javascript/controllers/sankey_chart_controller.js @@ -0,0 +1,204 @@ +import { Controller } from "@hotwired/stimulus"; +import * as d3 from "d3"; +import { sankey, sankeyLinkHorizontal } from "d3-sankey"; + +// Connects to data-controller="sankey-chart" +export default class extends Controller { + static values = { + data: Object, + nodeWidth: { type: Number, default: 15 }, + nodePadding: { type: Number, default: 20 }, + currencySymbol: { type: String, default: "$" } + }; + + connect() { + this.resizeObserver = new ResizeObserver(() => this.#draw()); + this.resizeObserver.observe(this.element); + this.#draw(); + } + + disconnect() { + this.resizeObserver?.disconnect(); + } + + #draw() { + const { nodes = [], links = [] } = this.dataValue || {}; + + if (!nodes.length || !links.length) return; + + // Clear previous SVG + d3.select(this.element).selectAll("svg").remove(); + + const width = this.element.clientWidth || 600; + const height = this.element.clientHeight || 400; + + const svg = d3 + .select(this.element) + .append("svg") + .attr("width", width) + .attr("height", height); + + const sankeyGenerator = sankey() + .nodeWidth(this.nodeWidthValue) + .nodePadding(this.nodePaddingValue) + .extent([ + [16, 16], + [width - 16, height - 16], + ]); + + const sankeyData = sankeyGenerator({ + nodes: nodes.map((d) => Object.assign({}, d)), + links: links.map((d) => Object.assign({}, d)), + }); + + // Define gradients for links + const defs = svg.append("defs"); + + sankeyData.links.forEach((link, i) => { + const gradientId = `link-gradient-${link.source.index}-${link.target.index}-${i}`; + + const getStopColorWithOpacity = (nodeColorInput, opacity = 0.1) => { + let colorStr = nodeColorInput || "var(--color-gray-400)"; + if (colorStr === "var(--color-success)") { + colorStr = "#10A861"; // Hex for --color-green-600 + } + // Add other CSS var to hex mappings here if needed + + if (colorStr.startsWith("var(--")) { // Unmapped CSS var, use as is (likely solid) + return colorStr; + } + + const d3Color = d3.color(colorStr); + return d3Color ? d3Color.copy({ opacity: opacity }) : "var(--color-gray-400)"; + }; + + const sourceStopColor = getStopColorWithOpacity(link.source.color); + const targetStopColor = getStopColorWithOpacity(link.target.color); + + const gradient = defs.append("linearGradient") + .attr("id", gradientId) + .attr("gradientUnits", "userSpaceOnUse") + .attr("x1", link.source.x1) + .attr("x2", link.target.x0); + + gradient.append("stop") + .attr("offset", "0%") + .attr("stop-color", sourceStopColor); + + gradient.append("stop") + .attr("offset", "100%") + .attr("stop-color", targetStopColor); + }); + + // Draw links + svg + .append("g") + .attr("fill", "none") + .selectAll("path") + .data(sankeyData.links) + .join("path") + .attr("d", (d) => { + const sourceX = d.source.x1; + const targetX = d.target.x0; + const path = d3.linkHorizontal()({ + source: [sourceX, d.y0], + target: [targetX, d.y1] + }); + return path; + }) + .attr("stroke", (d, i) => `url(#link-gradient-${d.source.index}-${d.target.index}-${i})`) + .attr("stroke-width", (d) => Math.max(1, d.width)) + .append("title") + .text((d) => `${nodes[d.source.index].name} → ${nodes[d.target.index].name}: ${this.currencySymbolValue}${Number.parseFloat(d.value).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} (${d.percentage}%)`); + + // Draw nodes + const node = svg + .append("g") + .selectAll("g") + .data(sankeyData.nodes) + .join("g"); + + const cornerRadius = 8; + + node.append("path") + .attr("d", (d) => { + const x0 = d.x0; + const y0 = d.y0; + const x1 = d.x1; + const y1 = d.y1; + const h = y1 - y0; + // const w = x1 - x0; // Not directly used in path string, but good for context + + // Dynamic corner radius based on node height, maxed at 8 + const effectiveCornerRadius = Math.max(0, Math.min(cornerRadius, h / 2)); + + const isSourceNode = d.sourceLinks && d.sourceLinks.length > 0 && (!d.targetLinks || d.targetLinks.length === 0); + const isTargetNode = d.targetLinks && d.targetLinks.length > 0 && (!d.sourceLinks || d.sourceLinks.length === 0); + + if (isSourceNode) { // Round left corners, flat right for "Total Income" + if (h < effectiveCornerRadius * 2) { + return `M ${x0},${y0} L ${x1},${y0} L ${x1},${y1} L ${x0},${y1} Z`; + } + return `M ${x0 + effectiveCornerRadius},${y0} + L ${x1},${y0} + L ${x1},${y1} + L ${x0 + effectiveCornerRadius},${y1} + Q ${x0},${y1} ${x0},${y1 - effectiveCornerRadius} + L ${x0},${y0 + effectiveCornerRadius} + Q ${x0},${y0} ${x0 + effectiveCornerRadius},${y0} Z`; + } + + if (isTargetNode) { // Flat left corners, round right for Categories/Surplus + if (h < effectiveCornerRadius * 2) { + return `M ${x0},${y0} L ${x1},${y0} L ${x1},${y1} L ${x0},${y1} Z`; + } + return `M ${x0},${y0} + L ${x1 - effectiveCornerRadius},${y0} + Q ${x1},${y0} ${x1},${y0 + effectiveCornerRadius} + L ${x1},${y1 - effectiveCornerRadius} + Q ${x1},${y1} ${x1 - effectiveCornerRadius},${y1} + L ${x0},${y1} Z`; + } + + // Fallback for intermediate nodes (e.g., "Cash Flow") - draw as a simple sharp-cornered rectangle + return `M ${x0},${y0} L ${x1},${y0} L ${x1},${y1} L ${x0},${y1} Z`; + }) + .attr("fill", (d) => d.color || "var(--color-gray-400)") + .attr("stroke", (d) => { + // If a node has an explicit color assigned (even if it's a gray variable), + // it gets no stroke. Only truly un-colored nodes (falling back to default fill) + // would get a stroke, but our current data structure assigns colors to all nodes. + if (d.color) { + return "none"; + } + return "var(--color-gray-500)"; // Fallback, likely unused with current data + }); + + const stimulusControllerInstance = this; + node + .append("text") + .attr("x", (d) => (d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)) + .attr("y", (d) => (d.y1 + d.y0) / 2) + .attr("dy", "-0.2em") + .attr("text-anchor", (d) => (d.x0 < width / 2 ? "start" : "end")) + .attr("class", "text-xs font-medium text-primary fill-current") + .each(function (d) { + const textElement = d3.select(this); + textElement.selectAll("tspan").remove(); + + // Node Name on the first line + textElement.append("tspan") + .text(d.name); + + // Financial details on the second line + const financialDetailsTspan = textElement.append("tspan") + .attr("x", textElement.attr("x")) + .attr("dy", "1.2em") + .attr("class", "font-mono text-secondary") + .style("font-size", "0.65rem"); // Explicitly set smaller font size + + financialDetailsTspan.append("tspan") + .text(stimulusControllerInstance.currencySymbolValue + Number.parseFloat(d.value).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })); + }); + } +} \ No newline at end of file diff --git a/app/javascript/controllers/sidebar_tabs_controller.js b/app/javascript/controllers/sidebar_tabs_controller.js deleted file mode 100644 index f88a70f7..00000000 --- a/app/javascript/controllers/sidebar_tabs_controller.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Controller } from "@hotwired/stimulus"; - -// Connects to data-controller="sidebar-tabs" -export default class extends Controller { - static targets = ["account"]; - - select(event) { - this.accountTargets.forEach((account) => { - if (account.contains(event.target)) { - account.classList.add("bg-container"); - } else { - account.classList.remove("bg-container"); - } - }); - } -} diff --git a/app/javascript/shims/d3-array-default.js b/app/javascript/shims/d3-array-default.js new file mode 100644 index 00000000..1b1e088e --- /dev/null +++ b/app/javascript/shims/d3-array-default.js @@ -0,0 +1,3 @@ +import * as d3Array from "d3-array-src"; +export * from "d3-array-src"; +export default d3Array; \ No newline at end of file diff --git a/app/javascript/shims/d3-shape-default.js b/app/javascript/shims/d3-shape-default.js new file mode 100644 index 00000000..23920eda --- /dev/null +++ b/app/javascript/shims/d3-shape-default.js @@ -0,0 +1,3 @@ +import * as d3Shape from "d3-shape-src"; +export * from "d3-shape-src"; +export default d3Shape; \ No newline at end of file diff --git a/app/jobs/fetch_security_info_job.rb b/app/jobs/fetch_security_info_job.rb deleted file mode 100644 index e789222f..00000000 --- a/app/jobs/fetch_security_info_job.rb +++ /dev/null @@ -1,21 +0,0 @@ -class FetchSecurityInfoJob < ApplicationJob - queue_as :low_priority - - def perform(security_id) - return unless Security.provider.present? - - security = Security.find(security_id) - - params = { - ticker: security.ticker - } - params[:mic_code] = security.exchange_mic if security.exchange_mic.present? - params[:operating_mic] = security.exchange_operating_mic if security.exchange_operating_mic.present? - - security_info_response = Security.provider.fetch_security_info(**params) - - security.update( - name: security_info_response.info.dig("name") - ) - end -end diff --git a/app/jobs/import_market_data_job.rb b/app/jobs/import_market_data_job.rb new file mode 100644 index 00000000..294d5836 --- /dev/null +++ b/app/jobs/import_market_data_job.rb @@ -0,0 +1,20 @@ +# This job runs daily at market close. See config/schedule.yml for details. +# +# The primary purpose of this job is to: +# 1. Determine what exchange rate pairs, security prices, and other market data all of our users need to view historical account balance data +# 2. For each needed rate/price, fetch from our data provider and upsert to our database +# +# Each individual account sync will still fetch any missing market data that isn't yet synced, but by running +# this job daily, we significantly reduce overlapping account syncs that both need the same market data (e.g. common security like `AAPL`) +# +class ImportMarketDataJob < ApplicationJob + queue_as :scheduled + + def perform(opts) + opts = opts.symbolize_keys + mode = opts.fetch(:mode, :full) + clear_cache = opts.fetch(:clear_cache, false) + + MarketDataImporter.new(mode: mode, clear_cache: clear_cache).import_all + end +end diff --git a/app/jobs/sync_cleaner_job.rb b/app/jobs/sync_cleaner_job.rb new file mode 100644 index 00000000..f5e42551 --- /dev/null +++ b/app/jobs/sync_cleaner_job.rb @@ -0,0 +1,7 @@ +class SyncCleanerJob < ApplicationJob + queue_as :scheduled + + def perform + Sync.clean + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 352335e0..4984fb89 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -1,5 +1,5 @@ class Account < ApplicationRecord - include Syncable, Monetizable, Chartable, Linkable, Convertible, Enrichable + include Syncable, Monetizable, Chartable, Linkable, Enrichable validates :name, :balance, :currency, presence: true @@ -61,6 +61,18 @@ class Account < ApplicationRecord end end + def syncing? + self_syncing = syncs.visible.any? + + # Since Plaid Items sync as a "group", if the item is syncing, even if the account + # sync hasn't yet started (i.e. we're still fetching the Plaid data), show it as syncing in UI. + if linked? + plaid_account&.plaid_item&.syncing? || self_syncing + else + self_syncing + end + end + def institution_domain url_string = plaid_account&.plaid_item&.institution_url return nil unless url_string.present? @@ -81,21 +93,6 @@ class Account < ApplicationRecord DestroyJob.perform_later(self) end - def sync_data(sync, start_date: nil) - Rails.logger.info("Processing balances (#{linked? ? 'reverse' : 'forward'})") - sync_balances - end - - def post_sync(sync) - family.remove_syncing_notice! - - accountable.post_sync(sync) - - unless sync.child? - family.auto_match_transfers! - end - end - def current_holdings holdings.where(currency: currency, date: holdings.maximum(:date)).order(amount: :desc) end @@ -172,10 +169,4 @@ class Account < ApplicationRecord def long_subtype_label accountable_class.long_subtype_label_for(subtype) || accountable_class.display_name end - - private - def sync_balances - strategy = linked? ? :reverse : :forward - Balance::Syncer.new(self, strategy: strategy).sync_balances - end end diff --git a/app/models/account/convertible.rb b/app/models/account/convertible.rb deleted file mode 100644 index fde6fa10..00000000 --- a/app/models/account/convertible.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Account::Convertible - extend ActiveSupport::Concern - - def sync_required_exchange_rates - unless requires_exchange_rates? - Rails.logger.info("No exchange rate sync needed for account #{id}") - return - end - - affected_row_count = ExchangeRate.sync_provider_rates( - from: currency, - to: target_currency, - start_date: start_date, - ) - - Rails.logger.info("Synced #{affected_row_count} exchange rates for account #{id}") - end - - private - def target_currency - family.currency - end - - def requires_exchange_rates? - currency != target_currency - end -end diff --git a/app/models/account/market_data_importer.rb b/app/models/account/market_data_importer.rb new file mode 100644 index 00000000..af538314 --- /dev/null +++ b/app/models/account/market_data_importer.rb @@ -0,0 +1,82 @@ +class Account::MarketDataImporter + attr_reader :account + + def initialize(account) + @account = account + end + + def import_all + import_exchange_rates + import_security_prices + end + + def import_exchange_rates + return unless needs_exchange_rates? + return unless ExchangeRate.provider + + pair_dates = {} + + # 1. ENTRY-BASED PAIRS – currencies that differ from the account currency + account.entries + .where.not(currency: account.currency) + .group(:currency) + .minimum(:date) + .each do |source_currency, date| + key = [ source_currency, account.currency ] + pair_dates[key] = [ pair_dates[key], date ].compact.min + end + + # 2. ACCOUNT-BASED PAIR – convert the account currency to the family currency (if different) + if foreign_account? + key = [ account.currency, account.family.currency ] + pair_dates[key] = [ pair_dates[key], account.start_date ].compact.min + end + + pair_dates.each do |(source, target), start_date| + ExchangeRate.import_provider_rates( + from: source, + to: target, + start_date: start_date, + end_date: Date.current + ) + end + end + + def import_security_prices + return unless Security.provider + + account_securities = account.trades.map(&:security).uniq + + return if account_securities.empty? + + account_securities.each do |security| + security.import_provider_prices( + start_date: first_required_price_date(security), + end_date: Date.current + ) + + security.import_provider_details + end + end + + private + # Calculates the first date we require a price for the given security scoped to this account + def first_required_price_date(security) + account.trades.with_entry + .where(security: security) + .where(entries: { account_id: account.id }) + .minimum("entries.date") + end + + def needs_exchange_rates? + has_multi_currency_entries? || foreign_account? + end + + def has_multi_currency_entries? + account.entries.where.not(currency: account.currency).exists? + end + + def foreign_account? + account.currency != account.family.currency + end +end diff --git a/app/models/account/sync_complete_event.rb b/app/models/account/sync_complete_event.rb new file mode 100644 index 00000000..32315375 --- /dev/null +++ b/app/models/account/sync_complete_event.rb @@ -0,0 +1,63 @@ +class Account::SyncCompleteEvent + attr_reader :account + + Error = Class.new(StandardError) + + def initialize(account) + @account = account + end + + def broadcast + # Replace account row in accounts list + account.broadcast_replace_to( + account.family, + target: "account_#{account.id}", + partial: "accounts/account", + locals: { account: account } + ) + + # Replace the groups this account belongs to in the sidebar + account_group_ids.each do |id| + account.broadcast_replace_to( + account.family, + target: id, + partial: "accounts/accountable_group", + locals: { account_group: account_group, open: true } + ) + end + + # If this is a manual, unlinked account (i.e. not part of a Plaid Item), + # trigger the family sync complete broadcast so net worth graph is updated + unless account.linked? + account.family.broadcast_sync_complete + end + + # Refresh entire account page (only applies if currently viewing this account) + account.broadcast_refresh + end + + private + # The sidebar will show the account in both its classification tab and the "all" tab, + # so we need to broadcast to both. + def account_group_ids + unless account_group.present? + error = Error.new("Account #{account.id} is not part of an account group") + Rails.logger.warn(error.message) + Sentry.capture_exception(error, level: :warning) + return [] + end + + id = account_group.id + [ id, "#{account_group.classification}_#{id}" ] + end + + def account_group + family_balance_sheet.account_groups.find do |group| + group.accounts.any? { |a| a.id == account.id } + end + end + + def family_balance_sheet + account.family.balance_sheet + end +end diff --git a/app/models/account/syncer.rb b/app/models/account/syncer.rb new file mode 100644 index 00000000..ab198a95 --- /dev/null +++ b/app/models/account/syncer.rb @@ -0,0 +1,37 @@ +class Account::Syncer + attr_reader :account + + def initialize(account) + @account = account + end + + def perform_sync(sync) + Rails.logger.info("Processing balances (#{account.linked? ? 'reverse' : 'forward'})") + import_market_data + materialize_balances + end + + def perform_post_sync + account.family.auto_match_transfers! + end + + private + def materialize_balances + strategy = account.linked? ? :reverse : :forward + Balance::Materializer.new(account, strategy: strategy).materialize_balances + end + + # Syncs all the exchange rates + security prices this account needs to display historical chart data + # + # This is a *supplemental* sync. The daily market data sync should have already populated + # a majority or all of this data, so this is often a no-op. + # + # We rescue errors here because if this operation fails, we don't want to fail the entire sync since + # we have reasonable fallbacks for missing market data. + def import_market_data + Account::MarketDataImporter.new(account).import_all + rescue => e + Rails.logger.error("Error syncing market data for account #{account.id}: #{e.message}") + Sentry.capture_exception(e) + end +end diff --git a/app/models/account_import.rb b/app/models/account_import.rb index 96fdfd47..aa4c6dfe 100644 --- a/app/models/account_import.rb +++ b/app/models/account_import.rb @@ -54,4 +54,8 @@ class AccountImport < Import CSV.parse(template, headers: true) end + + def max_row_count + 50 + end end diff --git a/app/models/balance/base_calculator.rb b/app/models/balance/base_calculator.rb deleted file mode 100644 index 2d01dfe7..00000000 --- a/app/models/balance/base_calculator.rb +++ /dev/null @@ -1,35 +0,0 @@ -class Balance::BaseCalculator - attr_reader :account - - def initialize(account) - @account = account - end - - def calculate - Rails.logger.tagged(self.class.name) do - calculate_balances - end - end - - private - def sync_cache - @sync_cache ||= Balance::SyncCache.new(account) - end - - def build_balance(date, cash_balance, holdings_value) - Balance.new( - account_id: account.id, - date: date, - balance: holdings_value + cash_balance, - cash_balance: cash_balance, - currency: account.currency - ) - end - - def calculate_next_balance(prior_balance, transactions, direction: :forward) - flows = transactions.sum(&:amount) - negated = direction == :forward ? account.asset? : account.liability? - flows *= -1 if negated - prior_balance + flows - end -end diff --git a/app/models/balance/forward_calculator.rb b/app/models/balance/forward_calculator.rb index d024d2c6..4e6f2d5c 100644 --- a/app/models/balance/forward_calculator.rb +++ b/app/models/balance/forward_calculator.rb @@ -1,4 +1,16 @@ -class Balance::ForwardCalculator < Balance::BaseCalculator +class Balance::ForwardCalculator + attr_reader :account + + def initialize(account) + @account = account + end + + def calculate + Rails.logger.tagged("Balance::ForwardCalculator") do + calculate_balances + end + end + private def calculate_balances current_cash_balance = 0 @@ -25,4 +37,25 @@ class Balance::ForwardCalculator < Balance::BaseCalculator @balances end + + def sync_cache + @sync_cache ||= Balance::SyncCache.new(account) + end + + def build_balance(date, cash_balance, holdings_value) + Balance.new( + account_id: account.id, + date: date, + balance: holdings_value + cash_balance, + cash_balance: cash_balance, + currency: account.currency + ) + end + + def calculate_next_balance(prior_balance, transactions, direction: :forward) + flows = transactions.sum(&:amount) + negated = direction == :forward ? account.asset? : account.liability? + flows *= -1 if negated + prior_balance + flows + end end diff --git a/app/models/balance/syncer.rb b/app/models/balance/materializer.rb similarity index 88% rename from app/models/balance/syncer.rb rename to app/models/balance/materializer.rb index 362b87aa..75a98ffd 100644 --- a/app/models/balance/syncer.rb +++ b/app/models/balance/materializer.rb @@ -1,4 +1,4 @@ -class Balance::Syncer +class Balance::Materializer attr_reader :account, :strategy def initialize(account, strategy:) @@ -6,9 +6,9 @@ class Balance::Syncer @strategy = strategy end - def sync_balances + def materialize_balances Balance.transaction do - sync_holdings + materialize_holdings calculate_balances Rails.logger.info("Persisting #{@balances.size} balances") @@ -19,14 +19,12 @@ class Balance::Syncer if strategy == :forward update_account_info end - - account.sync_required_exchange_rates end end private - def sync_holdings - @holdings = Holding::Syncer.new(account, strategy: strategy).sync_holdings + def materialize_holdings + @holdings = Holding::Materializer.new(account, strategy: strategy).materialize_holdings end def update_account_info diff --git a/app/models/balance/reverse_calculator.rb b/app/models/balance/reverse_calculator.rb index 6a2ba70b..52a05608 100644 --- a/app/models/balance/reverse_calculator.rb +++ b/app/models/balance/reverse_calculator.rb @@ -1,4 +1,16 @@ -class Balance::ReverseCalculator < Balance::BaseCalculator +class Balance::ReverseCalculator + attr_reader :account + + def initialize(account) + @account = account + end + + def calculate + Rails.logger.tagged("Balance::ReverseCalculator") do + calculate_balances + end + end + private def calculate_balances current_cash_balance = account.cash_balance @@ -35,4 +47,25 @@ class Balance::ReverseCalculator < Balance::BaseCalculator @balances end + + def sync_cache + @sync_cache ||= Balance::SyncCache.new(account) + end + + def build_balance(date, cash_balance, holdings_value) + Balance.new( + account_id: account.id, + date: date, + balance: holdings_value + cash_balance, + cash_balance: cash_balance, + currency: account.currency + ) + end + + def calculate_next_balance(prior_balance, transactions, direction: :forward) + flows = transactions.sum(&:amount) + negated = direction == :forward ? account.asset? : account.liability? + flows *= -1 if negated + prior_balance + flows + end end diff --git a/app/models/balance_sheet.rb b/app/models/balance_sheet.rb index c289f86f..a89a9859 100644 --- a/app/models/balance_sheet.rb +++ b/app/models/balance_sheet.rb @@ -22,20 +22,25 @@ class BalanceSheet end def classification_groups + asset_groups = account_groups("asset") + liability_groups = account_groups("liability") + [ ClassificationGroup.new( key: "asset", display_name: "Assets", icon: "plus", total_money: total_assets_money, - account_groups: account_groups("asset") + account_groups: asset_groups, + syncing?: asset_groups.any?(&:syncing?) ), ClassificationGroup.new( key: "liability", display_name: "Debts", icon: "minus", total_money: total_liabilities_money, - account_groups: account_groups("liability") + account_groups: liability_groups, + syncing?: liability_groups.any?(&:syncing?) ) ] end @@ -43,13 +48,17 @@ class BalanceSheet def account_groups(classification = nil) classification_accounts = classification ? totals_query.filter { |t| t.classification == classification } : totals_query classification_total = classification_accounts.sum(&:converted_balance) - account_groups = classification_accounts.group_by(&:accountable_type).transform_keys { |k| Accountable.from_type(k) } + account_groups = classification_accounts.group_by(&:accountable_type) + .transform_keys { |k| Accountable.from_type(k) } - account_groups.map do |accountable, accounts| + groups = account_groups.map do |accountable, accounts| group_total = accounts.sum(&:converted_balance) + key = accountable.model_name.param_key + AccountGroup.new( - key: accountable.model_name.param_key, + id: classification ? "#{classification}_#{key}_group" : "#{key}_group", + key: key, name: accountable.display_name, classification: accountable.classification, total: group_total, @@ -57,6 +66,7 @@ class BalanceSheet weight: classification_total.zero? ? 0 : group_total / classification_total.to_d * 100, missing_rates?: accounts.any? { |a| a.missing_rates? }, color: accountable.color, + syncing?: accounts.any?(&:is_syncing), accounts: accounts.map do |account| account.define_singleton_method(:weight) do classification_total.zero? ? 0 : account.converted_balance / classification_total.to_d * 100 @@ -65,7 +75,13 @@ class BalanceSheet account end.sort_by(&:weight).reverse ) - end.sort_by(&:weight).reverse + end + + groups.sort_by do |group| + manual_order = Accountable::TYPES + type_name = group.key.camelize + manual_order.index(type_name) || Float::INFINITY + end end def net_worth_series(period: Period.last_30_days) @@ -76,9 +92,13 @@ class BalanceSheet family.currency end + def syncing? + classification_groups.any? { |group| group.syncing? } + end + private - ClassificationGroup = Struct.new(:key, :display_name, :icon, :total_money, :account_groups, keyword_init: true) - AccountGroup = Struct.new(:key, :name, :accountable_type, :classification, :total, :total_money, :weight, :accounts, :color, :missing_rates?, keyword_init: true) + ClassificationGroup = Struct.new(:key, :display_name, :icon, :total_money, :account_groups, :syncing?, keyword_init: true) + AccountGroup = Struct.new(:id, :key, :name, :accountable_type, :classification, :total, :total_money, :weight, :accounts, :color, :missing_rates?, :syncing?, keyword_init: true) def active_accounts family.accounts.active.with_attached_logo @@ -87,9 +107,15 @@ class BalanceSheet def totals_query @totals_query ||= active_accounts .joins(ActiveRecord::Base.sanitize_sql_array([ "LEFT JOIN exchange_rates ON exchange_rates.date = CURRENT_DATE AND accounts.currency = exchange_rates.from_currency AND exchange_rates.to_currency = ?", currency ])) + .joins(ActiveRecord::Base.sanitize_sql_array([ + "LEFT JOIN syncs ON syncs.syncable_id = accounts.id AND syncs.syncable_type = 'Account' AND syncs.status IN (?) AND syncs.created_at > ?", + %w[pending syncing], + Sync::VISIBLE_FOR.ago + ])) .select( "accounts.*", "SUM(accounts.balance * COALESCE(exchange_rates.rate, 1)) as converted_balance", + "COUNT(syncs.id) > 0 as is_syncing", ActiveRecord::Base.sanitize_sql_array([ "COUNT(CASE WHEN accounts.currency <> ? AND exchange_rates.rate IS NULL THEN 1 END) as missing_rates", currency ]) ) .group(:classification, :accountable_type, :id) diff --git a/app/models/concerns/accountable.rb b/app/models/concerns/accountable.rb index 9d0ecb34..12ee9888 100644 --- a/app/models/concerns/accountable.rb +++ b/app/models/concerns/accountable.rb @@ -68,15 +68,6 @@ module Accountable end end - def post_sync(sync) - broadcast_replace_to( - account, - target: "chart_account_#{account.id}", - partial: "accounts/show/chart", - locals: { account: account } - ) - end - def display_name self.class.display_name end diff --git a/app/models/concerns/enrichable.rb b/app/models/concerns/enrichable.rb index e5804786..4c373b01 100644 --- a/app/models/concerns/enrichable.rb +++ b/app/models/concerns/enrichable.rb @@ -22,16 +22,23 @@ module Enrichable } end - def log_enrichment!(attribute_name:, attribute_value:, source:, metadata: {}) - de = DataEnrichment.find_or_create_by!( - enrichable: self, - attribute_name: attribute_name, - source: source, - ) + # Convenience method for a single attribute + def enrich_attribute(attr, value, source:, metadata: {}) + enrich_attributes({ attr => value }, source:, metadata:) + end - de.value = attribute_value - de.metadata = metadata - de.save! + # Enriches all attributes that haven't been locked yet + def enrich_attributes(attrs, source:, metadata: {}) + enrichable_attrs = Array(attrs).reject { |k, _v| locked?(k) } + + ActiveRecord::Base.transaction do + enrichable_attrs.each do |attr, value| + self.send("#{attr}=", value) + log_enrichment(attribute_name: attr, attribute_value: value, source: source, metadata: metadata) + end + + save + end end def locked?(attr) @@ -42,21 +49,33 @@ module Enrichable !locked?(attr) end - def lock!(attr) + def lock_attr!(attr) update!(locked_attributes: locked_attributes.merge(attr.to_s => Time.current)) end - def unlock!(attr) + def unlock_attr!(attr) update!(locked_attributes: locked_attributes.except(attr.to_s)) end def lock_saved_attributes! saved_changes.keys.reject { |attr| ignored_enrichable_attributes.include?(attr) }.each do |attr| - lock!(attr) + lock_attr!(attr) end end private + def log_enrichment(attribute_name:, attribute_value:, source:, metadata: {}) + de = DataEnrichment.find_or_create_by( + enrichable: self, + attribute_name: attribute_name, + source: source, + ) + + de.value = attribute_value + de.metadata = metadata + de.save + end + def ignored_enrichable_attributes %w[id updated_at created_at] end diff --git a/app/models/concerns/syncable.rb b/app/models/concerns/syncable.rb index ce3c30fd..72556bf7 100644 --- a/app/models/concerns/syncable.rb +++ b/app/models/concerns/syncable.rb @@ -6,24 +6,44 @@ module Syncable end def syncing? - syncs.where(status: [ :syncing, :pending ]).any? + raise NotImplementedError, "Subclasses must implement the syncing? method" end - def sync_later(start_date: nil, parent_sync: nil) - new_sync = syncs.create!(start_date: start_date, parent: parent_sync) - SyncJob.perform_later(new_sync) + # Schedules a sync for syncable. If there is an existing sync pending/syncing for this syncable, + # we do not create a new sync, and attempt to expand the sync window if needed. + def sync_later(parent_sync: nil, window_start_date: nil, window_end_date: nil) + Sync.transaction do + with_lock do + sync = self.syncs.incomplete.first + + if sync + Rails.logger.info("There is an existing sync, expanding window if needed (#{sync.id})") + sync.expand_window_if_needed(window_start_date, window_end_date) + else + sync = self.syncs.create!( + parent: parent_sync, + window_start_date: window_start_date, + window_end_date: window_end_date + ) + + SyncJob.perform_later(sync) + end + + sync + end + end end - def sync(start_date: nil) - syncs.create!(start_date: start_date).perform + def perform_sync(sync) + syncer.perform_sync(sync) end - def sync_data(sync, start_date: nil) - raise NotImplementedError, "Subclasses must implement the `sync_data` method" + def perform_post_sync + syncer.perform_post_sync end - def post_sync(sync) - # no-op, syncable can optionally provide implementation + def broadcast_sync_complete + sync_broadcaster.broadcast end def sync_error @@ -31,7 +51,7 @@ module Syncable end def last_synced_at - latest_sync&.last_ran_at + latest_sync&.completed_at end def last_sync_created_at @@ -40,6 +60,14 @@ module Syncable private def latest_sync - syncs.order(created_at: :desc).first + syncs.ordered.first + end + + def syncer + self.class::Syncer.new(self) + end + + def sync_broadcaster + self.class::SyncCompleteEvent.new(self) end end diff --git a/app/models/entry.rb b/app/models/entry.rb index c07f27cf..5b14987a 100644 --- a/app/models/entry.rb +++ b/app/models/entry.rb @@ -45,7 +45,7 @@ class Entry < ApplicationRecord def sync_account_later sync_start_date = [ date_previously_was, date ].compact.min unless destroyed? - account.sync_later(start_date: sync_start_date) + account.sync_later(window_start_date: sync_start_date) end def entryable_name_short @@ -85,7 +85,7 @@ class Entry < ApplicationRecord entry.update! bulk_attributes entry.lock_saved_attributes! - entry.entryable.lock!(:tag_ids) if entry.transaction? && entry.transaction.tags.any? + entry.entryable.lock_attr!(:tag_ids) if entry.transaction? && entry.transaction.tags.any? end end diff --git a/app/models/exchange_rate/importer.rb b/app/models/exchange_rate/importer.rb new file mode 100644 index 00000000..0975f2ed --- /dev/null +++ b/app/models/exchange_rate/importer.rb @@ -0,0 +1,156 @@ +class ExchangeRate::Importer + MissingExchangeRateError = Class.new(StandardError) + MissingStartRateError = Class.new(StandardError) + + def initialize(exchange_rate_provider:, from:, to:, start_date:, end_date:, clear_cache: false) + @exchange_rate_provider = exchange_rate_provider + @from = from + @to = to + @start_date = start_date + @end_date = normalize_end_date(end_date) + @clear_cache = clear_cache + end + + # Constructs a daily series of rates for the given currency pair for date range + def import_provider_rates + if !clear_cache && all_rates_exist? + Rails.logger.info("No new rates to sync for #{from} to #{to} between #{start_date} and #{end_date}, skipping") + return + end + + if provider_rates.empty? + Rails.logger.warn("Could not fetch rates for #{from} to #{to} between #{start_date} and #{end_date} because provider returned no rates") + return + end + + prev_rate_value = start_rate_value + + unless prev_rate_value.present? + error = MissingStartRateError.new("Could not find a start rate for #{from} to #{to} between #{start_date} and #{end_date}") + Rails.logger.error(error.message) + Sentry.capture_exception(error) + return + end + + gapfilled_rates = effective_start_date.upto(end_date).map do |date| + db_rate_value = db_rates[date]&.rate + provider_rate_value = provider_rates[date]&.rate + + chosen_rate = if clear_cache + provider_rate_value || db_rate_value # overwrite when possible + else + db_rate_value || provider_rate_value # fill gaps + end + + # Gapfill with LOCF strategy (last observation carried forward) + if chosen_rate.nil? + chosen_rate = prev_rate_value + end + + prev_rate_value = chosen_rate + + { + from_currency: from, + to_currency: to, + date: date, + rate: chosen_rate + } + end + + upsert_rows(gapfilled_rates) + end + + private + attr_reader :exchange_rate_provider, :from, :to, :start_date, :end_date, :clear_cache + + def upsert_rows(rows) + batch_size = 200 + + total_upsert_count = 0 + + rows.each_slice(batch_size) do |batch| + upserted_ids = ExchangeRate.upsert_all( + batch, + unique_by: %i[from_currency to_currency date], + returning: [ "id" ] + ) + + total_upsert_count += upserted_ids.count + end + + total_upsert_count + end + + # Since provider may not return values on weekends and holidays, we grab the first rate from the provider that is on or before the start date + def start_rate_value + provider_rate_value = provider_rates.select { |date, _| date <= start_date }.max_by { |date, _| date }&.last + db_rate_value = db_rates[start_date]&.rate + provider_rate_value || db_rate_value + end + + # No need to fetch/upsert rates for dates that we already have in the DB + def effective_start_date + return start_date if clear_cache + + first_missing_date = nil + + start_date.upto(end_date) do |date| + unless db_rates.key?(date) + first_missing_date = date + break + end + end + + first_missing_date || end_date + end + + def provider_rates + @provider_rates ||= begin + # Always fetch with a 5 day buffer to ensure we have a starting rate (for weekends and holidays) + provider_fetch_start_date = effective_start_date - 5.days + + provider_response = exchange_rate_provider.fetch_exchange_rates( + from: from, + to: to, + start_date: provider_fetch_start_date, + end_date: end_date + ) + + if provider_response.success? + provider_response.data.index_by(&:date) + else + message = "#{exchange_rate_provider.class.name} could not fetch exchange rate pair from: #{from} to: #{to} between: #{effective_start_date} and: #{Date.current}. Provider error: #{provider_response.error.message}" + Rails.logger.warn(message) + Sentry.capture_exception(MissingExchangeRateError.new(message), level: :warning) + {} + end + end + end + + def all_rates_exist? + db_count == expected_count + end + + def expected_count + (start_date..end_date).count + end + + def db_count + db_rates.count + end + + def db_rates + @db_rates ||= ExchangeRate.where(from_currency: from, to_currency: to, date: start_date..end_date) + .order(:date) + .to_a + .index_by(&:date) + end + + # Normalizes an end date so that it never exceeds today's date in the + # America/New_York timezone. If the caller passes a future date we clamp + # it to today so that upstream provider calls remain valid and predictable. + def normalize_end_date(requested_end_date) + today_est = Date.current.in_time_zone("America/New_York").to_date + [ requested_end_date, today_est ].min + end +end diff --git a/app/models/exchange_rate/provided.rb b/app/models/exchange_rate/provided.rb index dbe87133..defee421 100644 --- a/app/models/exchange_rate/provided.rb +++ b/app/models/exchange_rate/provided.rb @@ -27,29 +27,21 @@ module ExchangeRate::Provided rate end - def sync_provider_rates(from:, to:, start_date:, end_date: Date.current) + # @return [Integer] The number of exchange rates synced + def import_provider_rates(from:, to:, start_date:, end_date:, clear_cache: false) unless provider.present? - Rails.logger.warn("No provider configured for ExchangeRate.sync_provider_rates") + Rails.logger.warn("No provider configured for ExchangeRate.import_provider_rates") return 0 end - fetched_rates = provider.fetch_exchange_rates(from: from, to: to, start_date: start_date, end_date: end_date) - - unless fetched_rates.success? - Rails.logger.error("Provider error for ExchangeRate.sync_provider_rates: #{fetched_rates.error}") - return 0 - end - - rates_data = fetched_rates.data.map do |rate| - { - from_currency: rate.from, - to_currency: rate.to, - date: rate.date, - rate: rate.rate - } - end - - ExchangeRate.upsert_all(rates_data, unique_by: %i[from_currency to_currency date]) + ExchangeRate::Importer.new( + exchange_rate_provider: provider, + from: from, + to: to, + start_date: start_date, + end_date: end_date, + clear_cache: clear_cache + ).import_provider_rates end end end diff --git a/app/models/family.rb b/app/models/family.rb index a3a73eec..cd068cae 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -1,5 +1,5 @@ class Family < ApplicationRecord - include Syncable, AutoTransferMatchable, Subscribeable + include PlaidConnectable, Syncable, AutoTransferMatchable, Subscribeable DATE_FORMATS = [ [ "MM-DD-YYYY", "%m-%d-%Y" ], @@ -15,7 +15,6 @@ class Family < ApplicationRecord has_many :users, dependent: :destroy has_many :accounts, dependent: :destroy - has_many :plaid_items, dependent: :destroy has_many :invitations, dependent: :destroy has_many :imports, dependent: :destroy @@ -36,6 +35,15 @@ class Family < ApplicationRecord validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) } validates :date_format, inclusion: { in: DATE_FORMATS.map(&:last) } + # If any accounts or plaid items are syncing, the family is also syncing, even if a formal "Family Sync" is not running. + def syncing? + Sync.joins("LEFT JOIN plaid_items ON plaid_items.id = syncs.syncable_id AND syncs.syncable_type = 'PlaidItem'") + .joins("LEFT JOIN accounts ON accounts.id = syncs.syncable_id AND syncs.syncable_type = 'Account'") + .where("syncs.syncable_id = ? OR accounts.family_id = ? OR plaid_items.family_id = ?", id, id, id) + .visible + .exists? + end + def assigned_merchants merchant_ids = transactions.where.not(merchant_id: nil).pluck(:merchant_id).uniq Merchant.where(id: merchant_ids) @@ -65,64 +73,10 @@ class Family < ApplicationRecord @income_statement ||= IncomeStatement.new(self) end - def sync_data(sync, start_date: nil) - # We don't rely on this value to guard the app, but keep it eventually consistent - sync_trial_status! - - Rails.logger.info("Syncing accounts for family #{id}") - accounts.manual.each do |account| - account.sync_later(start_date: start_date, parent_sync: sync) - end - - Rails.logger.info("Applying rules for family #{id}") - rules.each do |rule| - rule.apply_later - end - end - - def remove_syncing_notice! - broadcast_remove target: "syncing-notice" - end - - def post_sync(sync) - auto_match_transfers! - broadcast_refresh - end - - # If family has any syncs pending/syncing within the last 10 minutes, we show a persistent "syncing" notice. - # Ignore syncs older than 10 minutes as they are considered "stale" - def syncing? - Sync.where( - "(syncable_type = 'Family' AND syncable_id = ?) OR - (syncable_type = 'Account' AND syncable_id IN (SELECT id FROM accounts WHERE family_id = ? AND plaid_account_id IS NULL)) OR - (syncable_type = 'PlaidItem' AND syncable_id IN (SELECT id FROM plaid_items WHERE family_id = ?))", - id, id, id - ).where(status: [ "pending", "syncing" ], created_at: 10.minutes.ago..).exists? - end - def eu? country != "US" && country != "CA" end - def get_link_token(webhooks_url:, redirect_url:, accountable_type: nil, region: :us, access_token: nil) - provider = if region.to_sym == :eu - Provider::Registry.get_provider(:plaid_eu) - else - Provider::Registry.get_provider(:plaid_us) - end - - # early return when no provider - return nil unless provider - - provider.get_link_token( - user_id: id, - webhooks_url: webhooks_url, - redirect_url: redirect_url, - accountable_type: accountable_type, - access_token: access_token - ).link_token - end - def requires_data_provider? # If family has any trades, they need a provider for historical prices return true if trades.any? diff --git a/app/models/family/auto_categorizer.rb b/app/models/family/auto_categorizer.rb index c35aa3b9..25fde493 100644 --- a/app/models/family/auto_categorizer.rb +++ b/app/models/family/auto_categorizer.rb @@ -27,23 +27,19 @@ class Family::AutoCategorizer end scope.each do |transaction| - transaction.lock!(:category_id) - auto_categorization = result.data.find { |c| c.transaction_id == transaction.id } category_id = user_categories_input.find { |c| c[:name] == auto_categorization&.category_name }&.dig(:id) if category_id.present? - Family.transaction do - transaction.log_enrichment!( - attribute_name: "category_id", - attribute_value: category_id, - source: "ai", - ) - - transaction.update!(category_id: category_id) - end + transaction.enrich_attribute( + :category_id, + category_id, + source: "ai" + ) end + + transaction.lock_attr!(:category_id) end end diff --git a/app/models/family/auto_merchant_detector.rb b/app/models/family/auto_merchant_detector.rb index 4b791e7a..39e58a3a 100644 --- a/app/models/family/auto_merchant_detector.rb +++ b/app/models/family/auto_merchant_detector.rb @@ -27,8 +27,6 @@ class Family::AutoMerchantDetector end scope.each do |transaction| - transaction.lock!(:merchant_id) - auto_detection = result.data.find { |c| c.transaction_id == transaction.id } merchant_id = user_merchants_input.find { |m| m[:name] == auto_detection&.business_name }&.dig(:id) @@ -46,16 +44,16 @@ class Family::AutoMerchantDetector merchant_id = merchant_id || ai_provider_merchant&.id if merchant_id.present? - Family.transaction do - transaction.log_enrichment!( - attribute_name: "merchant_id", - attribute_value: merchant_id, - source: "ai", - ) + transaction.enrich_attribute( + :merchant_id, + merchant_id, + source: "ai" + ) - transaction.update!(merchant_id: merchant_id) - end end + + # We lock the attribute so that this Rule doesn't try to run again + transaction.lock_attr!(:merchant_id) end end diff --git a/app/models/family/plaid_connectable.rb b/app/models/family/plaid_connectable.rb new file mode 100644 index 00000000..f2a997c8 --- /dev/null +++ b/app/models/family/plaid_connectable.rb @@ -0,0 +1,51 @@ +module Family::PlaidConnectable + extend ActiveSupport::Concern + + included do + has_many :plaid_items, dependent: :destroy + end + + def create_plaid_item!(public_token:, item_name:, region:) + provider = plaid_provider_for_region(region) + + public_token_response = provider.exchange_public_token(public_token) + + plaid_item = plaid_items.create!( + name: item_name, + plaid_id: public_token_response.item_id, + access_token: public_token_response.access_token, + plaid_region: region + ) + + plaid_item.sync_later + + plaid_item + end + + def get_link_token(webhooks_url:, redirect_url:, accountable_type: nil, region: :us, access_token: nil) + return nil unless plaid_us || plaid_eu + + provider = plaid_provider_for_region(region) + + provider.get_link_token( + user_id: self.id, + webhooks_url: webhooks_url, + redirect_url: redirect_url, + accountable_type: accountable_type, + access_token: access_token + ).link_token + end + + private + def plaid_us + @plaid ||= Provider::Registry.get_provider(:plaid_us) + end + + def plaid_eu + @plaid_eu ||= Provider::Registry.get_provider(:plaid_eu) + end + + def plaid_provider_for_region(region) + region.to_sym == :eu ? plaid_eu : plaid_us + end +end diff --git a/app/models/family/subscribeable.rb b/app/models/family/subscribeable.rb index 4c8624b7..94ee0971 100644 --- a/app/models/family/subscribeable.rb +++ b/app/models/family/subscribeable.rb @@ -72,10 +72,9 @@ module Family::Subscribeable (1 - days_left_in_trial.to_f / Subscription::TRIAL_DAYS) * 100 end - private - def sync_trial_status! - if subscription&.status == "trialing" && days_left_in_trial < 0 - subscription.update!(status: "paused") - end + def sync_trial_status! + if subscription&.status == "trialing" && days_left_in_trial < 0 + subscription.update!(status: "paused") end + end end diff --git a/app/models/family/sync_complete_event.rb b/app/models/family/sync_complete_event.rb new file mode 100644 index 00000000..628841d0 --- /dev/null +++ b/app/models/family/sync_complete_event.rb @@ -0,0 +1,21 @@ +class Family::SyncCompleteEvent + attr_reader :family + + def initialize(family) + @family = family + end + + def broadcast + family.broadcast_replace( + target: "balance-sheet", + partial: "pages/dashboard/balance_sheet", + locals: { balance_sheet: family.balance_sheet } + ) + + family.broadcast_replace( + target: "net-worth-chart", + partial: "pages/dashboard/net_worth_chart", + locals: { balance_sheet: family.balance_sheet, period: Period.last_30_days } + ) + end +end diff --git a/app/models/family/syncer.rb b/app/models/family/syncer.rb new file mode 100644 index 00000000..30ce2ad5 --- /dev/null +++ b/app/models/family/syncer.rb @@ -0,0 +1,31 @@ +class Family::Syncer + attr_reader :family + + def initialize(family) + @family = family + end + + def perform_sync(sync) + # We don't rely on this value to guard the app, but keep it eventually consistent + family.sync_trial_status! + + Rails.logger.info("Applying rules for family #{family.id}") + family.rules.each do |rule| + rule.apply_later + end + + # Schedule child syncs + child_syncables.each do |syncable| + syncable.sync_later(parent_sync: sync, window_start_date: sync.window_start_date, window_end_date: sync.window_end_date) + end + end + + def perform_post_sync + family.auto_match_transfers! + end + + private + def child_syncables + family.plaid_items + family.accounts.manual + end +end diff --git a/app/models/holding/base_calculator.rb b/app/models/holding/base_calculator.rb deleted file mode 100644 index d9b85d03..00000000 --- a/app/models/holding/base_calculator.rb +++ /dev/null @@ -1,62 +0,0 @@ -class Holding::BaseCalculator - attr_reader :account - - def initialize(account) - @account = account - end - - def calculate - Rails.logger.tagged(self.class.name) do - holdings = calculate_holdings - Holding.gapfill(holdings) - end - end - - private - def portfolio_cache - @portfolio_cache ||= Holding::PortfolioCache.new(account) - end - - def empty_portfolio - securities = portfolio_cache.get_securities - securities.each_with_object({}) { |security, hash| hash[security.id] = 0 } - end - - def generate_starting_portfolio - empty_portfolio - end - - def transform_portfolio(previous_portfolio, trade_entries, direction: :forward) - new_quantities = previous_portfolio.dup - - trade_entries.each do |trade_entry| - trade = trade_entry.entryable - security_id = trade.security_id - qty_change = trade.qty - qty_change = qty_change * -1 if direction == :reverse - new_quantities[security_id] = (new_quantities[security_id] || 0) + qty_change - end - - new_quantities - end - - def build_holdings(portfolio, date, price_source: nil) - portfolio.map do |security_id, qty| - price = portfolio_cache.get_price(security_id, date, source: price_source) - - if price.nil? - next - end - - Holding.new( - account_id: account.id, - security_id: security_id, - date: date, - qty: qty, - price: price.price, - currency: price.currency, - amount: qty * price.price - ) - end.compact - end -end diff --git a/app/models/holding/forward_calculator.rb b/app/models/holding/forward_calculator.rb index d2f2e8d7..43f91f7a 100644 --- a/app/models/holding/forward_calculator.rb +++ b/app/models/holding/forward_calculator.rb @@ -1,10 +1,12 @@ -class Holding::ForwardCalculator < Holding::BaseCalculator - private - def portfolio_cache - @portfolio_cache ||= Holding::PortfolioCache.new(account) - end +class Holding::ForwardCalculator + attr_reader :account - def calculate_holdings + def initialize(account) + @account = account + end + + def calculate + Rails.logger.tagged("Holding::ForwardCalculator") do current_portfolio = generate_starting_portfolio next_portfolio = {} holdings = [] @@ -16,6 +18,55 @@ class Holding::ForwardCalculator < Holding::BaseCalculator current_portfolio = next_portfolio end - holdings + Holding.gapfill(holdings) + end + end + + private + def portfolio_cache + @portfolio_cache ||= Holding::PortfolioCache.new(account) + end + + def empty_portfolio + securities = portfolio_cache.get_securities + securities.each_with_object({}) { |security, hash| hash[security.id] = 0 } + end + + def generate_starting_portfolio + empty_portfolio + end + + def transform_portfolio(previous_portfolio, trade_entries, direction: :forward) + new_quantities = previous_portfolio.dup + + trade_entries.each do |trade_entry| + trade = trade_entry.entryable + security_id = trade.security_id + qty_change = trade.qty + qty_change = qty_change * -1 if direction == :reverse + new_quantities[security_id] = (new_quantities[security_id] || 0) + qty_change + end + + new_quantities + end + + def build_holdings(portfolio, date, price_source: nil) + portfolio.map do |security_id, qty| + price = portfolio_cache.get_price(security_id, date, source: price_source) + + if price.nil? + next + end + + Holding.new( + account_id: account.id, + security_id: security_id, + date: date, + qty: qty, + price: price.price, + currency: price.currency, + amount: qty * price.price + ) + end.compact end end diff --git a/app/models/holding/syncer.rb b/app/models/holding/materializer.rb similarity index 87% rename from app/models/holding/syncer.rb rename to app/models/holding/materializer.rb index 345f2a3f..e4931128 100644 --- a/app/models/holding/syncer.rb +++ b/app/models/holding/materializer.rb @@ -1,10 +1,12 @@ -class Holding::Syncer +# "Materializes" holdings (similar to a DB materialized view, but done at the app level) +# into a series of records we can easily query and join with other data. +class Holding::Materializer def initialize(account, strategy:) @account = account @strategy = strategy end - def sync_holdings + def materialize_holdings calculate_holdings Rails.logger.info("Persisting #{@holdings.size} holdings") diff --git a/app/models/holding/portfolio_cache.rb b/app/models/holding/portfolio_cache.rb index 2d67a1d8..9ffed15b 100644 --- a/app/models/holding/portfolio_cache.rb +++ b/app/models/holding/portfolio_cache.rb @@ -83,9 +83,6 @@ class Holding::PortfolioCache securities.each do |security| Rails.logger.info "Loading security: ID=#{security.id} Ticker=#{security.ticker}" - # Load prices from provider to DB - security.sync_provider_prices(start_date: account.start_date) - # High priority prices from DB (synced from provider) db_prices = security.prices.where(date: account.start_date..Date.current).map do |price| PriceWithPriority.new( diff --git a/app/models/holding/reverse_calculator.rb b/app/models/holding/reverse_calculator.rb index 62e2dc95..f52184d7 100644 --- a/app/models/holding/reverse_calculator.rb +++ b/app/models/holding/reverse_calculator.rb @@ -1,4 +1,17 @@ -class Holding::ReverseCalculator < Holding::BaseCalculator +class Holding::ReverseCalculator + attr_reader :account + + def initialize(account) + @account = account + end + + def calculate + Rails.logger.tagged("Holding::ReverseCalculator") do + holdings = calculate_holdings + Holding.gapfill(holdings) + end + end + private # Reverse calculators will use the existing holdings as a source of security ids and prices # since it is common for a provider to supply "current day" holdings but not all the historical @@ -25,6 +38,11 @@ class Holding::ReverseCalculator < Holding::BaseCalculator holdings end + def empty_portfolio + securities = portfolio_cache.get_securities + securities.each_with_object({}) { |security, hash| hash[security.id] = 0 } + end + # Since this is a reverse sync, we start with today's holdings def generate_starting_portfolio holding_quantities = empty_portfolio @@ -37,4 +55,38 @@ class Holding::ReverseCalculator < Holding::BaseCalculator holding_quantities end + + def transform_portfolio(previous_portfolio, trade_entries, direction: :forward) + new_quantities = previous_portfolio.dup + + trade_entries.each do |trade_entry| + trade = trade_entry.entryable + security_id = trade.security_id + qty_change = trade.qty + qty_change = qty_change * -1 if direction == :reverse + new_quantities[security_id] = (new_quantities[security_id] || 0) + qty_change + end + + new_quantities + end + + def build_holdings(portfolio, date, price_source: nil) + portfolio.map do |security_id, qty| + price = portfolio_cache.get_price(security_id, date, source: price_source) + + if price.nil? + next + end + + Holding.new( + account_id: account.id, + security_id: security_id, + date: date, + qty: qty, + price: price.price, + currency: price.currency, + amount: qty * price.price + ) + end.compact + end end diff --git a/app/models/import.rb b/app/models/import.rb index 3ea68015..b0a02ea0 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -1,4 +1,6 @@ class Import < ApplicationRecord + MaxRowCountExceededError = Class.new(StandardError) + TYPES = %w[TransactionImport TradeImport AccountImport MintImport].freeze SIGNAGE_CONVENTIONS = %w[inflows_positive inflows_negative] SEPARATORS = [ [ "Comma (,)", "," ], [ "Semicolon (;)", ";" ] ].freeze @@ -52,6 +54,7 @@ class Import < ApplicationRecord end def publish_later + raise MaxRowCountExceededError if row_count_exceeded? raise "Import is not publishable" unless publishable? update! status: :importing @@ -60,9 +63,11 @@ class Import < ApplicationRecord end def publish + raise MaxRowCountExceededError if row_count_exceeded? + import! - family.sync + family.sync_later update! status: :complete rescue => error @@ -220,7 +225,15 @@ class Import < ApplicationRecord ) end + def max_row_count + 10000 + end + private + def row_count_exceeded? + rows.count > max_row_count + end + def import! # no-op, subclasses can implement for customization of algorithm end diff --git a/app/models/market_data_importer.rb b/app/models/market_data_importer.rb new file mode 100644 index 00000000..9eaf5964 --- /dev/null +++ b/app/models/market_data_importer.rb @@ -0,0 +1,132 @@ +class MarketDataImporter + # By default, our graphs show 1M as the view, so by fetching 31 days, + # we ensure we can always show an accurate default graph + SNAPSHOT_DAYS = 31 + + InvalidModeError = Class.new(StandardError) + + def initialize(mode: :full, clear_cache: false) + @mode = set_mode!(mode) + @clear_cache = clear_cache + end + + def import_all + import_security_prices + import_exchange_rates + end + + # Syncs historical security prices (and details) + def import_security_prices + unless Security.provider + Rails.logger.warn("No provider configured for MarketDataImporter.import_security_prices, skipping sync") + return + end + + Security.where.not(exchange_operating_mic: nil).find_each do |security| + security.import_provider_prices( + start_date: get_first_required_price_date(security), + end_date: end_date, + clear_cache: clear_cache + ) + + security.import_provider_details(clear_cache: clear_cache) + end + end + + def import_exchange_rates + unless ExchangeRate.provider + Rails.logger.warn("No provider configured for MarketDataImporter.import_exchange_rates, skipping sync") + return + end + + required_exchange_rate_pairs.each do |pair| + # pair is a Hash with keys :source, :target, and :start_date + start_date = snapshot? ? default_start_date : pair[:start_date] + + ExchangeRate.import_provider_rates( + from: pair[:source], + to: pair[:target], + start_date: start_date, + end_date: end_date, + clear_cache: clear_cache + ) + end + end + + private + attr_reader :mode, :clear_cache + + def snapshot? + mode.to_sym == :snapshot + end + + # Builds a unique list of currency pairs with the earliest date we need + # exchange rates for. + # + # Returns: Array of Hashes – [{ source:, target:, start_date: }, ...] + def required_exchange_rate_pairs + pair_dates = {} # { [source, target] => earliest_date } + + # 1. ENTRY-BASED PAIRS – we need rates from the first entry date + Entry.joins(:account) + .where.not("entries.currency = accounts.currency") + .group("entries.currency", "accounts.currency") + .minimum("entries.date") + .each do |(source, target), date| + key = [ source, target ] + pair_dates[key] = [ pair_dates[key], date ].compact.min + end + + # 2. ACCOUNT-BASED PAIRS – use the account's oldest entry date + account_first_entry_dates = Entry.group(:account_id).minimum(:date) + + Account.joins(:family) + .where.not("families.currency = accounts.currency") + .select("accounts.id, accounts.currency AS source, families.currency AS target") + .find_each do |account| + earliest_entry_date = account_first_entry_dates[account.id] + + chosen_date = [ earliest_entry_date, default_start_date ].compact.min + + key = [ account.source, account.target ] + pair_dates[key] = [ pair_dates[key], chosen_date ].compact.min + end + + # Convert to array of hashes for ease of use + pair_dates.map do |(source, target), date| + { source: source, target: target, start_date: date } + end + end + + def get_first_required_price_date(security) + return default_start_date if snapshot? + + Trade.with_entry.where(security: security).minimum(:date) || default_start_date + end + + # An approximation that grabs more than we likely need, but simplifies the logic + def get_first_required_exchange_rate_date(from_currency:) + return default_start_date if snapshot? + + Entry.where(currency: from_currency).minimum(:date) || default_start_date + end + + def default_start_date + SNAPSHOT_DAYS.days.ago.to_date + end + + # Since we're querying market data from a US-based API, end date should always be today (EST) + def end_date + Date.current.in_time_zone("America/New_York").to_date + end + + def set_mode!(mode) + valid_modes = [ :full, :snapshot ] + + unless valid_modes.include?(mode.to_sym) + raise InvalidModeError, "Invalid mode for MarketDataImporter, can only be :full or :snapshot, but was #{mode}" + end + + mode.to_sym + end +end diff --git a/app/models/plaid_account.rb b/app/models/plaid_account.rb index 4f60013e..4730985d 100644 --- a/app/models/plaid_account.rb +++ b/app/models/plaid_account.rb @@ -20,7 +20,10 @@ class PlaidAccount < ApplicationRecord internal_account = family.accounts.find_or_initialize_by(plaid_account_id: plaid_account.id) - internal_account.name = plaid_data.name + # Only set the name for new records or if the name is not locked + if internal_account.new_record? || internal_account.enrichable?(:name) + internal_account.name = plaid_data.name + end internal_account.balance = plaid_data.balances.current || plaid_data.balances.available internal_account.currency = plaid_data.balances.iso_currency_code internal_account.accountable = TYPE_MAPPING[plaid_data.type].new diff --git a/app/models/plaid_item.rb b/app/models/plaid_item.rb index 2226f12f..e693e69a 100644 --- a/app/models/plaid_item.rb +++ b/app/models/plaid_item.rb @@ -1,5 +1,5 @@ class PlaidItem < ApplicationRecord - include Provided, Syncable + include Syncable enum :plaid_region, { us: "us", eu: "eu" } enum :status, { good: "good", requires_update: "requires_update" }, default: :good @@ -22,39 +22,6 @@ class PlaidItem < ApplicationRecord scope :ordered, -> { order(created_at: :desc) } scope :needs_update, -> { where(status: :requires_update) } - class << self - def create_from_public_token(token, item_name:, region:) - response = plaid_provider_for_region(region).exchange_public_token(token) - - new_plaid_item = create!( - name: item_name, - plaid_id: response.item_id, - access_token: response.access_token, - plaid_region: region - ) - - new_plaid_item.sync_later - end - end - - def sync_data(sync, start_date: nil) - begin - Rails.logger.info("Fetching and loading Plaid data") - fetch_and_load_plaid_data(sync) - update!(status: :good) if requires_update? - - # Schedule account syncs - accounts.each do |account| - account.sync_later(start_date: start_date, parent_sync: sync) - end - - Rails.logger.info("Plaid data fetched and loaded") - rescue Plaid::ApiError => e - handle_plaid_error(e) - raise e - end - end - def get_update_link_token(webhooks_url:, redirect_url:) begin family.get_link_token( @@ -76,9 +43,8 @@ class PlaidItem < ApplicationRecord end end - def post_sync(sync) - auto_match_categories! - family.broadcast_refresh + def build_category_alias_matcher(user_categories) + Provider::Plaid::CategoryAliasMatcher.new(user_categories) end def destroy_later @@ -86,6 +52,14 @@ class PlaidItem < ApplicationRecord DestroyJob.perform_later(self) end + def syncing? + Sync.joins("LEFT JOIN accounts a ON a.id = syncs.syncable_id AND syncs.syncable_type = 'Account'") + .joins("LEFT JOIN plaid_accounts pa ON pa.id = a.plaid_account_id") + .where("syncs.syncable_id = ? OR pa.plaid_item_id = ?", id, id) + .visible + .exists? + end + def auto_match_categories! if family.categories.none? family.categories.bootstrap! @@ -103,137 +77,25 @@ class PlaidItem < ApplicationRecord category = alias_matcher.match(transaction.plaid_category_detailed) if category.present? - PlaidItem.transaction do - transaction.log_enrichment!( - attribute_name: "category_id", - attribute_value: category.id, - source: "plaid" - ) - transaction.set_category!(category) + # Matcher could either return a string or a Category object + user_category = if category.is_a?(String) + family.categories.find_or_create_by!(name: category) + else + category end + + transaction.enrich_attribute(:category_id, user_category.id, source: "plaid") end end end end private - def fetch_and_load_plaid_data(sync) - data = {} - - # Log what we're about to fetch - Rails.logger.info "Starting Plaid data fetch (accounts, transactions, investments, liabilities)" - - item = plaid_provider.get_item(access_token).item - update!(available_products: item.available_products, billed_products: item.billed_products) - - # Institution details - if item.institution_id.present? - begin - Rails.logger.info "Fetching Plaid institution details for #{item.institution_id}" - institution = plaid_provider.get_institution(item.institution_id) - update!( - institution_id: item.institution_id, - institution_url: institution.institution.url, - institution_color: institution.institution.primary_color - ) - rescue Plaid::ApiError => e - Rails.logger.warn "Failed to fetch Plaid institution details: #{e.message}" - end - end - - # Accounts - fetched_accounts = plaid_provider.get_item_accounts(self).accounts - data[:accounts] = fetched_accounts || [] - sync.update!(data: data) - Rails.logger.info "Processing Plaid accounts (count: #{fetched_accounts.size})" - - internal_plaid_accounts = fetched_accounts.map do |account| - internal_plaid_account = plaid_accounts.find_or_create_from_plaid_data!(account, family) - internal_plaid_account.sync_account_data!(account) - internal_plaid_account - end - - # Transactions - fetched_transactions = safe_fetch_plaid_data(:get_item_transactions) - data[:transactions] = fetched_transactions || [] - sync.update!(data: data) - - if fetched_transactions - Rails.logger.info "Processing Plaid transactions (added: #{fetched_transactions.added.size}, modified: #{fetched_transactions.modified.size}, removed: #{fetched_transactions.removed.size})" - transaction do - internal_plaid_accounts.each do |internal_plaid_account| - added = fetched_transactions.added.select { |t| t.account_id == internal_plaid_account.plaid_id } - modified = fetched_transactions.modified.select { |t| t.account_id == internal_plaid_account.plaid_id } - removed = fetched_transactions.removed.select { |t| t.account_id == internal_plaid_account.plaid_id } - - internal_plaid_account.sync_transactions!(added:, modified:, removed:) - end - - update!(next_cursor: fetched_transactions.cursor) - end - end - - # Investments - fetched_investments = safe_fetch_plaid_data(:get_item_investments) - data[:investments] = fetched_investments || [] - sync.update!(data: data) - - if fetched_investments - Rails.logger.info "Processing Plaid investments (transactions: #{fetched_investments.transactions.size}, holdings: #{fetched_investments.holdings.size}, securities: #{fetched_investments.securities.size})" - transaction do - internal_plaid_accounts.each do |internal_plaid_account| - transactions = fetched_investments.transactions.select { |t| t.account_id == internal_plaid_account.plaid_id } - holdings = fetched_investments.holdings.select { |h| h.account_id == internal_plaid_account.plaid_id } - securities = fetched_investments.securities - - internal_plaid_account.sync_investments!(transactions:, holdings:, securities:) - end - end - end - - # Liabilities - fetched_liabilities = safe_fetch_plaid_data(:get_item_liabilities) - data[:liabilities] = fetched_liabilities || [] - sync.update!(data: data) - - if fetched_liabilities - Rails.logger.info "Processing Plaid liabilities (credit: #{fetched_liabilities.credit&.size || 0}, mortgage: #{fetched_liabilities.mortgage&.size || 0}, student: #{fetched_liabilities.student&.size || 0})" - transaction do - internal_plaid_accounts.each do |internal_plaid_account| - credit = fetched_liabilities.credit&.find { |l| l.account_id == internal_plaid_account.plaid_id } - mortgage = fetched_liabilities.mortgage&.find { |l| l.account_id == internal_plaid_account.plaid_id } - student = fetched_liabilities.student&.find { |l| l.account_id == internal_plaid_account.plaid_id } - - internal_plaid_account.sync_credit_data!(credit) if credit - internal_plaid_account.sync_mortgage_data!(mortgage) if mortgage - internal_plaid_account.sync_student_loan_data!(student) if student - end - end - end - end - - def safe_fetch_plaid_data(method) - begin - plaid_provider.send(method, self) - rescue Plaid::ApiError => e - Rails.logger.warn("Error fetching #{method} for item #{id}: #{e.message}") - nil - end - end - def remove_plaid_item plaid_provider.remove_item(access_token) rescue StandardError => e Rails.logger.warn("Failed to remove Plaid item #{id}: #{e.message}") end - def handle_plaid_error(error) - error_body = JSON.parse(error.response_body) - - if error_body["error_code"] == "ITEM_LOGIN_REQUIRED" - update!(status: :requires_update) - end - end - class PlaidConnectionLostError < StandardError; end end diff --git a/app/models/plaid_item/provided.rb b/app/models/plaid_item/provided.rb deleted file mode 100644 index f2e8ee8f..00000000 --- a/app/models/plaid_item/provided.rb +++ /dev/null @@ -1,30 +0,0 @@ -module PlaidItem::Provided - extend ActiveSupport::Concern - - class_methods do - def plaid_us_provider - Provider::Registry.get_provider(:plaid_us) - end - - def plaid_eu_provider - Provider::Registry.get_provider(:plaid_eu) - end - - def plaid_provider_for_region(region) - region.to_sym == :eu ? plaid_eu_provider : plaid_us_provider - end - end - - def build_category_alias_matcher(user_categories) - Provider::Plaid::CategoryAliasMatcher.new(user_categories) - end - - private - def eu? - raise "eu? is not implemented for #{self.class.name}" - end - - def plaid_provider - eu? ? self.class.plaid_eu_provider : self.class.plaid_us_provider - end -end diff --git a/app/models/plaid_item/sync_complete_event.rb b/app/models/plaid_item/sync_complete_event.rb new file mode 100644 index 00000000..ca008a0a --- /dev/null +++ b/app/models/plaid_item/sync_complete_event.rb @@ -0,0 +1,22 @@ +class PlaidItem::SyncCompleteEvent + attr_reader :plaid_item + + def initialize(plaid_item) + @plaid_item = plaid_item + end + + def broadcast + plaid_item.accounts.each do |account| + account.broadcast_sync_complete + end + + plaid_item.broadcast_replace_to( + plaid_item.family, + target: "plaid_item_#{plaid_item.id}", + partial: "plaid_items/plaid_item", + locals: { plaid_item: plaid_item } + ) + + plaid_item.family.broadcast_sync_complete + end +end diff --git a/app/models/plaid_item/syncer.rb b/app/models/plaid_item/syncer.rb new file mode 100644 index 00000000..e32b9ed4 --- /dev/null +++ b/app/models/plaid_item/syncer.rb @@ -0,0 +1,149 @@ +class PlaidItem::Syncer + attr_reader :plaid_item + + def initialize(plaid_item) + @plaid_item = plaid_item + end + + def perform_sync(sync) + begin + Rails.logger.info("Fetching and loading Plaid data") + fetch_and_load_plaid_data + plaid_item.update!(status: :good) if plaid_item.requires_update? + + plaid_item.accounts.each do |account| + account.sync_later(parent_sync: sync, window_start_date: sync.window_start_date, window_end_date: sync.window_end_date) + end + + Rails.logger.info("Plaid data fetched and loaded") + rescue Plaid::ApiError => e + handle_plaid_error(e) + raise e + end + end + + def perform_post_sync + plaid_item.auto_match_categories! + end + + private + def plaid + plaid_item.plaid_region == "eu" ? plaid_eu : plaid_us + end + + def plaid_eu + @plaid_eu ||= Provider::Registry.get_provider(:plaid_eu) + end + + def plaid_us + @plaid_us ||= Provider::Registry.get_provider(:plaid_us) + end + + def safe_fetch_plaid_data(method) + begin + plaid.send(method, plaid_item) + rescue Plaid::ApiError => e + Rails.logger.warn("Error fetching #{method} for item #{plaid_item.id}: #{e.message}") + nil + end + end + + def handle_plaid_error(error) + error_body = JSON.parse(error.response_body) + + if error_body["error_code"] == "ITEM_LOGIN_REQUIRED" + plaid_item.update!(status: :requires_update) + end + end + + def fetch_and_load_plaid_data + data = {} + + # Log what we're about to fetch + Rails.logger.info "Starting Plaid data fetch (accounts, transactions, investments, liabilities)" + + item = plaid.get_item(plaid_item.access_token).item + plaid_item.update!(available_products: item.available_products, billed_products: item.billed_products) + + # Institution details + if item.institution_id.present? + begin + Rails.logger.info "Fetching Plaid institution details for #{item.institution_id}" + institution = plaid.get_institution(item.institution_id) + plaid_item.update!( + institution_id: item.institution_id, + institution_url: institution.institution.url, + institution_color: institution.institution.primary_color + ) + rescue Plaid::ApiError => e + Rails.logger.warn "Failed to fetch Plaid institution details: #{e.message}" + end + end + + # Accounts + fetched_accounts = plaid.get_item_accounts(plaid_item).accounts + data[:accounts] = fetched_accounts || [] + Rails.logger.info "Processing Plaid accounts (count: #{fetched_accounts.size})" + + internal_plaid_accounts = fetched_accounts.map do |account| + internal_plaid_account = plaid_item.plaid_accounts.find_or_create_from_plaid_data!(account, plaid_item.family) + internal_plaid_account.sync_account_data!(account) + internal_plaid_account + end + + # Transactions + fetched_transactions = safe_fetch_plaid_data(:get_item_transactions) + data[:transactions] = fetched_transactions || [] + + if fetched_transactions + Rails.logger.info "Processing Plaid transactions (added: #{fetched_transactions.added.size}, modified: #{fetched_transactions.modified.size}, removed: #{fetched_transactions.removed.size})" + PlaidItem.transaction do + internal_plaid_accounts.each do |internal_plaid_account| + added = fetched_transactions.added.select { |t| t.account_id == internal_plaid_account.plaid_id } + modified = fetched_transactions.modified.select { |t| t.account_id == internal_plaid_account.plaid_id } + removed = fetched_transactions.removed.select { |t| t.account_id == internal_plaid_account.plaid_id } + + internal_plaid_account.sync_transactions!(added:, modified:, removed:) + end + + plaid_item.update!(next_cursor: fetched_transactions.cursor) + end + end + + # Investments + fetched_investments = safe_fetch_plaid_data(:get_item_investments) + data[:investments] = fetched_investments || [] + + if fetched_investments + Rails.logger.info "Processing Plaid investments (transactions: #{fetched_investments.transactions.size}, holdings: #{fetched_investments.holdings.size}, securities: #{fetched_investments.securities.size})" + PlaidItem.transaction do + internal_plaid_accounts.each do |internal_plaid_account| + transactions = fetched_investments.transactions.select { |t| t.account_id == internal_plaid_account.plaid_id } + holdings = fetched_investments.holdings.select { |h| h.account_id == internal_plaid_account.plaid_id } + securities = fetched_investments.securities + + internal_plaid_account.sync_investments!(transactions:, holdings:, securities:) + end + end + end + + # Liabilities + fetched_liabilities = safe_fetch_plaid_data(:get_item_liabilities) + data[:liabilities] = fetched_liabilities || [] + + if fetched_liabilities + Rails.logger.info "Processing Plaid liabilities (credit: #{fetched_liabilities.credit&.size || 0}, mortgage: #{fetched_liabilities.mortgage&.size || 0}, student: #{fetched_liabilities.student&.size || 0})" + PlaidItem.transaction do + internal_plaid_accounts.each do |internal_plaid_account| + credit = fetched_liabilities.credit&.find { |l| l.account_id == internal_plaid_account.plaid_id } + mortgage = fetched_liabilities.mortgage&.find { |l| l.account_id == internal_plaid_account.plaid_id } + student = fetched_liabilities.student&.find { |l| l.account_id == internal_plaid_account.plaid_id } + + internal_plaid_account.sync_credit_data!(credit) if credit + internal_plaid_account.sync_mortgage_data!(mortgage) if mortgage + internal_plaid_account.sync_student_loan_data!(student) if student + end + end + end + end +end diff --git a/app/models/provider.rb b/app/models/provider.rb index c90866e7..e9702349 100644 --- a/app/models/provider.rb +++ b/app/models/provider.rb @@ -36,8 +36,6 @@ class Provider default_error_transformer(error) end - Sentry.capture_exception(transformed_error) - Response.new( success?: false, data: nil, diff --git a/app/models/provider/security_concept.rb b/app/models/provider/security_concept.rb index 1fc915e7..d54b2011 100644 --- a/app/models/provider/security_concept.rb +++ b/app/models/provider/security_concept.rb @@ -2,22 +2,22 @@ module Provider::SecurityConcept extend ActiveSupport::Concern Security = Data.define(:symbol, :name, :logo_url, :exchange_operating_mic) - SecurityInfo = Data.define(:symbol, :name, :links, :logo_url, :description, :kind) - Price = Data.define(:security, :date, :price, :currency) + SecurityInfo = Data.define(:symbol, :name, :links, :logo_url, :description, :kind, :exchange_operating_mic) + Price = Data.define(:symbol, :date, :price, :currency, :exchange_operating_mic) def search_securities(symbol, country_code: nil, exchange_operating_mic: nil) raise NotImplementedError, "Subclasses must implement #search_securities" end - def fetch_security_info(security) + def fetch_security_info(symbol:, exchange_operating_mic:) raise NotImplementedError, "Subclasses must implement #fetch_security_info" end - def fetch_security_price(security, date:) + def fetch_security_price(symbol:, exchange_operating_mic:, date:) raise NotImplementedError, "Subclasses must implement #fetch_security_price" end - def fetch_security_prices(security, start_date:, end_date:) + def fetch_security_prices(symbol:, exchange_operating_mic:, start_date:, end_date:) raise NotImplementedError, "Subclasses must implement #fetch_security_prices" end end diff --git a/app/models/provider/synth.rb b/app/models/provider/synth.rb index ff75ff49..8d76fc72 100644 --- a/app/models/provider/synth.rb +++ b/app/models/provider/synth.rb @@ -3,6 +3,8 @@ class Provider::Synth < Provider # Subclass so errors caught in this provider are raised as Provider::Synth::Error Error = Class.new(Provider::Error) + InvalidExchangeRateError = Class.new(Error) + InvalidSecurityPriceError = Class.new(Error) def initialize(api_key) @api_key = api_key @@ -48,7 +50,7 @@ class Provider::Synth < Provider rates = JSON.parse(response.body).dig("data", "rates") - Rate.new(date:, from:, to:, rate: rates.dig(to)) + Rate.new(date: date.to_date, from:, to:, rate: rates.dig(to)) end end @@ -65,8 +67,20 @@ class Provider::Synth < Provider end data.paginated.map do |rate| - Rate.new(date: rate.dig("date"), from:, to:, rate: rate.dig("rates", to)) - end + date = rate.dig("date") + rate = rate.dig("rates", to) + + if date.nil? || rate.nil? + Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} on: #{date}. Rate data: #{rate.inspect}") + Sentry.capture_exception(InvalidExchangeRateError.new("#{self.class.name} returned invalid rate data"), level: :warning) do |scope| + scope.set_context("rate", { from: from, to: to, date: date }) + end + + next + end + + Rate.new(date: date.to_date, from:, to:, rate:) + end.compact end end @@ -97,65 +111,75 @@ class Provider::Synth < Provider end end - def fetch_security_info(security) + def fetch_security_info(symbol:, exchange_operating_mic:) with_provider_response do - response = client.get("#{base_url}/tickers/#{security.ticker}") do |req| - req.params["mic_code"] = security.exchange_mic if security.exchange_mic.present? - req.params["operating_mic"] = security.exchange_operating_mic if security.exchange_operating_mic.present? + response = client.get("#{base_url}/tickers/#{symbol}") do |req| + req.params["operating_mic"] = exchange_operating_mic end data = JSON.parse(response.body).dig("data") SecurityInfo.new( - symbol: data.dig("ticker"), + symbol: symbol, name: data.dig("name"), links: data.dig("links"), logo_url: data.dig("logo_url"), description: data.dig("description"), - kind: data.dig("kind") + kind: data.dig("kind"), + exchange_operating_mic: exchange_operating_mic ) end end - def fetch_security_price(security, date:) + def fetch_security_price(symbol:, exchange_operating_mic:, date:) with_provider_response do - historical_data = fetch_security_prices(security, start_date: date, end_date: date) + historical_data = fetch_security_prices(symbol:, exchange_operating_mic:, start_date: date, end_date: date) - raise ProviderError, "No prices found for security #{security.ticker} on date #{date}" if historical_data.data.empty? + raise ProviderError, "No prices found for security #{symbol} on date #{date}" if historical_data.data.empty? historical_data.data.first end end - def fetch_security_prices(security, start_date:, end_date:) + def fetch_security_prices(symbol:, exchange_operating_mic:, start_date:, end_date:) with_provider_response do params = { start_date: start_date, - end_date: end_date + end_date: end_date, + operating_mic_code: exchange_operating_mic } - params[:operating_mic_code] = security.exchange_operating_mic if security.exchange_operating_mic.present? - data = paginate( - "#{base_url}/tickers/#{security.ticker}/open-close", + "#{base_url}/tickers/#{symbol}/open-close", params ) do |body| body.dig("prices") end currency = data.first_page.dig("currency") - country_code = data.first_page.dig("exchange", "country_code") - exchange_mic = data.first_page.dig("exchange", "mic_code") exchange_operating_mic = data.first_page.dig("exchange", "operating_mic_code") data.paginated.map do |price| + date = price.dig("date") + price = price.dig("close") || price.dig("open") + + if date.nil? || price.nil? + Rails.logger.warn("#{self.class.name} returned invalid price data for security #{symbol} on: #{date}. Price data: #{price.inspect}") + Sentry.capture_exception(InvalidSecurityPriceError.new("#{self.class.name} returned invalid security price data"), level: :warning) do |scope| + scope.set_context("security", { symbol: symbol, date: date }) + end + + next + end + Price.new( - security: security, - date: price.dig("date"), - price: price.dig("close") || price.dig("open"), - currency: currency + symbol: symbol, + date: date.to_date, + price: price, + currency: currency, + exchange_operating_mic: exchange_operating_mic ) - end + end.compact end end diff --git a/app/models/rule/action_executor/set_transaction_category.rb b/app/models/rule/action_executor/set_transaction_category.rb index ef186d96..6360e45a 100644 --- a/app/models/rule/action_executor/set_transaction_category.rb +++ b/app/models/rule/action_executor/set_transaction_category.rb @@ -17,15 +17,11 @@ class Rule::ActionExecutor::SetTransactionCategory < Rule::ActionExecutor end scope.each do |txn| - Rule.transaction do - txn.log_enrichment!( - attribute_name: "category_id", - attribute_value: category.id, - source: "rule" - ) - - txn.update!(category: category) - end + txn.enrich_attribute( + :category_id, + category.id, + source: "rule" + ) end end end diff --git a/app/models/rule/action_executor/set_transaction_merchant.rb b/app/models/rule/action_executor/set_transaction_merchant.rb index 492ece52..f343a79f 100644 --- a/app/models/rule/action_executor/set_transaction_merchant.rb +++ b/app/models/rule/action_executor/set_transaction_merchant.rb @@ -17,14 +17,11 @@ class Rule::ActionExecutor::SetTransactionMerchant < Rule::ActionExecutor end scope.each do |txn| - Rule.transaction do - txn.log_enrichment!( - attribute_name: "merchant_id", - attribute_value: merchant.id, - source: "rule" - ) - txn.update!(merchant: merchant) - end + txn.enrich_attribute( + :merchant_id, + merchant.id, + source: "rule" + ) end end end diff --git a/app/models/rule/action_executor/set_transaction_name.rb b/app/models/rule/action_executor/set_transaction_name.rb index 39f3ee26..1dd89fa3 100644 --- a/app/models/rule/action_executor/set_transaction_name.rb +++ b/app/models/rule/action_executor/set_transaction_name.rb @@ -16,14 +16,11 @@ class Rule::ActionExecutor::SetTransactionName < Rule::ActionExecutor end scope.each do |txn| - Rule.transaction do - txn.entry.log_enrichment!( - attribute_name: "name", - attribute_value: value, - source: "rule" - ) - txn.entry.update!(name: value) - end + txn.entry.enrich_attribute( + :name, + value, + source: "rule" + ) end end end diff --git a/app/models/rule/action_executor/set_transaction_tags.rb b/app/models/rule/action_executor/set_transaction_tags.rb index 4d539496..d74029ca 100644 --- a/app/models/rule/action_executor/set_transaction_tags.rb +++ b/app/models/rule/action_executor/set_transaction_tags.rb @@ -17,15 +17,11 @@ class Rule::ActionExecutor::SetTransactionTags < Rule::ActionExecutor end rows = scope.each do |txn| - Rule.transaction do - txn.log_enrichment!( - attribute_name: "tag_ids", - attribute_value: [ tag.id ], - source: "rule" - ) - - txn.update!(tag_ids: [ tag.id ]) - end + txn.enrich_attribute( + :tag_ids, + [ tag.id ], + source: "rule" + ) end end end diff --git a/app/models/security/price/importer.rb b/app/models/security/price/importer.rb new file mode 100644 index 00000000..bcee3762 --- /dev/null +++ b/app/models/security/price/importer.rb @@ -0,0 +1,155 @@ +class Security::Price::Importer + MissingSecurityPriceError = Class.new(StandardError) + MissingStartPriceError = Class.new(StandardError) + + def initialize(security:, security_provider:, start_date:, end_date:, clear_cache: false) + @security = security + @security_provider = security_provider + @start_date = start_date + @end_date = normalize_end_date(end_date) + @clear_cache = clear_cache + end + + # Constructs a daily series of prices for a single security over the date range. + # Returns the number of rows upserted. + def import_provider_prices + if !clear_cache && all_prices_exist? + Rails.logger.info("No new prices to sync for #{security.ticker} between #{start_date} and #{end_date}, skipping") + return 0 + end + + if provider_prices.empty? + Rails.logger.warn("Could not fetch prices for #{security.ticker} between #{start_date} and #{end_date} because provider returned no prices") + return 0 + end + + prev_price_value = start_price_value + + unless prev_price_value.present? + Rails.logger.error("Could not find a start price for #{security.ticker} on or before #{start_date}") + + Sentry.capture_exception(MissingStartPriceError.new("Could not determine start price for ticker")) do |scope| + scope.set_tags(security_id: security.id) + scope.set_context("security", { + id: security.id, + start_date: start_date + }) + end + + return 0 + end + + gapfilled_prices = effective_start_date.upto(end_date).map do |date| + db_price_value = db_prices[date]&.price + provider_price_value = provider_prices[date]&.price + provider_currency = provider_prices[date]&.currency + + chosen_price = if clear_cache + provider_price_value || db_price_value # overwrite when possible + else + db_price_value || provider_price_value # fill gaps + end + + # Gap-fill using LOCF (last observation carried forward) + chosen_price ||= prev_price_value + prev_price_value = chosen_price + + { + security_id: security.id, + date: date, + price: chosen_price, + currency: provider_currency || prev_price_currency || db_price_currency || "USD" + } + end + + upsert_rows(gapfilled_prices) + end + + private + attr_reader :security, :security_provider, :start_date, :end_date, :clear_cache + + def provider_prices + @provider_prices ||= begin + provider_fetch_start_date = effective_start_date - 5.days + + response = security_provider.fetch_security_prices( + symbol: security.ticker, + exchange_operating_mic: security.exchange_operating_mic, + start_date: provider_fetch_start_date, + end_date: end_date + ) + + if response.success? + response.data.index_by(&:date) + else + Rails.logger.warn("#{security_provider.class.name} could not fetch prices for #{security.ticker} between #{provider_fetch_start_date} and #{end_date}. Provider error: #{response.error.message}") + Sentry.capture_exception(MissingSecurityPriceError.new("Could not fetch prices for ticker"), level: :warning) do |scope| + scope.set_tags(security_id: security.id) + scope.set_context("security", { id: security.id, start_date: start_date, end_date: end_date }) + end + + {} + end + end + end + + def db_prices + @db_prices ||= Security::Price.where(security_id: security.id, date: start_date..end_date) + .order(:date) + .to_a + .index_by(&:date) + end + + def all_prices_exist? + db_prices.count == expected_count + end + + def expected_count + (start_date..end_date).count + end + + # Skip over ranges that already exist unless clearing cache + def effective_start_date + return start_date if clear_cache + + (start_date..end_date).detect { |d| !db_prices.key?(d) } || end_date + end + + def start_price_value + provider_price_value = provider_prices.select { |date, _| date <= start_date } + .max_by { |date, _| date } + &.last&.price + db_price_value = db_prices[start_date]&.price + provider_price_value || db_price_value + end + + def upsert_rows(rows) + batch_size = 200 + total_upsert_count = 0 + + rows.each_slice(batch_size) do |batch| + ids = Security::Price.upsert_all( + batch, + unique_by: %i[security_id date currency], + returning: [ "id" ] + ) + total_upsert_count += ids.count + end + + total_upsert_count + end + + def db_price_currency + db_prices.values.first&.currency + end + + def prev_price_currency + @prev_price_currency ||= provider_prices.values.first&.currency + end + + # Clamp to today (EST) so we never call our price API for a future date (our API is in EST/EDT timezone) + def normalize_end_date(requested_end_date) + today_est = Date.current.in_time_zone("America/New_York").to_date + [ requested_end_date, today_est ].min + end +end diff --git a/app/models/security/provided.rb b/app/models/security/provided.rb index b342c9e5..7927d6e6 100644 --- a/app/models/security/provided.rb +++ b/app/models/security/provided.rb @@ -1,6 +1,8 @@ module Security::Provided extend ActiveSupport::Concern + SecurityInfoMissingError = Class.new(StandardError) + class_methods do def provider registry = Provider::Registry.for_concept(:securities) @@ -28,44 +30,6 @@ module Security::Provided end end - def sync_provider_prices(start_date:, end_date: Date.current) - unless has_prices? - Rails.logger.warn("Security id=#{id} ticker=#{ticker} is not known by provider, skipping price sync") - return 0 - end - - unless provider.present? - Rails.logger.warn("No security provider configured, cannot sync prices for id=#{id} ticker=#{ticker}") - return 0 - end - - response = provider.fetch_security_prices(self, start_date: start_date, end_date: end_date) - - unless response.success? - Rails.logger.error("Provider error for sync_provider_prices with id=#{id} ticker=#{ticker}: #{response.error}") - return 0 - end - - fetched_prices = response.data.map do |price| - { - security_id: price.security.id, - date: price.date, - price: price.price, - currency: price.currency - } - end - - valid_prices = fetched_prices.reject do |price| - is_invalid = price[:date].nil? || price[:price].nil? || price[:currency].nil? - if is_invalid - Rails.logger.warn("Invalid price data for security_id=#{id}: Missing required fields in price record: #{price.inspect}") - end - is_invalid - end - - Security::Price.upsert_all(valid_prices, unique_by: %i[security_id date currency]) - end - def find_or_fetch_price(date: Date.current, cache: true) price = prices.find_by(date: date) @@ -87,6 +51,50 @@ module Security::Provided price end + def import_provider_details(clear_cache: false) + unless provider.present? + Rails.logger.warn("No provider configured for Security.import_provider_details") + return + end + + if self.name.present? && self.logo_url.present? && !clear_cache + return + end + + response = provider.fetch_security_info( + symbol: ticker, + exchange_operating_mic: exchange_operating_mic + ) + + if response.success? + update( + name: response.data.name, + logo_url: response.data.logo_url, + ) + else + Rails.logger.warn("Failed to fetch security info for #{ticker} from #{provider.class.name}: #{response.error.message}") + Sentry.capture_exception(SecurityInfoMissingError.new("Failed to get security info"), level: :warning) do |scope| + scope.set_tags(security_id: self.id) + scope.set_context("security", { id: self.id, provider_error: response.error.message }) + end + end + end + + def import_provider_prices(start_date:, end_date:, clear_cache: false) + unless provider.present? + Rails.logger.warn("No provider configured for Security.import_provider_prices") + return 0 + end + + Security::Price::Importer.new( + security: self, + security_provider: provider, + start_date: start_date, + end_date: end_date, + clear_cache: clear_cache + ).import_provider_prices + end + private def provider self.class.provider diff --git a/app/models/session.rb b/app/models/session.rb index ce26938a..66faa0f5 100644 --- a/app/models/session.rb +++ b/app/models/session.rb @@ -9,4 +9,14 @@ class Session < ApplicationRecord self.user_agent = Current.user_agent self.ip_address = Current.ip_address end + + def get_preferred_tab(tab_key) + data.dig("tab_preferences", tab_key) + end + + def set_preferred_tab(tab_key, tab_value) + data["tab_preferences"] ||= {} + data["tab_preferences"][tab_key] = tab_value + save! + end end diff --git a/app/models/stock_exchange.rb b/app/models/stock_exchange.rb deleted file mode 100644 index 93631426..00000000 --- a/app/models/stock_exchange.rb +++ /dev/null @@ -1,3 +0,0 @@ -class StockExchange < ApplicationRecord - scope :in_country, ->(country_code) { where(country_code: country_code) } -end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 5f96361e..90b5303d 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -17,6 +17,7 @@ class Subscription < ApplicationRecord validates :stripe_id, presence: true, if: :active? validates :trial_ends_at, presence: true, if: :trialing? + validates :family_id, uniqueness: true class << self def new_trial_ends_at diff --git a/app/models/sync.rb b/app/models/sync.rb index 4d923f12..be4bb8c3 100644 --- a/app/models/sync.rb +++ b/app/models/sync.rb @@ -1,4 +1,13 @@ class Sync < ApplicationRecord + # We run a cron that marks any syncs that have not been resolved in 24 hours as "stale" + # Syncs often become stale when new code is deployed and the worker restarts + STALE_AFTER = 24.hours + + # The max time that a sync will show in the UI (after 5 minutes) + VISIBLE_FOR = 5.minutes + + include AASM + Error = Class.new(StandardError) belongs_to :syncable, polymorphic: true @@ -6,12 +15,44 @@ class Sync < ApplicationRecord belongs_to :parent, class_name: "Sync", optional: true has_many :children, class_name: "Sync", foreign_key: :parent_id, dependent: :destroy - enum :status, { pending: "pending", syncing: "syncing", completed: "completed", failed: "failed" } - scope :ordered, -> { order(created_at: :desc) } + scope :incomplete, -> { where("syncs.status IN (?)", %w[pending syncing]) } + scope :visible, -> { incomplete.where("syncs.created_at > ?", VISIBLE_FOR.ago) } - def child? - parent_id.present? + validate :window_valid + + # Sync state machine + aasm column: :status, timestamps: true do + state :pending, initial: true + state :syncing + state :completed + state :failed + state :stale + + after_all_transitions :log_status_change + + event :start, after_commit: :report_warnings do + transitions from: :pending, to: :syncing + end + + event :complete do + transitions from: :syncing, to: :completed + end + + event :fail do + transitions from: :syncing, to: :failed + end + + # Marks a sync that never completed within the expected time window + event :mark_stale do + transitions from: %i[pending syncing], to: :stale + end + end + + class << self + def clean + incomplete.where("syncs.created_at < ?", STALE_AFTER.ago).find_each(&:mark_stale!) + end end def perform @@ -19,43 +60,106 @@ class Sync < ApplicationRecord start! begin - syncable.sync_data(self, start_date: start_date) - - complete! - Rails.logger.info("Sync completed, starting post-sync") - syncable.post_sync(self) - Rails.logger.info("Post-sync completed") - rescue StandardError => error - fail! error, report_error: true + syncable.perform_sync(self) + rescue => e + fail! + update(error: e.message) + report_error(e) + ensure + finalize_if_all_children_finalized end end end - private - def start! - Rails.logger.info("Starting sync") - update! status: :syncing - end + # Finalizes the current sync AND parent (if it exists) + def finalize_if_all_children_finalized + Sync.transaction do + lock! - def complete! - Rails.logger.info("Sync completed") - update! status: :completed, last_ran_at: Time.current - end + # If this is the "parent" and there are still children running, don't finalize. + return unless all_children_finalized? - def fail!(error, report_error: false) - Rails.logger.error("Sync failed: #{error.message}") - - if report_error - Sentry.capture_exception(error) do |scope| - scope.set_context("sync", { id: id, syncable_type: syncable_type, syncable_id: syncable_id }) - scope.set_tags(sync_id: id) + if syncing? + if has_failed_children? + fail! + else + complete! end end - update!( - status: :failed, - error: error.message, - last_ran_at: Time.current - ) + # If we make it here, the sync is finalized. Run post-sync, regardless of failure/success. + perform_post_sync + end + + # If this sync has a parent, try to finalize it so the child status propagates up the chain. + parent&.finalize_if_all_children_finalized + end + + # If a sync is pending, we can adjust the window if new syncs are created with a wider window. + def expand_window_if_needed(new_window_start_date, new_window_end_date) + return unless pending? + return if self.window_start_date.nil? && self.window_end_date.nil? # already as wide as possible + + earliest_start_date = if self.window_start_date && new_window_start_date + [ self.window_start_date, new_window_start_date ].min + else + nil + end + + latest_end_date = if self.window_end_date && new_window_end_date + [ self.window_end_date, new_window_end_date ].max + else + nil + end + + update( + window_start_date: earliest_start_date, + window_end_date: latest_end_date + ) + end + + private + def log_status_change + Rails.logger.info("changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})") + end + + def has_failed_children? + children.failed.any? + end + + def all_children_finalized? + children.incomplete.empty? + end + + def perform_post_sync + Rails.logger.info("Performing post-sync for #{syncable_type} (#{syncable.id})") + syncable.perform_post_sync + syncable.broadcast_sync_complete + rescue => e + Rails.logger.error("Error performing post-sync for #{syncable_type} (#{syncable.id}): #{e.message}") + report_error(e) + end + + def report_error(error) + Sentry.capture_exception(error) do |scope| + scope.set_tags(sync_id: id) + end + end + + def report_warnings + todays_sync_count = syncable.syncs.where(created_at: Date.current.all_day).count + + if todays_sync_count > 10 + Sentry.capture_exception( + Error.new("#{syncable_type} (#{syncable.id}) has exceeded 10 syncs today (count: #{todays_sync_count})"), + level: :warning + ) + end + end + + def window_valid + if window_start_date && window_end_date && window_start_date > window_end_date + errors.add(:window_end_date, "must be greater than window_start_date") + end end end diff --git a/app/models/trade_builder.rb b/app/models/trade_builder.rb index 5a2f9df1..cf9800e5 100644 --- a/app/models/trade_builder.rb +++ b/app/models/trade_builder.rb @@ -129,13 +129,9 @@ class TradeBuilder def security ticker_symbol, exchange_operating_mic = ticker.present? ? ticker.split("|") : [ manual_ticker, nil ] - security = Security.find_or_create_by!( + Security.find_or_create_by!( ticker: ticker_symbol, exchange_operating_mic: exchange_operating_mic ) - - FetchSecurityInfoJob.perform_later(security.id) - - security end end diff --git a/app/views/accounts/_account.html.erb b/app/views/accounts/_account.html.erb index 07ffd3d5..475f953e 100644 --- a/app/views/accounts/_account.html.erb +++ b/app/views/accounts/_account.html.erb @@ -30,9 +30,13 @@ <% end %>
-

"> - <%= format_money account.balance_money %> -

+ <% if account.syncing? %> +
+ <% else %> +

"> + <%= format_money account.balance_money %> +

+ <% end %> <% unless account.scheduled_for_deletion? %> <%= styled_form_with model: account, data: { turbo_frame: "_top", controller: "auto-submit-form" } do |f| %> diff --git a/app/views/accounts/_account_sidebar_tabs.html.erb b/app/views/accounts/_account_sidebar_tabs.html.erb index 9ca2781c..056e7be1 100644 --- a/app/views/accounts/_account_sidebar_tabs.html.erb +++ b/app/views/accounts/_account_sidebar_tabs.html.erb @@ -1,6 +1,6 @@ -<%# locals: (family:, active_account_group_tab:) %> +<%# locals: (family:, active_tab:, mobile: false) %> -
+
<% if family.missing_data_provider? %>
@@ -21,73 +21,71 @@
<% end %> -
- <%= render TabsComponent.new(active_tab: active_account_group_tab, url_param_key: "account_group_tab", testid: "account-sidebar-tabs") do |tabs| %> - <% tabs.with_nav do |nav| %> - <% nav.with_btn(id: "assets", label: "Assets") %> - <% nav.with_btn(id: "debts", label: "Debts") %> - <% nav.with_btn(id: "all", label: "All") %> - <% end %> + <%= render TabsComponent.new(active_tab: active_tab, session_key: "account_sidebar_tab", testid: "account-sidebar-tabs") do |tabs| %> + <% tabs.with_nav do |nav| %> + <% nav.with_btn(id: "asset", label: "Assets") %> + <% nav.with_btn(id: "liability", label: "Debts") %> + <% nav.with_btn(id: "all", label: "All") %> + <% end %> - <% tabs.with_panel(tab_id: "assets") do %> -
- <%= render LinkComponent.new( - text: "New asset", - variant: "ghost", - href: new_account_path(step: "method_select", classification: "asset"), - icon: "plus", - frame: :modal, - full_width: true, - class: "justify-start" - ) %> - -
- <% family.balance_sheet.account_groups("asset").each do |group| %> - <%= render "accounts/accountable_group", account_group: group %> - <% end %> -
-
- <% end %> - - <% tabs.with_panel(tab_id: "debts") do %> -
- <%= render LinkComponent.new( - text: "New debt", + <% tabs.with_panel(tab_id: "asset") do %> +
+ <%= render LinkComponent.new( + text: "New asset", variant: "ghost", - href: new_account_path(step: "method_select", classification: "liability"), + href: new_account_path(step: "method_select", classification: "asset"), icon: "plus", frame: :modal, - full_width: true, - class: "justify-start" - ) %> - -
- <% family.balance_sheet.account_groups("liability").each do |group| %> - <%= render "accounts/accountable_group", account_group: group %> - <% end %> -
-
- <% end %> - - <% tabs.with_panel(tab_id: "all") do %> -
- <%= render LinkComponent.new( - text: "New account", - variant: "ghost", full_width: true, - href: new_account_path(step: "method_select"), - icon: "plus", - frame: :modal, - class: "justify-start" - ) %> + class: "justify-start" + ) %> -
- <% family.balance_sheet.account_groups.each do |group| %> - <%= render "accounts/accountable_group", account_group: group %> - <% end %> -
+
+ <% family.balance_sheet.account_groups("asset").each do |group| %> + <%= render "accounts/accountable_group", account_group: group, mobile: mobile %> + <% end %>
- <% end %> +
<% end %> -
+ + <% tabs.with_panel(tab_id: "liability") do %> +
+ <%= render LinkComponent.new( + text: "New debt", + variant: "ghost", + href: new_account_path(step: "method_select", classification: "liability"), + icon: "plus", + frame: :modal, + full_width: true, + class: "justify-start" + ) %> + +
+ <% family.balance_sheet.account_groups("liability").each do |group| %> + <%= render "accounts/accountable_group", account_group: group, mobile: mobile %> + <% end %> +
+
+ <% end %> + + <% tabs.with_panel(tab_id: "all") do %> +
+ <%= render LinkComponent.new( + text: "New account", + variant: "ghost", + full_width: true, + href: new_account_path(step: "method_select"), + icon: "plus", + frame: :modal, + class: "justify-start" + ) %> + +
+ <% family.balance_sheet.account_groups.each do |group| %> + <%= render "accounts/accountable_group", account_group: group, mobile: mobile %> + <% end %> +
+
+ <% end %> + <% end %>
diff --git a/app/views/accounts/_accountable_group.html.erb b/app/views/accounts/_accountable_group.html.erb index e77dc533..b5393856 100644 --- a/app/views/accounts/_accountable_group.html.erb +++ b/app/views/accounts/_accountable_group.html.erb @@ -1,47 +1,68 @@ -<%# locals: (account_group:) %> +<%# locals: (account_group:, mobile: false, open: nil, **args) %> -<%= render DisclosureComponent.new(title: account_group.name, align: :left, open: account_group.accounts.any? { |account| page_active?(account_path(account)) }) do |disclosure| %> - <% disclosure.with_summary_content do %> -
- <%= tag.p format_money(account_group.total_money), class: "text-sm font-medium text-primary" %> +
"> + <% is_open = open.nil? ? account_group.accounts.any? { |account| page_active?(account_path(account)) } : open %> + <%= render DisclosureComponent.new(title: account_group.name, align: :left, open: is_open) do |disclosure| %> + <% disclosure.with_summary_content do %> +
+ <% if account_group.syncing? %> +
+
+
+
+
+
+ <% else %> + <%= tag.p format_money(account_group.total_money), class: "text-sm font-medium text-primary" %> + <%= turbo_frame_tag "#{account_group.key}_sparkline", src: accountable_sparkline_path(account_group.key), loading: "lazy" do %> +
+
+
+ <% end %> + <% end %> +
+ <% end %> - <%= turbo_frame_tag "#{account_group.key}_sparkline", src: accountable_sparkline_path(account_group.key), loading: "lazy" do %> -
- <%= render "shared/ruler", classes: "w-full" %> -
- <% end %> -
- <% end %> - -
- <% account_group.accounts.each do |account| %> - <%= link_to account_path(account), +
+ <% account_group.accounts.each do |account| %> + <%= link_to account_path(account), class: class_names( "block flex items-center gap-2 px-3 py-2 rounded-lg", page_active?(account_path(account)) ? "bg-container" : "hover:bg-surface-hover" - ), - data: { sidebar_tabs_target: "account", action: "click->sidebar-tabs#select" }, + ), title: account.name do %> - <%= render "accounts/logo", account: account, size: "sm", color: account_group.color %> + <%= render "accounts/logo", account: account, size: "sm", color: account_group.color %> -
- <%= tag.p account.name, class: "text-sm text-primary font-medium mb-0.5 truncate" %> - <%= tag.p account.short_subtype_label, class: "text-sm text-secondary truncate" %> -
+
+ <%= tag.p account.name, class: "text-sm text-primary font-medium mb-0.5 truncate" %> + <%= tag.p account.short_subtype_label, class: "text-sm text-secondary truncate" %> +
-
- <%= tag.p format_money(account.balance_money), class: "text-sm font-medium text-primary whitespace-nowrap" %> - - <%= turbo_frame_tag dom_id(account, :sparkline), src: sparkline_account_path(account), loading: "lazy" do %> - <%= render "shared/ruler", classes: "w-6" %> + <% if account.syncing? %> +
+
+
+
+
+
+
+
+ <% else %> +
+ <%= tag.p format_money(account.balance_money), class: "text-sm font-medium text-primary whitespace-nowrap" %> + <%= turbo_frame_tag dom_id(account, :sparkline), src: sparkline_account_path(account), loading: "lazy" do %> +
+
+
+ <% end %> +
<% end %> -
+ <% end %> <% end %> - <% end %> -
+
-
- <%= render LinkComponent.new( +
+ <%= render LinkComponent.new( href: new_polymorphic_path(account_group.key, step: "method_select"), text: "New #{account_group.name.downcase.singularize}", icon: "plus", @@ -50,5 +71,6 @@ frame: :modal, class: "justify-start" ) %> -
-<% end %> +
+ <% end %> +
diff --git a/app/views/accounts/_chart_loader.html.erb b/app/views/accounts/_chart_loader.html.erb index f6a9c852..b080329f 100644 --- a/app/views/accounts/_chart_loader.html.erb +++ b/app/views/accounts/_chart_loader.html.erb @@ -1,5 +1,7 @@ -
+
+
-
-

Loading...

+ +
+
diff --git a/app/views/accounts/chart.html.erb b/app/views/accounts/chart.html.erb index 11dcbaac..b181ef20 100644 --- a/app/views/accounts/chart.html.erb +++ b/app/views/accounts/chart.html.erb @@ -2,21 +2,25 @@ <% trend = series.trend %> <%= turbo_frame_tag dom_id(@account, :chart_details) do %> -
- <%= render partial: "shared/trend_change", locals: { trend: trend, comparison_label: @period.comparison_label } %> -
+ <% if @account.syncing? %> + <%= render "accounts/chart_loader" %> + <% else %> +
+ <%= render partial: "shared/trend_change", locals: { trend: trend, comparison_label: @period.comparison_label } %> +
-
- <% if series.any? %> -
+ <% if series.any? %> +
- <% else %> -
-

<%= t(".data_not_available") %>

-
- <% end %> -
+ <% else %> +
+

<%= t(".data_not_available") %>

+
+ <% end %> +
+ <% end %> <% end %> diff --git a/app/views/accounts/index.html.erb b/app/views/accounts/index.html.erb index b4c78332..35e647ea 100644 --- a/app/views/accounts/index.html.erb +++ b/app/views/accounts/index.html.erb @@ -2,17 +2,6 @@

<%= t(".accounts") %>

- <% if Rails.env.development? %> - <%= render ButtonComponent.new( - text: "Sync all", - href: sync_all_accounts_path, - method: :post, - variant: "outline", - disabled: Current.family.syncing?, - icon: "refresh-cw", - ) %> - <% end %> - <%= render LinkComponent.new( text: "New account", href: new_account_path(return_to: accounts_path), diff --git a/app/views/accounts/index/_account_groups.erb b/app/views/accounts/index/_account_groups.erb index 8192c1ab..6c1ba1ed 100644 --- a/app/views/accounts/index/_account_groups.erb +++ b/app/views/accounts/index/_account_groups.erb @@ -6,7 +6,10 @@

<%= Accountable.from_type(group).display_name %>

·

<%= accounts.count %>

-

<%= totals_by_currency(collection: accounts, money_method: :balance_money) %>

+ + <% unless accounts.any?(&:syncing?) %> +

<%= totals_by_currency(collection: accounts, money_method: :balance_money) %>

+ <% end %>
<% accounts.each do |account| %> diff --git a/app/views/accounts/show/_chart.html.erb b/app/views/accounts/show/_chart.html.erb index 08e67e84..0f63c9f1 100644 --- a/app/views/accounts/show/_chart.html.erb +++ b/app/views/accounts/show/_chart.html.erb @@ -1,17 +1,24 @@ -<%# locals: (account:, title: nil, tooltip: nil, chart_view: nil, **args) %> +<%# locals: (account:, tooltip: nil, chart_view: nil, **args) %> <% period = @period || Period.last_30_days %> <% default_value_title = account.asset? ? t(".balance") : t(".owed") %> -
+
- <%= tag.p title || default_value_title, class: "text-sm font-medium text-secondary" %> - <%= tooltip %> + <%= tag.p account.investment? ? "Total value" : default_value_title, class: "text-sm font-medium text-secondary" %> + + <% if !account.syncing? && account.investment? %> + <%= render "investments/value_tooltip", balance: account.balance_money, holdings: account.balance_money - account.cash_balance_money, cash: account.cash_balance_money %> + <% end %>
- <%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium truncate" %> + <% if account.syncing? %> +
+ <% else %> + <%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium truncate" %> + <% end %>
<%= form_with url: request.path, method: :get, data: { controller: "auto-submit-form" } do |form| %> diff --git a/app/views/accounts/show/_template.html.erb b/app/views/accounts/show/_template.html.erb index cfac9402..efc7ea5a 100644 --- a/app/views/accounts/show/_template.html.erb +++ b/app/views/accounts/show/_template.html.erb @@ -1,8 +1,8 @@ -<%# locals: (account:, header: nil, chart: nil, tabs: nil) %> +<%# locals: (account:, header: nil, chart: nil, chart_view: nil, tabs: nil) %> <%= turbo_stream_from account %> -<%= turbo_frame_tag dom_id(account) do %> +<%= turbo_frame_tag dom_id(account, :container) do %> <%= tag.div class: "space-y-4 pb-32" do %> <% if header.present? %> <%= header %> @@ -13,7 +13,7 @@ <% if chart.present? %> <%= chart %> <% else %> - <%= render "accounts/show/chart", account: account %> + <%= render "accounts/show/chart", account: account, chart_view: chart_view %> <% end %>
diff --git a/app/views/category/dropdowns/show.html.erb b/app/views/category/dropdowns/show.html.erb index e321e167..4329918f 100644 --- a/app/views/category/dropdowns/show.html.erb +++ b/app/views/category/dropdowns/show.html.erb @@ -2,13 +2,13 @@
- " - autocomplete="nope" - type="search" - class="bg-container placeholder:text-sm placeholder:text-secondary font-normal h-10 relative pl-10 w-full border-none rounded-lg focus:outline-hidden focus:ring-0" - data-list-filter-target="input" - data-action="list-filter#filter" /> + " + autocomplete="nope" + type="search" + class="bg-container placeholder:text-sm placeholder:text-secondary font-normal h-10 relative pl-10 w-full border-none rounded-lg focus:outline-hidden focus:ring-0" + data-list-filter-target="input" + data-action="list-filter#filter"> <%= icon("search", class: "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2") %>
diff --git a/app/views/holdings/_cash.html.erb b/app/views/holdings/_cash.html.erb index a279d9ab..702f40d0 100644 --- a/app/views/holdings/_cash.html.erb +++ b/app/views/holdings/_cash.html.erb @@ -19,8 +19,13 @@
<% cash_weight = account.balance.zero? ? 0 : account.cash_balance / account.balance * 100 %> - <%= render "shared/progress_circle", progress: cash_weight %> - <%= tag.p number_to_percentage(cash_weight, precision: 1) %> + + <% if account.syncing? %> +
+ <% else %> + <%= render "shared/progress_circle", progress: cash_weight %> + <%= tag.p number_to_percentage(cash_weight, precision: 1) %> + <% end %>
@@ -28,7 +33,13 @@
- <%= tag.p format_money account.cash_balance_money %> + <% if account.syncing? %> +
+
+
+ <% else %> + <%= tag.p format_money account.cash_balance_money %> + <% end %>
diff --git a/app/views/holdings/_holding.html.erb b/app/views/holdings/_holding.html.erb index 5fe0e4c9..c8a2ac59 100644 --- a/app/views/holdings/_holding.html.erb +++ b/app/views/holdings/_holding.html.erb @@ -17,7 +17,9 @@
- <% if holding.weight %> + <% if holding.account.syncing? %> +
+ <% elsif holding.weight %> <%= render "shared/progress_circle", progress: holding.weight %> <%= tag.p number_to_percentage(holding.weight, precision: 1) %> <% else %> @@ -26,21 +28,39 @@
- <%= tag.p format_money holding.avg_cost %> - <%= tag.p t(".per_share"), class: "font-normal text-secondary" %> -
- -
- <% if holding.amount_money %> - <%= tag.p format_money holding.amount_money %> + <% if holding.account.syncing? %> +
+
+
<% else %> - <%= tag.p "--", class: "text-secondary" %> + <%= tag.p format_money holding.avg_cost %> + <%= tag.p t(".per_share"), class: "font-normal text-secondary" %> <% end %> - <%= tag.p t(".shares", qty: number_with_precision(holding.qty, precision: 1)), class: "font-normal text-secondary" %>
- <% if holding.trend %> + <% if holding.account.syncing? %> +
+
+
+
+ <% else %> + <% if holding.amount_money %> + <%= tag.p format_money holding.amount_money %> + <% else %> + <%= tag.p "--", class: "text-secondary" %> + <% end %> + <%= tag.p t(".shares", qty: number_with_precision(holding.qty, precision: 1)), class: "font-normal text-secondary" %> + <% end %> +
+ +
+ <% if holding.account.syncing? %> +
+
+
+
+ <% elsif holding.trend %> <%= tag.p format_money(holding.trend.value), style: "color: #{holding.trend.color};" %> <%= tag.p "(#{number_to_percentage(holding.trend.percent, precision: 1)})", style: "color: #{holding.trend.color};" %> <% else %> diff --git a/app/views/investments/show.html.erb b/app/views/investments/show.html.erb index 7bd7da3b..84be8629 100644 --- a/app/views/investments/show.html.erb +++ b/app/views/investments/show.html.erb @@ -1,25 +1,7 @@ -<%= turbo_stream_from @account %> - -<%= turbo_frame_tag dom_id(@account) do %> - <%= tag.div class: "space-y-4" do %> - <%= render "accounts/show/header", account: @account %> - - <%= render "accounts/show/chart", - account: @account, - title: t(".chart_title"), - chart_view: @chart_view, - tooltip: render( - "investments/value_tooltip", - balance: @account.balance_money, - holdings: @account.balance_money - @account.cash_balance_money, - cash: @account.cash_balance_money - ) %> - -
- <%= render "accounts/show/tabs", account: @account, tabs: [ - { key: "activity", contents: render("accounts/show/activity", account: @account) }, - { key: "holdings", contents: render("investments/holdings_tab", account: @account) }, - ] %> -
- <% end %> -<% end %> +<%= render "accounts/show/template", + account: @account, + chart_view: @chart_view, + tabs: render("accounts/show/tabs", account: @account, tabs: [ + { key: "activity", contents: render("accounts/show/activity", account: @account) }, + { key: "holdings", contents: render("investments/holdings_tab", account: @account) }, + ]) %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 53e7f666..e8fc202e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -26,7 +26,8 @@ <%= render( "accounts/account_sidebar_tabs", family: Current.family, - active_account_group_tab: params[:account_group_tab] || "assets" + active_tab: @account_group_tab, + mobile: true ) %>
@@ -80,8 +81,8 @@ <%= yield :sidebar %> <% else %>
-
- <%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: params[:account_group_tab] || "assets" %> +
+ <%= render "accounts/account_sidebar_tabs", family: Current.family, active_tab: @account_group_tab %>
<% if Current.family.trialing? && !self_hosted? %> diff --git a/app/views/layouts/shared/_htmldoc.html.erb b/app/views/layouts/shared/_htmldoc.html.erb index fadcd396..30887f7b 100644 --- a/app/views/layouts/shared/_htmldoc.html.erb +++ b/app/views/layouts/shared/_htmldoc.html.erb @@ -23,10 +23,6 @@ <%= render_flash_notifications %>
- - <% if Current.family&.syncing? %> - <%= render "shared/notifications/loading", id: "syncing-notice", message: "Syncing accounts data..." %> - <% end %>
diff --git a/app/views/loans/_overview.html.erb b/app/views/loans/_overview.html.erb index f04ccc49..e1649583 100644 --- a/app/views/loans/_overview.html.erb +++ b/app/views/loans/_overview.html.erb @@ -11,7 +11,7 @@ <%= summary_card title: t(".interest_rate") do %> <% if account.loan.interest_rate.present? %> - <%= number_to_percentage(account.loan.interest_rate, precision: 2) %> + <%= number_to_percentage(account.loan.interest_rate, precision: 3) %> <% else %> <%= t(".unknown") %> <% end %> diff --git a/app/views/onboardings/goals.html.erb b/app/views/onboardings/goals.html.erb index f08f51cb..9ea04b07 100644 --- a/app/views/onboardings/goals.html.erb +++ b/app/views/onboardings/goals.html.erb @@ -26,7 +26,7 @@
<% [ - { icon: "layers", label: "See all my accounts in one piece", value: "unified_accounts" }, + { icon: "layers", label: "See all my accounts in one place", value: "unified_accounts" }, { icon: "banknote", label: "Understand cashflow and expenses", value: "cashflow" }, { icon: "pie-chart", label: "Manage financial plans and budgeting", value: "budgeting" }, { icon: "users", label: "Manage finances with a partner", value: "partner" }, diff --git a/app/views/pages/dashboard.html.erb b/app/views/pages/dashboard.html.erb index 569caaa2..82de3bd1 100644 --- a/app/views/pages/dashboard.html.erb +++ b/app/views/pages/dashboard.html.erb @@ -25,16 +25,27 @@
<% if Current.family.accounts.any? %> -
- <%= render partial: "pages/dashboard/net_worth_chart", locals: { series: @balance_sheet.net_worth_series(period: @period), period: @period } %> +
+ <%= render partial: "pages/dashboard/net_worth_chart", locals: { + balance_sheet: @balance_sheet, + period: @period + } %>
+
+ <%= render "pages/dashboard/balance_sheet", balance_sheet: @balance_sheet %> +
+ + <%= turbo_frame_tag "cashflow_sankey_section" do %> +
+ <%= render partial: "pages/dashboard/cashflow_sankey", locals: { + sankey_data: @cashflow_sankey_data, + period: @cashflow_period + } %> +
+ <% end %> <% else %> -
+
<%= render "pages/dashboard/no_accounts_graph_placeholder" %>
<% end %> - -
- <%= render "pages/dashboard/balance_sheet", balance_sheet: @balance_sheet %> -
diff --git a/app/views/pages/dashboard/_balance_sheet.html.erb b/app/views/pages/dashboard/_balance_sheet.html.erb index 3c853970..46a90297 100644 --- a/app/views/pages/dashboard/_balance_sheet.html.erb +++ b/app/views/pages/dashboard/_balance_sheet.html.erb @@ -1,6 +1,6 @@ -<%# locals: (balance_sheet:) %> +<%# locals: (balance_sheet:, **args) %> -
+
<% balance_sheet.classification_groups.each do |classification_group| %>

@@ -11,27 +11,39 @@ <% if classification_group.account_groups.any? %> · - <%= classification_group.total_money.format(precision: 0) %> + <% if classification_group.syncing? %> +
+
+
+ <% else %> + <%= classification_group.total_money.format(precision: 0) %> + <% end %> <% end %>

<% if classification_group.account_groups.any? %> +
<% classification_group.account_groups.each do |account_group| %>
<% end %>
-
- <% classification_group.account_groups.each do |account_group| %> -
-
-

<%= account_group.name %>

- · -

<%= number_to_percentage(account_group.weight, precision: 0) %>

-
- <% end %> -
+ + <% if classification_group.syncing? %> +

Calculating latest balance data...

+ <% else %> +
+ <% classification_group.account_groups.each do |account_group| %> +
+
+

<%= account_group.name %>

+ · +

<%= number_to_percentage(account_group.weight, precision: 0) %>

+
+ <% end %> +
+ <% end %>
@@ -57,15 +69,27 @@

<%= account_group.name %>

-
-
- <%= render "pages/dashboard/group_weight", weight: account_group.weight, color: account_group.color %> -
+ <% if account_group.syncing? %> +
+
+
+
-
-

<%= format_money(account_group.total_money) %>

+
+
+
-
+ <% else %> +
+
+ <%= render "pages/dashboard/group_weight", weight: account_group.weight, color: account_group.color %> +
+ +
+

<%= format_money(account_group.total_money) %>

+
+
+ <% end %>
@@ -77,19 +101,31 @@ <%= link_to account.name, account_path(account) %>
-
-
- <%= render "pages/dashboard/group_weight", weight: account.weight, color: account_group.color %> -
+ <% if account.syncing? %> +
+
+
+
-
-

<%= format_money(account.balance_money) %>

+
+
+
-
+ <% else %> +
+
+ <%= render "pages/dashboard/group_weight", weight: account.weight, color: account_group.color %> +
+ +
+

<%= format_money(account.balance_money) %>

+
+
+ <% end %>
<% if idx < account_group.accounts.size - 1 %> - <%= render "shared/ruler", classes: "ml-12 mr-4" %> + <%= render "shared/ruler", classes: "ml-21 mr-4" %> <% end %> <% end %>
diff --git a/app/views/pages/dashboard/_cashflow_sankey.html.erb b/app/views/pages/dashboard/_cashflow_sankey.html.erb new file mode 100644 index 00000000..4bddc3eb --- /dev/null +++ b/app/views/pages/dashboard/_cashflow_sankey.html.erb @@ -0,0 +1,24 @@ +<%# locals: (sankey_data:, period:) %> +
+
+

+ Cashflow +

+ + <%= form_with url: root_path, method: :get, data: { controller: "auto-submit-form", turbo_frame: "cashflow_sankey_section" } do |form| %> + <%= form.select :cashflow_period, + Period.as_options, + { selected: period.key }, + data: { "auto-submit-form-target": "auto" }, + class: "bg-container border border-secondary font-medium rounded-lg px-3 py-2 text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0" %> + <% end %> +
+ +
+
+
+
\ No newline at end of file diff --git a/app/views/pages/dashboard/_net_worth_chart.html.erb b/app/views/pages/dashboard/_net_worth_chart.html.erb index a8870cac..a6b65852 100644 --- a/app/views/pages/dashboard/_net_worth_chart.html.erb +++ b/app/views/pages/dashboard/_net_worth_chart.html.erb @@ -1,37 +1,55 @@ -<%# locals: (series:, period:) %> +<%# locals: (balance_sheet:, period:, **args) %> -
-
+
+ <% series = balance_sheet.net_worth_series(period: period) %> +
-

<%= t(".title") %>

-

- <%= series.current.format %> -

- <% if series.trend.nil? %> -

<%= t(".data_not_available") %>

- <% else %> - <%= render partial: "shared/trend_change", locals: { trend: series.trend, comparison_label: period.comparison_label } %> - <% end %> -
-
+
+

<%= t(".title") %>

- <%= form_with url: root_path, method: :get, data: { controller: "auto-submit-form" } do |form| %> - <%= form.select :period, + <% if balance_sheet.syncing? %> +
+
+
+
+ <% else %> +

+ <%= series.current.format %> +

+ + <% if series.trend.nil? %> +

<%= t(".data_not_available") %>

+ <% else %> + <%= render partial: "shared/trend_change", locals: { trend: series.trend, comparison_label: period.comparison_label } %> + <% end %> + <% end %> +
+
+ + <%= form_with url: root_path, method: :get, data: { controller: "auto-submit-form" } do |form| %> + <%= form.select :period, Period.as_options, { selected: period.key }, data: { "auto-submit-form-target": "auto" }, class: "bg-container border border-secondary font-medium rounded-lg px-3 py-2 text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0" %> - <% end %> -
+ <% end %> +
-<% if series.any? %> -
+
+
+
+ <% else %> + <% if series.any? %> +
-<% else %> -
-

<%= t(".data_not_available") %>

-
-<% end %> + <% else %> +
+

<%= t(".data_not_available") %>

+
+ <% end %> + <% end %> +
diff --git a/app/views/rule/conditions/_condition_group.html.erb b/app/views/rule/conditions/_condition_group.html.erb index e04a09f7..77383833 100644 --- a/app/views/rule/conditions/_condition_group.html.erb +++ b/app/views/rule/conditions/_condition_group.html.erb @@ -28,13 +28,13 @@ <%# Sub-condition template, used by Stimulus controller to add new sub-conditions dynamically %>
    - <%= form.fields_for :sub_conditions do |scf| %> + <%= form.fields_for :sub_conditions, condition.sub_conditions.select(&:persisted?) do |scf| %> <%= render "rule/conditions/condition", form: scf, show_prefix: false %> <% end %>
diff --git a/app/views/rules/_rule.html.erb b/app/views/rules/_rule.html.erb index 3526911d..9f35c465 100644 --- a/app/views/rules/_rule.html.erb +++ b/app/views/rules/_rule.html.erb @@ -50,7 +50,7 @@ <% if rule.effective_date.nil? %> All past and future <%= rule.resource_type.pluralize %> <% else %> - <%= rule.resource_type.pluralize %> on or after <%= rule.effective_date.strftime('%b %-d, %Y') %> + <%= rule.resource_type.pluralize %> on or after <%= rule.effective_date.strftime("%b %-d, %Y") %> <% end %>

diff --git a/app/views/rules/confirm.html.erb b/app/views/rules/confirm.html.erb index 28be9496..03311791 100644 --- a/app/views/rules/confirm.html.erb +++ b/app/views/rules/confirm.html.erb @@ -1,5 +1,5 @@ <%= render DialogComponent.new(reload_on_close: true) do |dialog| %> - <% + <% title = if @rule.name.present? "Confirm changes to \"#{@rule.name}\"" else @@ -7,7 +7,7 @@ end %> <% dialog.with_header(title: title) %> - + <% dialog.with_body do %>

You are about to apply this rule to diff --git a/app/views/rules/edit.html.erb b/app/views/rules/edit.html.erb index f73edc90..91dea816 100644 --- a/app/views/rules/edit.html.erb +++ b/app/views/rules/edit.html.erb @@ -1,7 +1,7 @@ <%= link_to "Back to rules", rules_path %> <%= render DialogComponent.new do |dialog| %> - <% + <% title = if @rule.name.present? "Edit #{@rule.resource_type} rule \"#{@rule.name}\"" else diff --git a/app/views/settings/_settings_nav.html.erb b/app/views/settings/_settings_nav.html.erb index acd82489..83bceea4 100644 --- a/app/views/settings/_settings_nav.html.erb +++ b/app/views/settings/_settings_nav.html.erb @@ -1,31 +1,31 @@ <% nav_sections = [ { - header: t('.general_section_title'), + header: t(".general_section_title"), items: [ - { label: t('.profile_label'), path: settings_profile_path, icon: 'circle-user' }, - { label: t('.preferences_label'), path: settings_preferences_path, icon: 'bolt' }, - { label: t('.security_label'), path: settings_security_path, icon: 'shield-check' }, - { label: t('.self_hosting_label'), path: settings_hosting_path, icon: 'database', if: self_hosted? }, - { label: t('.billing_label'), path: settings_billing_path, icon: 'circle-dollar-sign', if: !self_hosted? }, - { label: t('.accounts_label'), path: accounts_path, icon: 'layers' }, - { label: t('.imports_label'), path: imports_path, icon: 'download' } + { label: t(".profile_label"), path: settings_profile_path, icon: "circle-user" }, + { label: t(".preferences_label"), path: settings_preferences_path, icon: "bolt" }, + { label: t(".security_label"), path: settings_security_path, icon: "shield-check" }, + { label: t(".self_hosting_label"), path: settings_hosting_path, icon: "database", if: self_hosted? }, + { label: t(".billing_label"), path: settings_billing_path, icon: "circle-dollar-sign", if: !self_hosted? }, + { label: t(".accounts_label"), path: accounts_path, icon: "layers" }, + { label: t(".imports_label"), path: imports_path, icon: "download" } ] }, { - header: t('.transactions_section_title'), + header: t(".transactions_section_title"), items: [ - { label: t('.tags_label'), path: tags_path, icon: 'tags' }, - { label: t('.categories_label'), path: categories_path, icon: 'shapes' }, - { label: t('.rules_label'), path: rules_path, icon: 'git-branch' }, - { label: t('.merchants_label'), path: family_merchants_path, icon: 'store' } + { label: t(".tags_label"), path: tags_path, icon: "tags" }, + { label: t(".categories_label"), path: categories_path, icon: "shapes" }, + { label: t(".rules_label"), path: rules_path, icon: "git-branch" }, + { label: t(".merchants_label"), path: family_merchants_path, icon: "store" } ] }, { - header: t('.other_section_title'), + header: t(".other_section_title"), items: [ - { label: t('.whats_new_label'), path: changelog_path, icon: 'box' }, - { label: t('.feedback_label'), path: feedback_path, icon: 'megaphone' } + { label: t(".whats_new_label"), path: changelog_path, icon: "box" }, + { label: t(".feedback_label"), path: feedback_path, icon: "megaphone" } ] } ] diff --git a/app/views/shared/notifications/_loading.html.erb b/app/views/shared/notifications/_loading.html.erb deleted file mode 100644 index 18eabd89..00000000 --- a/app/views/shared/notifications/_loading.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<%# locals: (message:, id: nil) %> - -<%= tag.div id: id, class: "flex gap-3 rounded-lg bg-container p-4 group w-full md:max-w-80 shadow-border-xs" do %> -

- <%= icon "loader", class: "animate-pulse" %> -
- - <%= tag.p message, class: "text-primary text-sm font-medium" %> -<% end %> diff --git a/app/views/transactions/index.html.erb b/app/views/transactions/index.html.erb index 9781672f..52bb9c3d 100644 --- a/app/views/transactions/index.html.erb +++ b/app/views/transactions/index.html.erb @@ -4,9 +4,6 @@
<%= render MenuComponent.new do |menu| %> - <% if Rails.env.development? %> - <% menu.with_item(variant: "button", text: "Dev only: Sync all", href: sync_all_accounts_path, method: :post, icon: "refresh-cw") %> - <% end %> <% menu.with_item(variant: "link", text: "New rule", href: new_rule_path(resource_type: "transaction"), icon: "plus", data: { turbo_frame: :modal }) %> <% menu.with_item(variant: "link", text: "Edit rules", href: rules_path, icon: "git-branch", data: { turbo_frame: :_top }) %> <% menu.with_item(variant: "link", text: "Edit categories", href: categories_path, icon: "shapes", data: { turbo_frame: :_top }) %> diff --git a/config/exchanges.yml b/config/exchanges.yml deleted file mode 100644 index 9b429d14..00000000 --- a/config/exchanges.yml +++ /dev/null @@ -1,1020 +0,0 @@ -- name: NASDAQ Stock Exchange - acronym: NASDAQ - mic: XNAS - country: USA - country_code: US - city: New York - website: www.nasdaq.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: New York Stock Exchange - acronym: NYSE - mic: XNYS - country: USA - country_code: US - city: New York - website: www.nyse.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: NYSE ARCA - acronym: NYSEARCA - mic: ARCX - country: USA - country_code: US - city: New York - website: www.nyse.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: OTC Markets - acronym: - mic: OTCM - country: USA - country_code: US - city: New York - website: www.otcmarkets.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: Buenos Aires Stock Exchange - acronym: BCBA - mic: XBUE - country: Argentina - country_code: AR - city: Buenos Aires - website: www.bcba.sba.com.ar - timezone: - timezone: America/Argentina/Buenos_Aires - abbr: -03 - abbr_dst: -03 - currency: - code: ARS - symbol: AR$ - name: Argentine Peso -- name: Bahrein Bourse - acronym: BSE - mic: XBAH - country: Bahrain - country_code: BH - city: Manama - website: www.bahrainbourse.com.bh - timezone: - timezone: Asia/Bahrain - abbr: +03 - abbr_dst: +03 - currency: - code: BHD - symbol: BD - name: Bahraini Dinar -- name: Euronext Brussels - acronym: Euronext - mic: XBRU - country: Belgium - country_code: BE - city: Brussels - website: www.euronext.com - timezone: - timezone: Europe/Brussels - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: B3 - Brasil Bolsa Balcão S.A - acronym: Bovespa - mic: BVMF - country: Brazil - country_code: BR - city: Sao Paulo - website: www.bmfbovespa.com.br - timezone: - timezone: America/Sao_Paulo - abbr: -03 - abbr_dst: -03 - currency: - code: BRL - symbol: R$ - name: Brazilian Real -- name: Toronto Stock Exchange - acronym: TSX - mic: XTSE - country: Canada - country_code: CA - city: Toronto - website: www.tse.com - timezone: - timezone: America/Toronto - abbr: EST - abbr_dst: EDT - currency: - code: CAD - symbol: CA$ - name: Canadian Dollar -- name: Canadian Securities Exchange - acronym: CNSX - mic: XCNQ - country: Canada - country_code: CA - city: Toronto - website: www.cnsx.ca - timezone: - timezone: America/Toronto - abbr: EST - abbr_dst: EDT - currency: - code: CAD - symbol: CA$ - name: Canadian Dollar -- name: Santiago Stock Exchange - acronym: BVS - mic: XSGO - country: Chile - country_code: CL - city: Santiago - website: www.bolsadesantiago.com - timezone: - timezone: America/Santiago - abbr: -03 - abbr_dst: -04 - currency: - code: CLP - symbol: CL$ - name: Chilean Peso -- name: Shanghai Stock Exchange - acronym: SSE - mic: XSHG - country: China - country_code: CN - city: Shanghai - website: www.sse.com.cn - timezone: - timezone: Asia/Shanghai - abbr: CST - abbr_dst: CST - currency: - code: CNY - symbol: CN¥ - name: Chinese Yuan -- name: Shenzhen Stock Exchange - acronym: SZSE - mic: XSHE - country: China - country_code: CN - city: Shenzhen - website: www.szse.cn - timezone: - timezone: Asia/Shanghai - abbr: CST - abbr_dst: CST - currency: - code: CNY - symbol: CN¥ - name: Chinese Yuan -- name: Bolsa de Valores de Colombia - acronym: BVC - mic: XBOG - country: Colombia - country_code: CO - city: Bogota - website: www.bvc.com.co - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: COP - symbol: CO$ - name: Colombian Peso -- name: Copenhagen Stock Exchange - acronym: OMXC - mic: XCSE - country: Denmark - country_code: DK - city: Copenhagen - website: www.nasdaqomxnordic.com - timezone: - timezone: Europe/Copenhagen - abbr: CET - abbr_dst: CEST - currency: - code: DKK - symbol: Dkr - name: Danish Krone -- name: Eqyptian Exchange - acronym: EGX - mic: XCAI - country: Egypt - country_code: EG - city: Cairo - website: www.egyptse.com - timezone: - timezone: Africa/Cairo - abbr: EET - abbr_dst: EET - currency: - code: EGP - symbol: EGP - name: Egyptian Pound -- name: Tallinn Stock Exchange - acronym: OMXT - mic: XTAL - country: Estonia - country_code: EE - city: Tallinn - website: www.nasdaqbaltic.com - timezone: - timezone: Europe/Tallinn - abbr: EET - abbr_dst: EEST - currency: - code: EUR - symbol: € - name: Euro -- name: Helsinki Stock Exchange - acronym: OMXH - mic: XHEL - country: Finland - country_code: FI - city: Helsinki - website: www.nasdaqomxnordic.com - timezone: - timezone: Europe/Helsinki - abbr: EET - abbr_dst: EEST - currency: - code: EUR - symbol: € - name: Euro -- name: Euronext Paris - acronym: Euronext - mic: XPAR - country: France - country_code: FR - city: Paris - website: www.euronext.com - timezone: - timezone: Europe/Paris - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: Deutsche Börse - acronym: FSX - mic: XFRA - country: Germany - country_code: DE - city: Frankfurt - website: www.deutsche-boerse.com - timezone: - timezone: Europe/Berlin - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: Börse Stuttgart - acronym: XSTU - mic: XSTU - country: Germany - country_code: DE - city: Stuttgart - website: www.boerse-stuttgart.de - timezone: - timezone: Europe/Berlin - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: Deutsche Börse Xetra - acronym: XETR - mic: XETRA - country: Germany - country_code: DE - city: Frankfurt - website: - timezone: - timezone: Europe/Berlin - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: Hong Kong Stock Exchange - acronym: HKEX - mic: XHKG - country: Hong Kong - country_code: HK - city: Hong Kong - website: www.hkex.com.hk - timezone: - timezone: Asia/Hong_Kong - abbr: HKT - abbr_dst: HKT - currency: - code: HKD - symbol: HK$ - name: Hong Kong Dollar -- name: Nasdaq Island - acronym: XICE - mic: XICE - country: Iceland - country_code: IS - city: Reykjavík - website: www.nasdaqomxnordic.com - timezone: - timezone: Atlantic/Reykjavik - abbr: GMT - abbr_dst: GMT - currency: - code: ISK - symbol: Ikr - name: Icelandic Króna -- name: Bombay Stock Exchange - acronym: MSE - mic: XBOM - country: India - country_code: IN - city: Mumbai - website: www.bseindia.com - timezone: - timezone: Asia/Kolkata - abbr: IST - abbr_dst: IST - currency: - code: INR - symbol: Rs - name: Indian Rupee -- name: National Stock Exchange India - acronym: NSE - mic: XNSE - country: India - country_code: IN - city: Mumbai - website: www.nseindia.com - timezone: - timezone: Asia/Kolkata - abbr: IST - abbr_dst: IST - currency: - code: INR - symbol: Rs - name: Indian Rupee -- name: Jakarta Stock Exchange - acronym: IDX - mic: XIDX - country: Indonesia - country_code: ID - city: Jakarta - website: www.idx.co.id - timezone: - timezone: Asia/Jakarta - abbr: WIB - abbr_dst: WIB - currency: - code: IDR - symbol: Rp - name: Indonesian Rupiah -- name: Tel Aviv Stock Exchange - acronym: TASE - mic: XTAE - country: Israel - country_code: IL - city: Tel Aviv - website: www.tase.co.il - timezone: - timezone: Asia/Jerusalem - abbr: IST - abbr_dst: IDT - currency: - code: ILS - symbol: ₪ - name: Israeli New Sheqel -- name: Borsa Italiana - acronym: MIL - mic: XMIL - country: Italy - country_code: IT - city: Milano - website: www.borsaitaliana.it - timezone: - timezone: Europe/Rome - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: Nagoya Stock Exchange - acronym: NSE - mic: XNGO - country: Japan - country_code: JP - city: Nagoya - website: www.nse.or.jp - timezone: - timezone: Asia/Tokyo - abbr: JST - abbr_dst: JST - currency: - code: JPY - symbol: ¥ - name: Japanese Yen -- name: Fukuoka Stock Exchange - acronym: XFKA - mic: XFKA - country: Japan - country_code: JP - city: Fukuoka - website: www.fse.or.jp - timezone: - timezone: Asia/Tokyo - abbr: JST - abbr_dst: JST - currency: - code: JPY - symbol: ¥ - name: Japanese Yen -- name: Sapporo Stock Exchange - acronym: XSAP - mic: XSAP - country: Japan - country_code: JP - city: Sapporo - website: www.sse.or.jp - timezone: - timezone: Asia/Tokyo - abbr: JST - abbr_dst: JST - currency: - code: JPY - symbol: ¥ - name: Japanese Yen -- name: Nasdaq Riga - acronym: OMXR - mic: XRIS - country: Latvia - country_code: LV - city: Riga - website: www.nasdaqbaltic.com - timezone: - timezone: Europe/Riga - abbr: EET - abbr_dst: EEST - currency: - code: EUR - symbol: € - name: Euro -- name: Nasdaq Vilnius - acronym: OMXV - mic: XLIT - country: Lithuania - country_code: LT - city: Vilnius - website: www.nasdaqbaltic.com - timezone: - timezone: Europe/Vilnius - abbr: EET - abbr_dst: EEST - currency: - code: EUR - symbol: € - name: Euro -- name: Malaysia Stock Exchange - acronym: MYX - mic: XKLS - country: Malaysia - country_code: MY - city: Kuala Lumpur - website: www.bursamalaysia.com - timezone: - timezone: Asia/Kuala_Lumpur - abbr: +08 - abbr_dst: +08 - currency: - code: MYR - symbol: RM - name: Malaysian Ringgit -- name: Mexican Stock Exchange - acronym: BMV - mic: XMEX - country: Mexico - country_code: MX - city: Mexico City - website: www.bmv.com.mx - timezone: - timezone: America/Mexico_City - abbr: CST - abbr_dst: CDT - currency: - code: MXN - symbol: MX$ - name: Mexican Peso -- name: Euronext Amsterdam - acronym: Euronext - mic: XAMS - country: Netherlands - country_code: NL - city: Amsterdam - website: www.euronext.com - timezone: - timezone: Europe/Amsterdam - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: New Zealand Stock Exchange - acronym: NZX - mic: XNZE - country: New Zealand - country_code: NZ - city: Wellington - website: www.nzx.com - timezone: - timezone: Pacific/Auckland - abbr: NZDT - abbr_dst: NZST - currency: - code: NZD - symbol: NZ$ - name: New Zealand Dollar -- name: Nigerian Stock Exchange - acronym: NSE - mic: XNSA - country: Nigeria - country_code: NG - city: Lagos - website: www.nse.com.ng - timezone: - timezone: Africa/Lagos - abbr: WAT - abbr_dst: WAT - currency: - code: NGN - symbol: ₦ - name: Nigerian Naira -- name: Oslo Stock Exchange - acronym: OSE - mic: XOSL - country: Norway - country_code: NO - city: Oslo - website: www.oslobors.no - timezone: - timezone: Europe/Oslo - abbr: CET - abbr_dst: CEST - currency: - code: NOK - symbol: Nkr - name: Norwegian Krone -- name: Bolsa de Valores de Lima - acronym: BVL - mic: XLIM - country: Peru - country_code: PE - city: Lima - website: www.bvl.com.pe - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: PEN - symbol: S/. - name: Peruvian Nuevo Sol -- name: Warsaw Stock Exchange - acronym: GPW - mic: XWAR - country: Poland - country_code: PL - city: Warsaw - website: www.gpw.pl - timezone: - timezone: Europe/Warsaw - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: Euronext Lisbon - acronym: Euronext - mic: XLIS - country: Portugal - country_code: PT - city: Lisboa - website: www.euronext.com - timezone: - timezone: Europe/Lisbon - abbr: WET - abbr_dst: WEST - currency: - code: EUR - symbol: € - name: Euro -- name: Qatar Stock Exchange - acronym: QE - mic: DSMD - country: Qatar - country_code: QA - city: Doha - website: www.qatarexchange.qa - timezone: - timezone: Asia/Qatar - abbr: +03 - abbr_dst: +03 - currency: - code: QAR - symbol: QR - name: Qatari Rial -- name: Moscow Stock Exchange - acronym: MOEX - mic: MISX - country: Russia - country_code: RU - city: Moscow - website: www.moex.com - timezone: - timezone: Europe/Moscow - abbr: MSK - abbr_dst: MSK - currency: - code: RUB - symbol: RUB - name: Russian Ruble -- name: Saudi Stock Exchange - acronym: TADAWUL - mic: XSAU - country: Saudi Arabia - country_code: SA - city: Riyadh - website: www.tadawul.com.sa - timezone: - timezone: Asia/Riyadh - abbr: +03 - abbr_dst: +03 - currency: - code: SAR - symbol: SR - name: Saudi Riyal -- name: Belgrade Stock Exchange - acronym: BELEX - mic: XBEL - country: Serbia - country_code: RS - city: Belgrade - website: www.belex.rs - timezone: - timezone: Europe/Belgrade - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: Singapore Stock Exchange - acronym: SGX - mic: XSES - country: Singapore - country_code: SG - city: Singapore - website: www.sgx.com - timezone: - timezone: Asia/Singapore - abbr: +08 - abbr_dst: +08 - currency: - code: SGD - symbol: S$ - name: Singapore Dollar -- name: Johannesburg Stock Exchange - acronym: JSE - mic: XJSE - country: South Africa - country_code: ZA - city: Johannesburg - website: www.jse.co.za - timezone: - timezone: Africa/Johannesburg - abbr: SAST - abbr_dst: SAST - currency: - code: ZAR - symbol: R - name: South African Rand -- name: Korean Stock Exchange - acronym: KRX - mic: XKRX - country: South Korea - country_code: KR - city: Seoul - website: http://eng.krx.co.kr - timezone: - timezone: Asia/Seoul - abbr: KST - abbr_dst: KST - currency: - code: KRW - symbol: ₩ - name: South Korean Won -- name: Bolsas y Mercados Españoles - acronym: BME - mic: BMEX - country: Spain - country_code: ES - city: Madrid - website: www.bolsasymercados.es - timezone: - timezone: Europe/Madrid - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: Stockholm Stock Exchange - acronym: OMX - mic: XSTO - country: Sweden - country_code: SE - city: Stockholm - website: www.nasdaqomxnordic.com - timezone: - timezone: Europe/Stockholm - abbr: CET - abbr_dst: CEST - currency: - code: EUR - symbol: € - name: Euro -- name: SIX Swiss Exchange - acronym: SIX - mic: XSWX - country: Switzerland - country_code: CH - city: Zurich - website: www.six-swiss-exchange.com - timezone: - timezone: Europe/Zurich - abbr: CET - abbr_dst: CEST - currency: - code: CHF - symbol: CHF - name: Swiss Franc -- name: Taiwan Stock Exchange - acronym: TWSE - mic: XTAI - country: Taiwan - country_code: TW - city: Taipei - website: www.twse.com.tw/en/ - timezone: - timezone: Asia/Taipei - abbr: CST - abbr_dst: CST - currency: - code: TWD - symbol: NT$ - name: New Taiwan Dollar -- name: Stock Exchange of Thailand - acronym: SET - mic: XBKK - country: Thailand - country_code: TH - city: Bangkok - website: www.set.or.th - timezone: - timezone: Asia/Bangkok - abbr: +07 - abbr_dst: +07 - currency: - code: THB - symbol: ฿ - name: Thai Baht -- name: Istanbul Stock Exchange - acronym: BIST - mic: XIST - country: Turkey - country_code: TR - city: Istanbul - website: www.borsaistanbul.com - timezone: - timezone: Europe/Istanbul - abbr: +03 - abbr_dst: +03 - currency: - code: TRY - symbol: TL - name: Turkish Lira -- name: Dubai Financial Market - acronym: DFM - mic: XDFM - country: United Arab Emirates - country_code: AE - city: Dubai - website: www.dfm.co.ae - timezone: - timezone: Asia/Dubai - abbr: +04 - abbr_dst: +04 - currency: - code: AED - symbol: AED - name: United Arab Emirates Dirham -- name: London Stock Exchange - acronym: LSE - mic: XLON - country: United Kingdom - country_code: GB - city: London - website: www.londonstockexchange.com - timezone: - timezone: Europe/London - abbr: GMT - abbr_dst: BST - currency: - code: GBP - symbol: £ - name: British Pound Sterling -- name: Ho Chi Minh Stock Exchange - acronym: HOSE - mic: XSTC - country: Vietnam - country_code: VN - city: Ho Chi Minh City - website: www.hsx.vn - timezone: - timezone: Asia/Ho_Chi_Minh - abbr: +07 - abbr_dst: +07 - currency: - code: VND - symbol: ₫ - name: Vietnamese Dong -- name: American Stock Exchange - acronym: AMEX - mic: XASE - country: USA - country_code: US - city: New York - website: www.nyse.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: Cboe BZX U.S. Equities Exchang - acronym: BATS - mic: XCBO - country: USA - country_code: US - city: Chicago - website: markets.cboe.com - timezone: - timezone: America/Chicago - abbr: CDT - abbr_dst: CDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: US Mutual Funds - acronym: NMFQS - mic: NMFQS - country: USA - country_code: US - city: New York - website: - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: OTC Bulletin Board - acronym: OTCBB - mic: OOTC - country: USA - country_code: US - city: Washington - website: www.otcmarkets.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: OTC Grey Market - acronym: OTCGREY - mic: PSGM - country: USA - country_code: US - city: New York - website: www.otcmarkets.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: OTCQB Marketplace - acronym: OTCQB - mic: OTCB - country: USA - country_code: US - city: New York - website: www.otcmarkets.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: OTCQX Marketplace - acronym: OTCQX - mic: OTCQ - country: USA - country_code: US - city: New York - website: www.otcmarkets.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: OTC PINK current - acronym: PINK - mic: PINC - country: USA - country_code: US - city: New York - website: www.otcmarkets.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar -- name: Investors Exchange - acronym: IEX - mic: IEXG - country: USA - country_code: US - city: New York - website: www.iextrading.com - timezone: - timezone: America/New_York - abbr: EST - abbr_dst: EDT - currency: - code: USD - symbol: $ - name: US Dollar \ No newline at end of file diff --git a/config/importmap.rb b/config/importmap.rb index 7343cd06..5f1d8087 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -7,12 +7,12 @@ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin_all_from "app/javascript/controllers", under: "controllers" pin_all_from "app/components", under: "controllers", to: "" pin_all_from "app/javascript/services", under: "services", to: "services" -pin "@github/hotkey", to: "@github--hotkey.js" # @3.1.0 +pin "@github/hotkey", to: "@github--hotkey.js" # @3.1.1 pin "@simonwep/pickr", to: "@simonwep--pickr.js" # @1.9.1 # D3 packages -pin "d3" # @7.8.5 -pin "d3-array" # @3.2.4 +pin "d3" # @7.9.0 +pin "d3-array", to: "shims/d3-array-default.js" pin "d3-axis" # @3.0.0 pin "d3-brush" # @3.0.0 pin "d3-chord" # @3.0.1 @@ -26,7 +26,7 @@ pin "d3-ease" # @3.0.1 pin "d3-fetch" # @3.0.1 pin "d3-force" # @3.0.0 pin "d3-format" # @3.1.0 -pin "d3-geo" # @3.1.0 +pin "d3-geo" # @3.1.1 pin "d3-hierarchy" # @3.1.2 pin "d3-interpolate" # @3.0.1 pin "d3-path" # @3.1.0 @@ -34,9 +34,9 @@ pin "d3-polygon" # @3.0.1 pin "d3-quadtree" # @3.0.1 pin "d3-random" # @3.0.1 pin "d3-scale" # @4.0.2 -pin "d3-scale-chromatic" # @3.0.0 +pin "d3-scale-chromatic" # @3.1.0 pin "d3-selection" # @3.0.0 -pin "d3-shape" # @3.2.0 +pin "d3-shape", to: "shims/d3-shape-default.js" pin "d3-time" # @3.1.0 pin "d3-time-format" # @4.1.0 pin "d3-timer" # @3.0.1 @@ -45,7 +45,10 @@ pin "d3-zoom" # @3.0.0 pin "delaunator" # @5.0.1 pin "internmap" # @2.0.3 pin "robust-predicates" # @3.0.2 -pin "@floating-ui/dom", to: "@floating-ui--dom.js" # @1.6.9 -pin "@floating-ui/core", to: "@floating-ui--core.js" # @1.6.6 -pin "@floating-ui/utils", to: "@floating-ui--utils.js" # @0.2.6 -pin "@floating-ui/utils/dom", to: "@floating-ui--utils--dom.js" # @0.2.6 +pin "@floating-ui/dom", to: "@floating-ui--dom.js" # @1.7.0 +pin "@floating-ui/core", to: "@floating-ui--core.js" # @1.7.0 +pin "@floating-ui/utils", to: "@floating-ui--utils.js" # @0.2.9 +pin "@floating-ui/utils/dom", to: "@floating-ui--utils--dom.js" # @0.2.9 +pin "d3-sankey" # @0.12.3 +pin "d3-array-src", to: "d3-array.js" +pin "d3-shape-src", to: "d3-shape.js" diff --git a/config/initializers/mini_profiler.rb b/config/initializers/mini_profiler.rb index d304a50e..6a79f19e 100644 --- a/config/initializers/mini_profiler.rb +++ b/config/initializers/mini_profiler.rb @@ -1,3 +1,4 @@ Rails.application.configure do - Rack::MiniProfiler.config.skip_paths = [ "/design-system" ] + Rack::MiniProfiler.config.skip_paths = [ "/design-system", "/assets", "/cable", "/manifest", "/favicon.ico", "/hotwire-livereload", "/logo-pwa.png" ] + Rack::MiniProfiler.config.max_traces_to_show = 50 end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 1209a4fa..70a6e476 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,9 +1,16 @@ require "sidekiq/web" -Sidekiq::Web.use(Rack::Auth::Basic) do |username, password| - configured_username = ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_WEB_USERNAME", "maybe")) - configured_password = ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_WEB_PASSWORD", "maybe")) +if Rails.env.production? + Sidekiq::Web.use(Rack::Auth::Basic) do |username, password| + configured_username = ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_WEB_USERNAME", "maybe")) + configured_password = ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_WEB_PASSWORD", "maybe")) - ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), configured_username) && - ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), configured_password) + ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), configured_username) && + ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), configured_password) + end +end + +Sidekiq::Cron.configure do |config| + # 10 min "catch-up" window in case worker process is re-deploying when cron tick occurs + config.reschedule_grace_period = 600 end diff --git a/config/routes.rb b/config/routes.rb index 8384d116..ec9e2cce 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ require "sidekiq/web" +require "sidekiq/cron/web" Rails.application.routes.draw do # MFA routes @@ -25,6 +26,8 @@ Rails.application.routes.draw do get "changelog", to: "pages#changelog" get "feedback", to: "pages#feedback" + resource :current_session, only: %i[update] + resource :registration, only: %i[new create] resources :sessions, only: %i[new create destroy] resource :password_reset, only: %i[new create edit update] @@ -104,10 +107,6 @@ Rails.application.routes.draw do end resources :accounts, only: %i[index new], shallow: true do - collection do - post :sync_all - end - member do post :sync get :chart diff --git a/config/schedule.yml b/config/schedule.yml new file mode 100644 index 00000000..75b709a6 --- /dev/null +++ b/config/schedule.yml @@ -0,0 +1,14 @@ +import_market_data: + cron: "0 22 * * 1-5" # 5:00 PM EST / 6:00 PM EDT (NY time) + class: "ImportMarketDataJob" + queue: "scheduled" + description: "Imports market data daily at 5:00 PM EST (1 hour after market close)" + args: + mode: "full" + clear_cache: false + +clean_syncs: + cron: "0 * * * *" # every hour + class: "SyncCleanerJob" + queue: "scheduled" + description: "Cleans up stale syncs" diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 20343e8c..4fce6f00 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,6 +1,7 @@ concurrency: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %> queues: - - [high_priority, 6] + - [scheduled, 10] # For cron-like jobs (e.g. "daily market data sync") + - [high_priority, 4] - [medium_priority, 2] - [low_priority, 1] - [default, 1] diff --git a/db/migrate/20250512171654_update_sync_timestamps.rb b/db/migrate/20250512171654_update_sync_timestamps.rb new file mode 100644 index 00000000..ac0830b6 --- /dev/null +++ b/db/migrate/20250512171654_update_sync_timestamps.rb @@ -0,0 +1,65 @@ +class UpdateSyncTimestamps < ActiveRecord::Migration[7.2] + def change + # Timestamps, managed by aasm + add_column :syncs, :pending_at, :datetime + add_column :syncs, :syncing_at, :datetime + add_column :syncs, :completed_at, :datetime + add_column :syncs, :failed_at, :datetime + + add_column :syncs, :window_start_date, :date + add_column :syncs, :window_end_date, :date + + reversible do |dir| + dir.up do + execute <<-SQL + UPDATE syncs + SET + completed_at = CASE + WHEN status = 'completed' THEN last_ran_at + ELSE NULL + END, + failed_at = CASE + WHEN status = 'failed' THEN last_ran_at + ELSE NULL + END + SQL + + execute <<-SQL + UPDATE syncs + SET window_start_date = start_date + SQL + + # Due to some recent bugs, some self hosters have syncs that are stuck. + # This manually fails those syncs so they stop seeing syncing UI notices. + if Rails.application.config.app_mode.self_hosted? + puts "Self hosted: Fail syncs older than 2 hours" + execute <<-SQL + UPDATE syncs + SET status = 'failed' + WHERE ( + status = 'syncing' AND + created_at < NOW() - INTERVAL '2 hours' + ) + SQL + end + end + + dir.down do + execute <<-SQL + UPDATE syncs + SET + last_ran_at = COALESCE(completed_at, failed_at) + SQL + + execute <<-SQL + UPDATE syncs + SET start_date = window_start_date + SQL + end + end + + remove_column :syncs, :start_date, :date + remove_column :syncs, :last_ran_at, :datetime + remove_column :syncs, :error_backtrace, :text, array: true + end +end diff --git a/db/migrate/20250514214242_add_metadata_to_session.rb b/db/migrate/20250514214242_add_metadata_to_session.rb new file mode 100644 index 00000000..849cdccf --- /dev/null +++ b/db/migrate/20250514214242_add_metadata_to_session.rb @@ -0,0 +1,5 @@ +class AddMetadataToSession < ActiveRecord::Migration[7.2] + def change + add_column :sessions, :data, :jsonb, default: {} + end +end diff --git a/db/migrate/20250516180846_remove_stock_exchanges.rb b/db/migrate/20250516180846_remove_stock_exchanges.rb new file mode 100644 index 00000000..19c4a529 --- /dev/null +++ b/db/migrate/20250516180846_remove_stock_exchanges.rb @@ -0,0 +1,11 @@ +class RemoveStockExchanges < ActiveRecord::Migration[7.2] + def change + drop_table :stock_exchanges do |t| + t.string :name, null: false + t.string :acronym + t.string :mic, null: false + t.string :country, null: false + t.string :country_code, null: false + end + end +end diff --git a/db/migrate/20250518181619_add_auto_sync_preference_to_family.rb b/db/migrate/20250518181619_add_auto_sync_preference_to_family.rb new file mode 100644 index 00000000..80e1cd9f --- /dev/null +++ b/db/migrate/20250518181619_add_auto_sync_preference_to_family.rb @@ -0,0 +1,5 @@ +class AddAutoSyncPreferenceToFamily < ActiveRecord::Migration[7.2] + def change + add_column :families, :auto_sync_on_login, :boolean, default: true, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 9cfb546c..fe602824 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_05_13_122703) do +ActiveRecord::Schema[7.2].define(version: 2025_05_18_181619) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -30,7 +30,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_13_122703) do t.decimal "balance", precision: 19, scale: 4 t.string "currency" t.boolean "is_active", default: true, null: false - t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Loan'::character varying, 'CreditCard'::character varying, 'OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true + t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true t.uuid "import_id" t.uuid "plaid_account_id" t.boolean "scheduled_for_deletion", default: false @@ -227,6 +227,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_13_122703) do t.string "timezone" t.boolean "data_enrichment_enabled", default: false t.boolean "early_access", default: false + t.boolean "auto_sync_on_login", default: true, null: false end create_table "holdings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -537,6 +538,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_13_122703) do t.uuid "active_impersonator_session_id" t.datetime "subscribed_at" t.jsonb "prev_transaction_page_params", default: {} + t.jsonb "data", default: {} t.index ["active_impersonator_session_id"], name: "index_sessions_on_active_impersonator_session_id" t.index ["user_id"], name: "index_sessions_on_user_id" end @@ -549,27 +551,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_13_122703) do t.index ["var"], name: "index_settings_on_var", unique: true end - create_table "stock_exchanges", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.string "name", null: false - t.string "acronym" - t.string "mic", null: false - t.string "country", null: false - t.string "country_code", null: false - t.string "city" - t.string "website" - t.string "timezone_name" - t.string "timezone_abbr" - t.string "timezone_abbr_dst" - t.string "currency_code" - t.string "currency_symbol" - t.string "currency_name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["country"], name: "index_stock_exchanges_on_country" - t.index ["country_code"], name: "index_stock_exchanges_on_country_code" - t.index ["currency_code"], name: "index_stock_exchanges_on_currency_code" - end - create_table "subscriptions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "family_id", null: false t.string "status", null: false @@ -587,15 +568,18 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_13_122703) do create_table "syncs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "syncable_type", null: false t.uuid "syncable_id", null: false - t.datetime "last_ran_at" - t.date "start_date" t.string "status", default: "pending" t.string "error" t.jsonb "data" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.text "error_backtrace", array: true t.uuid "parent_id" + t.datetime "pending_at" + t.datetime "syncing_at" + t.datetime "completed_at" + t.datetime "failed_at" + t.date "window_start_date" + t.date "window_end_date" t.index ["parent_id"], name: "index_syncs_on_parent_id" t.index ["syncable_type", "syncable_id"], name: "index_syncs_on_syncable" end diff --git a/test/controllers/accounts_controller_test.rb b/test/controllers/accounts_controller_test.rb index d85a5ffa..a3d827e8 100644 --- a/test/controllers/accounts_controller_test.rb +++ b/test/controllers/accounts_controller_test.rb @@ -15,9 +15,4 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest post sync_account_path(@account) assert_redirected_to account_path(@account) end - - test "can sync all accounts" do - post sync_all_accounts_path - assert_redirected_to accounts_path - end end diff --git a/test/controllers/concerns/auto_sync_test.rb b/test/controllers/concerns/auto_sync_test.rb index a388456b..0ae19ab1 100644 --- a/test/controllers/concerns/auto_sync_test.rb +++ b/test/controllers/concerns/auto_sync_test.rb @@ -20,7 +20,7 @@ class AutoSyncTest < ActionDispatch::IntegrationTest travel_to Time.current.beginning_of_day last_sync_datetime = 1.hour.ago - Sync.create!(syncable: @family, created_at: last_sync_datetime) + Sync.create!(syncable: @family, created_at: last_sync_datetime, status: "completed") assert_difference "Sync.count", 1 do get root_path @@ -32,7 +32,15 @@ class AutoSyncTest < ActionDispatch::IntegrationTest last_created_sync_at = 23.hours.ago - Sync.create!(syncable: @family, created_at: last_created_sync_at) + Sync.create!(syncable: @family, created_at: last_created_sync_at, status: "completed") + + assert_no_difference "Sync.count" do + get root_path + end + end + + test "does not auto-sync if preference is disabled" do + @family.update!(auto_sync_on_login: false) assert_no_difference "Sync.count" do get root_path diff --git a/test/controllers/current_sessions_controller_test.rb b/test/controllers/current_sessions_controller_test.rb new file mode 100644 index 00000000..9d498fb3 --- /dev/null +++ b/test/controllers/current_sessions_controller_test.rb @@ -0,0 +1,15 @@ +require "test_helper" + +class CurrentSessionsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:family_admin) + sign_in @user + end + + test "can update the preferred tab for any namespace" do + put current_session_url, params: { current_session: { tab_key: "accounts_sidebar_tab", tab_value: "asset" } } + assert_response :success + session = Session.order(updated_at: :desc).first + assert_equal "asset", session.get_preferred_tab("accounts_sidebar_tab") + end +end diff --git a/test/controllers/plaid_items_controller_test.rb b/test/controllers/plaid_items_controller_test.rb index 7aede85c..4cf8e10a 100644 --- a/test/controllers/plaid_items_controller_test.rb +++ b/test/controllers/plaid_items_controller_test.rb @@ -8,7 +8,7 @@ class PlaidItemsControllerTest < ActionDispatch::IntegrationTest test "create" do @plaid_provider = mock - PlaidItem.expects(:plaid_provider_for_region).with("us").returns(@plaid_provider) + Provider::Registry.expects(:get_provider).with(:plaid_us).returns(@plaid_provider) public_token = "public-sandbox-1234" diff --git a/test/fixtures/stock_exchanges.yml b/test/fixtures/stock_exchanges.yml deleted file mode 100644 index 872e5b27..00000000 --- a/test/fixtures/stock_exchanges.yml +++ /dev/null @@ -1,13 +0,0 @@ -nasdaq: - name: NASDAQ - mic: XNAS - acronym: NASDAQ - country: USA - country_code: US - -nyse: - name: New York Stock Exchange - mic: XNYS - acronym: NYSE - country: USA - country_code: US diff --git a/test/fixtures/syncs.yml b/test/fixtures/syncs.yml index 1b010568..5d954280 100644 --- a/test/fixtures/syncs.yml +++ b/test/fixtures/syncs.yml @@ -1,17 +1,17 @@ account: syncable_type: Account syncable: depository - last_ran_at: <%= Time.now %> status: completed + completed_at: <%= Time.now %> plaid_item: syncable_type: PlaidItem syncable: one - last_ran_at: <%= Time.now %> status: completed + completed_at: <%= Time.now %> family: syncable_type: Family syncable: dylan_family - last_ran_at: <%= Time.now %> status: completed + completed_at: <%= Time.now %> diff --git a/test/interfaces/exchange_rate_provider_interface_test.rb b/test/interfaces/exchange_rate_provider_interface_test.rb index 9293c4d9..3716f6fc 100644 --- a/test/interfaces/exchange_rate_provider_interface_test.rb +++ b/test/interfaces/exchange_rate_provider_interface_test.rb @@ -15,6 +15,7 @@ module ExchangeRateProviderInterfaceTest assert_equal "USD", rate.from assert_equal "GBP", rate.to + assert rate.date.is_a?(Date) assert_in_delta 0.78, rate.rate, 0.01 end end @@ -26,6 +27,7 @@ module ExchangeRateProviderInterfaceTest ) assert_equal 213, response.data.count # 213 days between 01.01.2024 and 31.07.2024 + assert response.data.first.date.is_a?(Date) end end diff --git a/test/interfaces/security_provider_interface_test.rb b/test/interfaces/security_provider_interface_test.rb index 44385ede..a994bb73 100644 --- a/test/interfaces/security_provider_interface_test.rb +++ b/test/interfaces/security_provider_interface_test.rb @@ -7,7 +7,7 @@ module SecurityProviderInterfaceTest aapl = securities(:aapl) VCR.use_cassette("#{vcr_key_prefix}/security_price") do - response = @subject.fetch_security_price(aapl, date: Date.iso8601("2024-08-01")) + response = @subject.fetch_security_price(symbol: aapl.ticker, exchange_operating_mic: aapl.exchange_operating_mic, date: Date.iso8601("2024-08-01")) assert response.success? assert response.data.present? @@ -19,12 +19,14 @@ module SecurityProviderInterfaceTest VCR.use_cassette("#{vcr_key_prefix}/security_prices") do response = @subject.fetch_security_prices( - aapl, + symbol: aapl.ticker, + exchange_operating_mic: aapl.exchange_operating_mic, start_date: Date.iso8601("2024-01-01"), end_date: Date.iso8601("2024-08-01") ) assert response.success? + assert response.data.first.date.is_a?(Date) assert_equal 147, response.data.count # Synth won't return prices on weekends / holidays, so less than total day count of 213 end end @@ -44,7 +46,11 @@ module SecurityProviderInterfaceTest aapl = securities(:aapl) VCR.use_cassette("#{vcr_key_prefix}/security_info") do - response = @subject.fetch_security_info(aapl) + response = @subject.fetch_security_info( + symbol: aapl.ticker, + exchange_operating_mic: aapl.exchange_operating_mic + ) + info = response.data assert_equal "AAPL", info.symbol diff --git a/test/interfaces/syncable_interface_test.rb b/test/interfaces/syncable_interface_test.rb index 6613dbcc..df142052 100644 --- a/test/interfaces/syncable_interface_test.rb +++ b/test/interfaces/syncable_interface_test.rb @@ -7,18 +7,30 @@ module SyncableInterfaceTest test "can sync later" do assert_difference "@syncable.syncs.count", 1 do assert_enqueued_with job: SyncJob do - @syncable.sync_later + @syncable.sync_later(window_start_date: 2.days.ago.to_date) end end end - test "can sync" do - assert_difference "@syncable.syncs.count", 1 do - @syncable.sync(start_date: 2.days.ago.to_date) - end + test "can perform sync" do + mock_sync = mock + @syncable.class.any_instance.expects(:perform_sync).with(mock_sync).once + @syncable.perform_sync(mock_sync) end - test "implements sync_data" do - assert_respond_to @syncable, :sync_data + test "second sync request widens existing pending window" do + later_start = 2.days.ago.to_date + first_sync = @syncable.sync_later(window_start_date: later_start, window_end_date: later_start) + + earlier_start = 5.days.ago.to_date + wider_end = Date.current + + assert_no_difference "@syncable.syncs.count" do + @syncable.sync_later(window_start_date: earlier_start, window_end_date: wider_end) + end + + first_sync.reload + assert_equal earlier_start, first_sync.window_start_date + assert_equal wider_end, first_sync.window_end_date end end diff --git a/test/jobs/sync_job_test.rb b/test/jobs/sync_job_test.rb index b8d34400..b392b3a3 100644 --- a/test/jobs/sync_job_test.rb +++ b/test/jobs/sync_job_test.rb @@ -4,7 +4,7 @@ class SyncJobTest < ActiveJob::TestCase test "sync is performed" do syncable = accounts(:depository) - sync = syncable.syncs.create!(start_date: 2.days.ago.to_date) + sync = syncable.syncs.create!(window_start_date: 2.days.ago.to_date) sync.expects(:perform).once diff --git a/test/models/account/convertible_test.rb b/test/models/account/convertible_test.rb deleted file mode 100644 index 8d77cae5..00000000 --- a/test/models/account/convertible_test.rb +++ /dev/null @@ -1,52 +0,0 @@ -require "test_helper" -require "ostruct" - -class Account::ConvertibleTest < ActiveSupport::TestCase - include EntriesTestHelper, ProviderTestHelper - - setup do - @family = families(:empty) - @family.update!(currency: "USD") - - # Foreign account (currency is not in the family's primary currency, so it will require exchange rates for net worth rollups) - @account = @family.accounts.create!(name: "Test Account", currency: "EUR", balance: 10000, accountable: Depository.new) - - @provider = mock - ExchangeRate.stubs(:provider).returns(@provider) - end - - test "syncs required exchange rates for an account" do - create_valuation(account: @account, date: 1.day.ago.to_date, amount: 9500, currency: "EUR") - - # Since we had a valuation 1 day ago, this account starts 2 days ago and needs daily exchange rates looking forward - assert_equal 2.days.ago.to_date, @account.start_date - - ExchangeRate.delete_all - - provider_response = provider_success_response( - [ - OpenStruct.new(from: "EUR", to: "USD", date: 2.days.ago.to_date, rate: 1.1), - OpenStruct.new(from: "EUR", to: "USD", date: 1.day.ago.to_date, rate: 1.2), - OpenStruct.new(from: "EUR", to: "USD", date: Date.current, rate: 1.3) - ] - ) - - @provider.expects(:fetch_exchange_rates) - .with(from: "EUR", to: "USD", start_date: 2.days.ago.to_date, end_date: Date.current) - .returns(provider_response) - - assert_difference "ExchangeRate.count", 3 do - @account.sync_required_exchange_rates - end - end - - test "does not sync rates for a domestic account" do - @account.update!(currency: "USD") - - @provider.expects(:fetch_exchange_rates).never - - assert_no_difference "ExchangeRate.count" do - @account.sync_required_exchange_rates - end - end -end diff --git a/test/models/account/entry_test.rb b/test/models/account/entry_test.rb index 5a07ad6e..edd55e68 100644 --- a/test/models/account/entry_test.rb +++ b/test/models/account/entry_test.rb @@ -30,7 +30,7 @@ class EntryTest < ActiveSupport::TestCase prior_date = @entry.date - 1 @entry.update! date: prior_date - @entry.account.expects(:sync_later).with(start_date: prior_date) + @entry.account.expects(:sync_later).with(window_start_date: prior_date) @entry.sync_account_later end @@ -38,14 +38,14 @@ class EntryTest < ActiveSupport::TestCase prior_date = @entry.date @entry.update! date: @entry.date + 1 - @entry.account.expects(:sync_later).with(start_date: prior_date) + @entry.account.expects(:sync_later).with(window_start_date: prior_date) @entry.sync_account_later end test "triggers sync with correct start date when transaction deleted" do @entry.destroy! - @entry.account.expects(:sync_later).with(start_date: nil) + @entry.account.expects(:sync_later).with(window_start_date: nil) @entry.sync_account_later end diff --git a/test/models/account/market_data_importer_test.rb b/test/models/account/market_data_importer_test.rb new file mode 100644 index 00000000..74c42d36 --- /dev/null +++ b/test/models/account/market_data_importer_test.rb @@ -0,0 +1,107 @@ +require "test_helper" +require "ostruct" + +class Account::MarketDataImporterTest < ActiveSupport::TestCase + include ProviderTestHelper + + PROVIDER_BUFFER = 5.days + + setup do + # Ensure a clean slate for deterministic assertions + Security::Price.delete_all + ExchangeRate.delete_all + Trade.delete_all + Holding.delete_all + Security.delete_all + Entry.delete_all + + @provider = mock("provider") + Provider::Registry.any_instance + .stubs(:get_provider) + .with(:synth) + .returns(@provider) + end + + test "syncs required exchange rates for a foreign-currency account" do + family = Family.create!(name: "Smith", currency: "USD") + + account = family.accounts.create!( + name: "Chequing", + currency: "CAD", + balance: 100, + accountable: Depository.new + ) + + # Seed a rate for the first required day so that the importer only needs the next day forward + existing_date = account.start_date + ExchangeRate.create!(from_currency: "CAD", to_currency: "USD", date: existing_date, rate: 2.0) + + expected_start_date = (existing_date + 1.day) - PROVIDER_BUFFER + end_date = Date.current.in_time_zone("America/New_York").to_date + + @provider.expects(:fetch_exchange_rates) + .with(from: "CAD", + to: "USD", + start_date: expected_start_date, + end_date: end_date) + .returns(provider_success_response([ + OpenStruct.new(from: "CAD", to: "USD", date: existing_date, rate: 1.5) + ])) + + before = ExchangeRate.count + Account::MarketDataImporter.new(account).import_all + after = ExchangeRate.count + + assert_operator after, :>, before, "Should insert at least one new exchange-rate row" + end + + test "syncs security prices for securities traded by the account" do + family = Family.create!(name: "Smith", currency: "USD") + + account = family.accounts.create!( + name: "Brokerage", + currency: "USD", + balance: 0, + accountable: Investment.new + ) + + security = Security.create!(ticker: "AAPL", exchange_operating_mic: "XNAS") + + trade_date = 10.days.ago.to_date + trade = Trade.new(security: security, qty: 1, price: 100, currency: "USD") + + account.entries.create!( + name: "Buy AAPL", + date: trade_date, + amount: 100, + currency: "USD", + entryable: trade + ) + + expected_start_date = trade_date - PROVIDER_BUFFER + end_date = Date.current.in_time_zone("America/New_York").to_date + + @provider.expects(:fetch_security_prices) + .with(symbol: security.ticker, + exchange_operating_mic: security.exchange_operating_mic, + start_date: expected_start_date, + end_date: end_date) + .returns(provider_success_response([ + OpenStruct.new(security: security, + date: trade_date, + price: 100, + currency: "USD") + ])) + + @provider.stubs(:fetch_security_info) + .with(symbol: security.ticker, exchange_operating_mic: security.exchange_operating_mic) + .returns(provider_success_response(OpenStruct.new(name: "Apple", logo_url: "logo"))) + + # Ignore exchange-rate calls for this test + @provider.stubs(:fetch_exchange_rates).returns(provider_success_response([])) + + Account::MarketDataImporter.new(account).import_all + + assert_equal 1, Security::Price.where(security: security, date: trade_date).count + end +end diff --git a/test/models/balance/forward_calculator_test.rb b/test/models/balance/forward_calculator_test.rb index 24aecdb5..05215c25 100644 --- a/test/models/balance/forward_calculator_test.rb +++ b/test/models/balance/forward_calculator_test.rb @@ -15,19 +15,19 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase test "balance generation respects user timezone and last generated date is current user date" do # Simulate user in EST timezone - Time.zone = "America/New_York" + Time.use_zone("America/New_York") do + # Set current time to 1am UTC on Jan 5, 2025 + # This would be 8pm EST on Jan 4, 2025 (user's time, and the last date we should generate balances for) + travel_to Time.utc(2025, 01, 05, 1, 0, 0) - # Set current time to 1am UTC on Jan 5, 2025 - # This would be 8pm EST on Jan 4, 2025 (user's time, and the last date we should generate balances for) - travel_to Time.utc(2025, 01, 05, 1, 0, 0) + # Create a valuation for Jan 3, 2025 + create_valuation(account: @account, date: "2025-01-03", amount: 17000) - # Create a valuation for Jan 3, 2025 - create_valuation(account: @account, date: "2025-01-03", amount: 17000) + expected = [ [ "2025-01-02", 0 ], [ "2025-01-03", 17000 ], [ "2025-01-04", 17000 ] ] + calculated = Balance::ForwardCalculator.new(@account).calculate - expected = [ [ "2025-01-02", 0 ], [ "2025-01-03", 17000 ], [ "2025-01-04", 17000 ] ] - calculated = Balance::ForwardCalculator.new(@account).calculate - - assert_equal expected, calculated.map { |b| [ b.date.to_s, b.balance ] } + assert_equal expected, calculated.map { |b| [ b.date.to_s, b.balance ] } + end end # When syncing forwards, we don't care about the account balance. We generate everything based on entries, starting from 0. diff --git a/test/models/balance/syncer_test.rb b/test/models/balance/materializer_test.rb similarity index 82% rename from test/models/balance/syncer_test.rb rename to test/models/balance/materializer_test.rb index 648f6b3a..4a5ac439 100644 --- a/test/models/balance/syncer_test.rb +++ b/test/models/balance/materializer_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class Balance::SyncerTest < ActiveSupport::TestCase +class Balance::MaterializerTest < ActiveSupport::TestCase include EntriesTestHelper setup do @@ -14,7 +14,7 @@ class Balance::SyncerTest < ActiveSupport::TestCase end test "syncs balances" do - Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once + Holding::Materializer.any_instance.expects(:materialize_holdings).returns([]).once @account.expects(:start_date).returns(2.days.ago.to_date) @@ -26,7 +26,7 @@ class Balance::SyncerTest < ActiveSupport::TestCase ) assert_difference "@account.balances.count", 2 do - Balance::Syncer.new(@account, strategy: :forward).sync_balances + Balance::Materializer.new(@account, strategy: :forward).materialize_balances end end @@ -45,7 +45,7 @@ class Balance::SyncerTest < ActiveSupport::TestCase ) assert_difference "@account.balances.count", 3 do - Balance::Syncer.new(@account, strategy: :forward).sync_balances + Balance::Materializer.new(@account, strategy: :forward).materialize_balances end end end diff --git a/test/models/balance/reverse_calculator_test.rb b/test/models/balance/reverse_calculator_test.rb index 38ede057..6d73aea8 100644 --- a/test/models/balance/reverse_calculator_test.rb +++ b/test/models/balance/reverse_calculator_test.rb @@ -25,18 +25,18 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase test "balance generation respects user timezone and last generated date is current user date" do # Simulate user in EST timezone - Time.zone = "America/New_York" + Time.use_zone("America/New_York") do + # Set current time to 1am UTC on Jan 5, 2025 + # This would be 8pm EST on Jan 4, 2025 (user's time, and the last date we should generate balances for) + travel_to Time.utc(2025, 01, 05, 1, 0, 0) - # Set current time to 1am UTC on Jan 5, 2025 - # This would be 8pm EST on Jan 4, 2025 (user's time, and the last date we should generate balances for) - travel_to Time.utc(2025, 01, 05, 1, 0, 0) + create_valuation(account: @account, date: "2025-01-03", amount: 17000) - create_valuation(account: @account, date: "2025-01-03", amount: 17000) + expected = [ [ "2025-01-02", 17000 ], [ "2025-01-03", 17000 ], [ "2025-01-04", @account.balance ] ] + calculated = Balance::ReverseCalculator.new(@account).calculate - expected = [ [ "2025-01-02", 17000 ], [ "2025-01-03", 17000 ], [ "2025-01-04", @account.balance ] ] - calculated = Balance::ReverseCalculator.new(@account).calculate - - assert_equal expected, calculated.sort_by(&:date).map { |b| [ b.date.to_s, b.balance ] } + assert_equal expected, calculated.sort_by(&:date).map { |b| [ b.date.to_s, b.balance ] } + end end test "valuations sync" do diff --git a/test/models/concerns/enrichable_test.rb b/test/models/concerns/enrichable_test.rb new file mode 100644 index 00000000..890b79c1 --- /dev/null +++ b/test/models/concerns/enrichable_test.rb @@ -0,0 +1,79 @@ +require "test_helper" + +class EnrichableTest < ActiveSupport::TestCase + setup do + @enrichable = accounts(:depository) + end + + test "can enrich multiple attributes" do + assert_difference "DataEnrichment.count", 2 do + @enrichable.enrich_attributes({ name: "Updated Checking", balance: 6_000 }, source: "plaid") + end + + assert_equal "Updated Checking", @enrichable.name + assert_equal 6_000, @enrichable.balance.to_d + end + + test "can enrich a single attribute" do + assert_difference "DataEnrichment.count", 1 do + @enrichable.enrich_attribute(:name, "Single Update", source: "ai") + end + + assert_equal "Single Update", @enrichable.name + end + + test "can lock an attribute" do + refute @enrichable.locked?(:name) + + @enrichable.lock_attr!(:name) + assert @enrichable.locked?(:name) + end + + test "can unlock an attribute" do + @enrichable.lock_attr!(:name) + assert @enrichable.locked?(:name) + + @enrichable.unlock_attr!(:name) + refute @enrichable.locked?(:name) + end + + test "can lock saved attributes" do + @enrichable.name = "User Override" + @enrichable.balance = 1_234 + @enrichable.save! + + @enrichable.lock_saved_attributes! + + assert @enrichable.locked?(:name) + assert @enrichable.locked?(:balance) + end + + test "does not enrich locked attributes" do + original_name = @enrichable.name + + @enrichable.lock_attr!(:name) + + assert_no_difference "DataEnrichment.count" do + @enrichable.enrich_attribute(:name, "Should Not Change", source: "plaid") + end + + assert_equal original_name, @enrichable.reload.name + end + + test "enrichable? reflects lock state" do + assert @enrichable.enrichable?(:name) + + @enrichable.lock_attr!(:name) + + refute @enrichable.enrichable?(:name) + end + + test "enrichable scope includes and excludes records based on lock state" do + # Initially, the record should be enrichable for :name + assert_includes Account.enrichable(:name), @enrichable + + @enrichable.lock_attr!(:name) + + refute_includes Account.enrichable(:name), @enrichable + end +end diff --git a/test/models/exchange_rate/importer_test.rb b/test/models/exchange_rate/importer_test.rb new file mode 100644 index 00000000..dab40fa8 --- /dev/null +++ b/test/models/exchange_rate/importer_test.rb @@ -0,0 +1,148 @@ +require "test_helper" +require "ostruct" + +class ExchangeRate::ImporterTest < ActiveSupport::TestCase + include ProviderTestHelper + + setup do + @provider = mock + end + + test "syncs missing rates from provider" do + ExchangeRate.delete_all + + provider_response = provider_success_response([ + OpenStruct.new(from: "USD", to: "EUR", date: 2.days.ago.to_date, rate: 1.3), + OpenStruct.new(from: "USD", to: "EUR", date: 1.day.ago.to_date, rate: 1.4), + OpenStruct.new(from: "USD", to: "EUR", date: Date.current, rate: 1.5) + ]) + + @provider.expects(:fetch_exchange_rates) + .with(from: "USD", to: "EUR", start_date: get_provider_fetch_start_date(2.days.ago.to_date), end_date: Date.current) + .returns(provider_response) + + ExchangeRate::Importer.new( + exchange_rate_provider: @provider, + from: "USD", + to: "EUR", + start_date: 2.days.ago.to_date, + end_date: Date.current + ).import_provider_rates + + db_rates = ExchangeRate.where(from_currency: "USD", to_currency: "EUR", date: 2.days.ago.to_date..Date.current) + .order(:date) + + assert_equal 3, db_rates.count + assert_equal 1.3, db_rates[0].rate + assert_equal 1.4, db_rates[1].rate + assert_equal 1.5, db_rates[2].rate + end + + test "syncs diff when some rates already exist" do + ExchangeRate.delete_all + + # Pre-populate DB with the first two days + ExchangeRate.create!(from_currency: "USD", to_currency: "EUR", date: 3.days.ago.to_date, rate: 1.2) + ExchangeRate.create!(from_currency: "USD", to_currency: "EUR", date: 2.days.ago.to_date, rate: 1.25) + + provider_response = provider_success_response([ + OpenStruct.new(from: "USD", to: "EUR", date: 1.day.ago.to_date, rate: 1.3) + ]) + + @provider.expects(:fetch_exchange_rates) + .with(from: "USD", to: "EUR", start_date: get_provider_fetch_start_date(1.day.ago.to_date), end_date: Date.current) + .returns(provider_response) + + ExchangeRate::Importer.new( + exchange_rate_provider: @provider, + from: "USD", + to: "EUR", + start_date: 3.days.ago.to_date, + end_date: Date.current + ).import_provider_rates + + db_rates = ExchangeRate.order(:date) + assert_equal 4, db_rates.count + assert_equal [ 1.2, 1.25, 1.3, 1.3 ], db_rates.map(&:rate) + end + + test "no provider calls when all rates exist" do + ExchangeRate.delete_all + + (3.days.ago.to_date..Date.current).each_with_index do |date, idx| + ExchangeRate.create!(from_currency: "USD", to_currency: "EUR", date:, rate: 1.2 + idx * 0.01) + end + + @provider.expects(:fetch_exchange_rates).never + + ExchangeRate::Importer.new( + exchange_rate_provider: @provider, + from: "USD", + to: "EUR", + start_date: 3.days.ago.to_date, + end_date: Date.current + ).import_provider_rates + end + + # A helpful "reset" option for when we need to refresh provider data + test "full upsert if clear_cache is true" do + ExchangeRate.delete_all + + # Seed DB with stale data + (2.days.ago.to_date..Date.current).each do |date| + ExchangeRate.create!(from_currency: "USD", to_currency: "EUR", date:, rate: 1.0) + end + + provider_response = provider_success_response([ + OpenStruct.new(from: "USD", to: "EUR", date: 2.days.ago.to_date, rate: 1.3), + OpenStruct.new(from: "USD", to: "EUR", date: 1.day.ago.to_date, rate: 1.4), + OpenStruct.new(from: "USD", to: "EUR", date: Date.current, rate: 1.5) + ]) + + @provider.expects(:fetch_exchange_rates) + .with(from: "USD", to: "EUR", start_date: get_provider_fetch_start_date(2.days.ago.to_date), end_date: Date.current) + .returns(provider_response) + + ExchangeRate::Importer.new( + exchange_rate_provider: @provider, + from: "USD", + to: "EUR", + start_date: 2.days.ago.to_date, + end_date: Date.current, + clear_cache: true + ).import_provider_rates + + db_rates = ExchangeRate.where(from_currency: "USD", to_currency: "EUR").order(:date) + assert_equal [ 1.3, 1.4, 1.5 ], db_rates.map(&:rate) + end + + test "clamps end_date to today when future date is provided" do + ExchangeRate.delete_all + + future_date = Date.current + 3.days + + provider_response = provider_success_response([ + OpenStruct.new(from: "USD", to: "EUR", date: Date.current, rate: 1.6) + ]) + + @provider.expects(:fetch_exchange_rates) + .with(from: "USD", to: "EUR", start_date: get_provider_fetch_start_date(Date.current), end_date: Date.current) + .returns(provider_response) + + ExchangeRate::Importer.new( + exchange_rate_provider: @provider, + from: "USD", + to: "EUR", + start_date: Date.current, + end_date: future_date + ).import_provider_rates + + assert_equal 1, ExchangeRate.count + end + + private + def get_provider_fetch_start_date(start_date) + # We fetch with a 5 day buffer to account for weekends and holidays + start_date - 5.days + end +end diff --git a/test/models/exchange_rate_test.rb b/test/models/exchange_rate_test.rb index 64fc328b..021b4edf 100644 --- a/test/models/exchange_rate_test.rb +++ b/test/models/exchange_rate_test.rb @@ -67,26 +67,4 @@ class ExchangeRateTest < ActiveSupport::TestCase assert_nil ExchangeRate.find_or_fetch_rate(from: "USD", to: "EUR", date: Date.current, cache: true) end - - test "upserts rates for currency pair and date range" do - ExchangeRate.delete_all - - ExchangeRate.create!(date: 1.day.ago.to_date, from_currency: "USD", to_currency: "EUR", rate: 0.9) - - provider_response = provider_success_response([ - OpenStruct.new(from: "USD", to: "EUR", date: Date.current, rate: 1.3), - OpenStruct.new(from: "USD", to: "EUR", date: 1.day.ago.to_date, rate: 1.4), - OpenStruct.new(from: "USD", to: "EUR", date: 2.days.ago.to_date, rate: 1.5) - ]) - - @provider.expects(:fetch_exchange_rates) - .with(from: "USD", to: "EUR", start_date: 2.days.ago.to_date, end_date: Date.current) - .returns(provider_response) - - ExchangeRate.sync_provider_rates(from: "USD", to: "EUR", start_date: 2.days.ago.to_date) - - assert_equal 1.3, ExchangeRate.find_by(from_currency: "USD", to_currency: "EUR", date: Date.current).rate - assert_equal 1.4, ExchangeRate.find_by(from_currency: "USD", to_currency: "EUR", date: 1.day.ago.to_date).rate - assert_equal 1.5, ExchangeRate.find_by(from_currency: "USD", to_currency: "EUR", date: 2.days.ago.to_date).rate - end end diff --git a/test/models/family/syncer_test.rb b/test/models/family/syncer_test.rb new file mode 100644 index 00000000..7fe01a7e --- /dev/null +++ b/test/models/family/syncer_test.rb @@ -0,0 +1,30 @@ +require "test_helper" + +class Family::SyncerTest < ActiveSupport::TestCase + setup do + @family = families(:dylan_family) + end + + test "syncs plaid items and manual accounts" do + family_sync = syncs(:family) + + manual_accounts_count = @family.accounts.manual.count + items_count = @family.plaid_items.count + + syncer = Family::Syncer.new(@family) + + Account.any_instance + .expects(:sync_later) + .with(parent_sync: family_sync, window_start_date: nil, window_end_date: nil) + .times(manual_accounts_count) + + PlaidItem.any_instance + .expects(:sync_later) + .with(parent_sync: family_sync, window_start_date: nil, window_end_date: nil) + .times(items_count) + + syncer.perform_sync(family_sync) + + assert_equal "completed", family_sync.reload.status + end +end diff --git a/test/models/family_test.rb b/test/models/family_test.rb index 24876a77..0229aa6e 100644 --- a/test/models/family_test.rb +++ b/test/models/family_test.rb @@ -1,25 +1,9 @@ require "test_helper" -require "csv" class FamilyTest < ActiveSupport::TestCase - include EntriesTestHelper include SyncableInterfaceTest def setup - @family = families(:empty) @syncable = families(:dylan_family) end - - test "syncs plaid items and manual accounts" do - family_sync = syncs(:family) - - manual_accounts_count = @syncable.accounts.manual.count - items_count = @syncable.plaid_items.count - - Account.any_instance.expects(:sync_later) - .with(start_date: nil, parent_sync: family_sync) - .times(manual_accounts_count) - - @syncable.sync_data(family_sync, start_date: family_sync.start_date) - end end diff --git a/test/models/holding/forward_calculator_test.rb b/test/models/holding/forward_calculator_test.rb index 7a8aaa53..89e9b28c 100644 --- a/test/models/holding/forward_calculator_test.rb +++ b/test/models/holding/forward_calculator_test.rb @@ -20,22 +20,22 @@ class Holding::ForwardCalculatorTest < ActiveSupport::TestCase test "holding generation respects user timezone and last generated date is current user date" do # Simulate user in EST timezone - Time.zone = "America/New_York" + Time.use_zone("America/New_York") do + # Set current time to 1am UTC on Jan 5, 2025 + # This would be 8pm EST on Jan 4, 2025 (user's time, and the last date we should generate holdings for) + travel_to Time.utc(2025, 01, 05, 1, 0, 0) - # Set current time to 1am UTC on Jan 5, 2025 - # This would be 8pm EST on Jan 4, 2025 (user's time, and the last date we should generate holdings for) - travel_to Time.utc(2025, 01, 05, 1, 0, 0) + voo = Security.create!(ticker: "VOO", name: "Vanguard S&P 500 ETF") + Security::Price.create!(security: voo, date: "2025-01-02", price: 500) + Security::Price.create!(security: voo, date: "2025-01-03", price: 500) + Security::Price.create!(security: voo, date: "2025-01-04", price: 500) + create_trade(voo, qty: 10, date: "2025-01-03", price: 500, account: @account) - voo = Security.create!(ticker: "VOO", name: "Vanguard S&P 500 ETF") - Security::Price.create!(security: voo, date: "2025-01-02", price: 500) - Security::Price.create!(security: voo, date: "2025-01-03", price: 500) - Security::Price.create!(security: voo, date: "2025-01-04", price: 500) - create_trade(voo, qty: 10, date: "2025-01-03", price: 500, account: @account) + expected = [ [ "2025-01-02", 0 ], [ "2025-01-03", 5000 ], [ "2025-01-04", 5000 ] ] + calculated = Holding::ForwardCalculator.new(@account).calculate - expected = [ [ "2025-01-02", 0 ], [ "2025-01-03", 5000 ], [ "2025-01-04", 5000 ] ] - calculated = Holding::ForwardCalculator.new(@account).calculate - - assert_equal expected, calculated.map { |b| [ b.date.to_s, b.amount ] } + assert_equal expected, calculated.map { |b| [ b.date.to_s, b.amount ] } + end end test "forward portfolio calculation" do diff --git a/test/models/holding/syncer_test.rb b/test/models/holding/materializer_test.rb similarity index 78% rename from test/models/holding/syncer_test.rb rename to test/models/holding/materializer_test.rb index 19e191c8..41ae8ff6 100644 --- a/test/models/holding/syncer_test.rb +++ b/test/models/holding/materializer_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class Holding::SyncerTest < ActiveSupport::TestCase +class Holding::MaterializerTest < ActiveSupport::TestCase include EntriesTestHelper setup do @@ -14,7 +14,7 @@ class Holding::SyncerTest < ActiveSupport::TestCase # Should have yesterday's and today's holdings assert_difference "@account.holdings.count", 2 do - Holding::Syncer.new(@account, strategy: :forward).sync_holdings + Holding::Materializer.new(@account, strategy: :forward).materialize_holdings end end @@ -23,7 +23,7 @@ class Holding::SyncerTest < ActiveSupport::TestCase Holding.create!(account: @account, security: @aapl, qty: 1, price: 100, amount: 100, currency: "USD", date: Date.current) assert_difference "Holding.count", -1 do - Holding::Syncer.new(@account, strategy: :forward).sync_holdings + Holding::Materializer.new(@account, strategy: :forward).materialize_holdings end end end diff --git a/test/models/holding/portfolio_cache_test.rb b/test/models/holding/portfolio_cache_test.rb index 2518e2b7..4677fc41 100644 --- a/test/models/holding/portfolio_cache_test.rb +++ b/test/models/holding/portfolio_cache_test.rb @@ -28,37 +28,18 @@ class Holding::PortfolioCacheTest < ActiveSupport::TestCase price: db_price ) - expect_provider_prices([], start_date: @account.start_date) - cache = Holding::PortfolioCache.new(@account) assert_equal db_price, cache.get_price(@security.id, Date.current).price end - test "if no price in DB, try fetching from provider" do - Security::Price.delete_all - - provider_price = Security::Price.new( - security: @security, - date: Date.current, - price: 220, - currency: "USD" - ) - - expect_provider_prices([ provider_price ], start_date: @account.start_date) - - cache = Holding::PortfolioCache.new(@account) - assert_equal provider_price.price, cache.get_price(@security.id, Date.current).price - end - - test "if no price from db or provider, try getting the price from trades" do + test "if no price from db, try getting the price from trades" do Security::Price.destroy_all - expect_provider_prices([], start_date: @account.start_date) cache = Holding::PortfolioCache.new(@account) assert_equal @trade.price, cache.get_price(@security.id, @trade.entry.date).price end - test "if no price from db, provider, or trades, search holdings" do + test "if no price from db or trades, search holdings" do Security::Price.delete_all Entry.delete_all @@ -72,16 +53,7 @@ class Holding::PortfolioCacheTest < ActiveSupport::TestCase currency: "USD" ) - expect_provider_prices([], start_date: @account.start_date) - cache = Holding::PortfolioCache.new(@account, use_holdings: true) assert_equal holding.price, cache.get_price(@security.id, holding.date).price end - - private - def expect_provider_prices(prices, start_date:, end_date: Date.current) - @provider.expects(:fetch_security_prices) - .with(@security, start_date: start_date, end_date: end_date) - .returns(provider_success_response(prices)) - end end diff --git a/test/models/holding/reverse_calculator_test.rb b/test/models/holding/reverse_calculator_test.rb index ec9dc204..785d7b94 100644 --- a/test/models/holding/reverse_calculator_test.rb +++ b/test/models/holding/reverse_calculator_test.rb @@ -20,26 +20,26 @@ class Holding::ReverseCalculatorTest < ActiveSupport::TestCase test "holding generation respects user timezone and last generated date is current user date" do # Simulate user in EST timezone - Time.zone = "America/New_York" + Time.use_zone("America/New_York") do + # Set current time to 1am UTC on Jan 5, 2025 + # This would be 8pm EST on Jan 4, 2025 (user's time, and the last date we should generate holdings for) + travel_to Time.utc(2025, 01, 05, 1, 0, 0) - # Set current time to 1am UTC on Jan 5, 2025 - # This would be 8pm EST on Jan 4, 2025 (user's time, and the last date we should generate holdings for) - travel_to Time.utc(2025, 01, 05, 1, 0, 0) + voo = Security.create!(ticker: "VOO", name: "Vanguard S&P 500 ETF") + Security::Price.create!(security: voo, date: "2025-01-02", price: 500) + Security::Price.create!(security: voo, date: "2025-01-03", price: 500) + Security::Price.create!(security: voo, date: "2025-01-04", price: 500) - voo = Security.create!(ticker: "VOO", name: "Vanguard S&P 500 ETF") - Security::Price.create!(security: voo, date: "2025-01-02", price: 500) - Security::Price.create!(security: voo, date: "2025-01-03", price: 500) - Security::Price.create!(security: voo, date: "2025-01-04", price: 500) + # Today's holdings (provided) + @account.holdings.create!(security: voo, date: "2025-01-04", qty: 10, price: 500, amount: 5000, currency: "USD") - # Today's holdings (provided) - @account.holdings.create!(security: voo, date: "2025-01-04", qty: 10, price: 500, amount: 5000, currency: "USD") + create_trade(voo, qty: 10, date: "2025-01-03", price: 500, account: @account) - create_trade(voo, qty: 10, date: "2025-01-03", price: 500, account: @account) + expected = [ [ "2025-01-02", 0 ], [ "2025-01-03", 5000 ], [ "2025-01-04", 5000 ] ] + calculated = Holding::ReverseCalculator.new(@account).calculate - expected = [ [ "2025-01-02", 0 ], [ "2025-01-03", 5000 ], [ "2025-01-04", 5000 ] ] - calculated = Holding::ReverseCalculator.new(@account).calculate - - assert_equal expected, calculated.sort_by(&:date).map { |b| [ b.date.to_s, b.amount ] } + assert_equal expected, calculated.sort_by(&:date).map { |b| [ b.date.to_s, b.amount ] } + end end # Should be able to handle this case, although we should not be reverse-syncing an account without provided current day holdings diff --git a/test/models/market_data_importer_test.rb b/test/models/market_data_importer_test.rb new file mode 100644 index 00000000..b39bf0ad --- /dev/null +++ b/test/models/market_data_importer_test.rb @@ -0,0 +1,85 @@ +require "test_helper" +require "ostruct" + +class MarketDataImporterTest < ActiveSupport::TestCase + include ProviderTestHelper + + SNAPSHOT_START_DATE = MarketDataImporter::SNAPSHOT_DAYS.days.ago.to_date + PROVIDER_BUFFER = 5.days + + setup do + Security::Price.delete_all + ExchangeRate.delete_all + Trade.delete_all + Holding.delete_all + Security.delete_all + + @provider = mock("provider") + Provider::Registry.any_instance + .stubs(:get_provider) + .with(:synth) + .returns(@provider) + end + + test "syncs required exchange rates" do + family = Family.create!(name: "Smith", currency: "USD") + family.accounts.create!(name: "Chequing", + currency: "CAD", + balance: 100, + accountable: Depository.new) + + # Seed stale rate so only the next missing day is fetched + ExchangeRate.create!(from_currency: "CAD", + to_currency: "USD", + date: SNAPSHOT_START_DATE, + rate: 2.0) + + expected_start_date = (SNAPSHOT_START_DATE + 1.day) - PROVIDER_BUFFER + end_date = Date.current.in_time_zone("America/New_York").to_date + + @provider.expects(:fetch_exchange_rates) + .with(from: "CAD", + to: "USD", + start_date: expected_start_date, + end_date: end_date) + .returns(provider_success_response([ + OpenStruct.new(from: "CAD", to: "USD", date: SNAPSHOT_START_DATE, rate: 1.5) + ])) + + before = ExchangeRate.count + MarketDataImporter.new(mode: :snapshot).import_exchange_rates + after = ExchangeRate.count + + assert_operator after, :>, before, "Should insert at least one new exchange-rate row" + end + + test "syncs security prices" do + security = Security.create!(ticker: "AAPL", exchange_operating_mic: "XNAS") + + expected_start_date = SNAPSHOT_START_DATE - PROVIDER_BUFFER + end_date = Date.current.in_time_zone("America/New_York").to_date + + @provider.expects(:fetch_security_prices) + .with(symbol: security.ticker, + exchange_operating_mic: security.exchange_operating_mic, + start_date: expected_start_date, + end_date: end_date) + .returns(provider_success_response([ + OpenStruct.new(security: security, + date: SNAPSHOT_START_DATE, + price: 100, + currency: "USD") + ])) + + @provider.stubs(:fetch_security_info) + .with(symbol: "AAPL", exchange_operating_mic: "XNAS") + .returns(provider_success_response(OpenStruct.new(name: "Apple", logo_url: "logo"))) + + # Ignore exchange rate calls for this test + @provider.stubs(:fetch_exchange_rates).returns(provider_success_response([])) + + MarketDataImporter.new(mode: :snapshot).import_security_prices + + assert_equal 1, Security::Price.where(security: security, date: SNAPSHOT_START_DATE).count + end +end diff --git a/test/models/rule/action_test.rb b/test/models/rule/action_test.rb index 624849fc..f71bb2cb 100644 --- a/test/models/rule/action_test.rb +++ b/test/models/rule/action_test.rb @@ -21,7 +21,7 @@ class Rule::ActionTest < ActiveSupport::TestCase test "set_transaction_category" do # Does not modify transactions that are locked (user edited them) - @txn1.lock!(:category_id) + @txn1.lock_attr!(:category_id) action = Rule::Action.new( rule: @transaction_rule, @@ -42,7 +42,7 @@ class Rule::ActionTest < ActiveSupport::TestCase tag = @family.tags.create!(name: "Rule test tag") # Does not modify transactions that are locked (user edited them) - @txn1.lock!(:tag_ids) + @txn1.lock_attr!(:tag_ids) action = Rule::Action.new( rule: @transaction_rule, @@ -63,7 +63,7 @@ class Rule::ActionTest < ActiveSupport::TestCase merchant = @family.merchants.create!(name: "Rule test merchant") # Does not modify transactions that are locked (user edited them) - @txn1.lock!(:merchant_id) + @txn1.lock_attr!(:merchant_id) action = Rule::Action.new( rule: @transaction_rule, @@ -84,7 +84,7 @@ class Rule::ActionTest < ActiveSupport::TestCase new_name = "Renamed Transaction" # Does not modify transactions that are locked (user edited them) - @txn1.lock!(:name) + @txn1.lock_attr!(:name) action = Rule::Action.new( rule: @transaction_rule, diff --git a/test/models/security/price/importer_test.rb b/test/models/security/price/importer_test.rb new file mode 100644 index 00000000..665a91f6 --- /dev/null +++ b/test/models/security/price/importer_test.rb @@ -0,0 +1,143 @@ +require "test_helper" +require "ostruct" + +class Security::Price::ImporterTest < ActiveSupport::TestCase + include ProviderTestHelper + + setup do + @provider = mock + @security = Security.create!(ticker: "AAPL") + end + + test "syncs missing prices from provider" do + Security::Price.delete_all + + provider_response = provider_success_response([ + OpenStruct.new(security: @security, date: 2.days.ago.to_date, price: 150, currency: "USD"), + OpenStruct.new(security: @security, date: 1.day.ago.to_date, price: 155, currency: "USD"), + OpenStruct.new(security: @security, date: Date.current, price: 160, currency: "USD") + ]) + + @provider.expects(:fetch_security_prices) + .with(symbol: @security.ticker, exchange_operating_mic: @security.exchange_operating_mic, + start_date: get_provider_fetch_start_date(2.days.ago.to_date), end_date: Date.current) + .returns(provider_response) + + Security::Price::Importer.new( + security: @security, + security_provider: @provider, + start_date: 2.days.ago.to_date, + end_date: Date.current + ).import_provider_prices + + db_prices = Security::Price.where(security: @security, date: 2.days.ago.to_date..Date.current).order(:date) + + assert_equal 3, db_prices.count + assert_equal [ 150, 155, 160 ], db_prices.map(&:price) + end + + test "syncs diff when some prices already exist" do + Security::Price.delete_all + + # Pre-populate DB with first two days + Security::Price.create!(security: @security, date: 3.days.ago.to_date, price: 140, currency: "USD") + Security::Price.create!(security: @security, date: 2.days.ago.to_date, price: 145, currency: "USD") + + provider_response = provider_success_response([ + OpenStruct.new(security: @security, date: 1.day.ago.to_date, price: 150, currency: "USD") + ]) + + @provider.expects(:fetch_security_prices) + .with(symbol: @security.ticker, exchange_operating_mic: @security.exchange_operating_mic, + start_date: get_provider_fetch_start_date(1.day.ago.to_date), end_date: Date.current) + .returns(provider_response) + + Security::Price::Importer.new( + security: @security, + security_provider: @provider, + start_date: 3.days.ago.to_date, + end_date: Date.current + ).import_provider_prices + + db_prices = Security::Price.where(security: @security).order(:date) + assert_equal 4, db_prices.count + assert_equal [ 140, 145, 150, 150 ], db_prices.map(&:price) + end + + test "no provider calls when all prices exist" do + Security::Price.delete_all + + (3.days.ago.to_date..Date.current).each_with_index do |date, idx| + Security::Price.create!(security: @security, date:, price: 100 + idx, currency: "USD") + end + + @provider.expects(:fetch_security_prices).never + + Security::Price::Importer.new( + security: @security, + security_provider: @provider, + start_date: 3.days.ago.to_date, + end_date: Date.current + ).import_provider_prices + end + + test "full upsert if clear_cache is true" do + Security::Price.delete_all + + # Seed DB with stale prices + (2.days.ago.to_date..Date.current).each do |date| + Security::Price.create!(security: @security, date:, price: 100, currency: "USD") + end + + provider_response = provider_success_response([ + OpenStruct.new(security: @security, date: 2.days.ago.to_date, price: 150, currency: "USD"), + OpenStruct.new(security: @security, date: 1.day.ago.to_date, price: 155, currency: "USD"), + OpenStruct.new(security: @security, date: Date.current, price: 160, currency: "USD") + ]) + + @provider.expects(:fetch_security_prices) + .with(symbol: @security.ticker, exchange_operating_mic: @security.exchange_operating_mic, + start_date: get_provider_fetch_start_date(2.days.ago.to_date), end_date: Date.current) + .returns(provider_response) + + Security::Price::Importer.new( + security: @security, + security_provider: @provider, + start_date: 2.days.ago.to_date, + end_date: Date.current, + clear_cache: true + ).import_provider_prices + + db_prices = Security::Price.where(security: @security).order(:date) + assert_equal [ 150, 155, 160 ], db_prices.map(&:price) + end + + test "clamps end_date to today when future date is provided" do + Security::Price.delete_all + + future_date = Date.current + 3.days + + provider_response = provider_success_response([ + OpenStruct.new(security: @security, date: Date.current, price: 165, currency: "USD") + ]) + + @provider.expects(:fetch_security_prices) + .with(symbol: @security.ticker, exchange_operating_mic: @security.exchange_operating_mic, + start_date: get_provider_fetch_start_date(Date.current), end_date: Date.current) + .returns(provider_response) + + Security::Price::Importer.new( + security: @security, + security_provider: @provider, + start_date: Date.current, + end_date: future_date + ).import_provider_prices + + assert_equal 1, Security::Price.count + end + + private + def get_provider_fetch_start_date(start_date) + start_date - 5.days + end +end diff --git a/test/models/security/price_test.rb b/test/models/security/price_test.rb index bd150359..14c022fe 100644 --- a/test/models/security/price_test.rb +++ b/test/models/security/price_test.rb @@ -49,25 +49,6 @@ class Security::PriceTest < ActiveSupport::TestCase assert_not @security.find_or_fetch_price(date: Date.current) end - test "upserts historical prices from provider" do - Security::Price.delete_all - - # Will be overwritten by upsert - Security::Price.create!(security: @security, date: 1.day.ago.to_date, price: 190, currency: "USD") - - expect_provider_prices(security: @security, start_date: 2.days.ago.to_date, end_date: Date.current, prices: [ - Security::Price.new(security: @security, date: Date.current, price: 215, currency: "USD"), - Security::Price.new(security: @security, date: 1.day.ago.to_date, price: 214, currency: "USD"), - Security::Price.new(security: @security, date: 2.days.ago.to_date, price: 213, currency: "USD") - ]) - - @security.sync_provider_prices(start_date: 2.days.ago.to_date) - - assert_equal 215, @security.prices.find_by(date: Date.current).price - assert_equal 214, @security.prices.find_by(date: 1.day.ago.to_date).price - assert_equal 213, @security.prices.find_by(date: 2.days.ago.to_date).price - end - private def expect_provider_price(security:, price:, date:) @provider.expects(:fetch_security_price) diff --git a/test/models/sync_test.rb b/test/models/sync_test.rb index 99019146..05765ea0 100644 --- a/test/models/sync_test.rb +++ b/test/models/sync_test.rb @@ -1,34 +1,211 @@ require "test_helper" class SyncTest < ActiveSupport::TestCase - setup do - @sync = syncs(:account) - @sync.update(status: "pending") - end + include ActiveJob::TestHelper test "runs successful sync" do - @sync.syncable.expects(:sync_data).with(@sync, start_date: @sync.start_date).once + syncable = accounts(:depository) + sync = Sync.create!(syncable: syncable) - assert_equal "pending", @sync.status + syncable.expects(:perform_sync).with(sync).once - previously_ran_at = @sync.last_ran_at + assert_equal "pending", sync.status - @sync.perform + sync.perform - assert @sync.last_ran_at > previously_ran_at - assert_equal "completed", @sync.status + assert sync.completed_at < Time.now + assert_equal "completed", sync.status end test "handles sync errors" do - @sync.syncable.expects(:sync_data).with(@sync, start_date: @sync.start_date).raises(StandardError.new("test sync error")) + syncable = accounts(:depository) + sync = Sync.create!(syncable: syncable) - assert_equal "pending", @sync.status - previously_ran_at = @sync.last_ran_at + syncable.expects(:perform_sync).with(sync).raises(StandardError.new("test sync error")) - @sync.perform + assert_equal "pending", sync.status - assert @sync.last_ran_at > previously_ran_at - assert_equal "failed", @sync.status - assert_equal "test sync error", @sync.error + sync.perform + + assert sync.failed_at < Time.now + assert_equal "failed", sync.status + assert_equal "test sync error", sync.error + end + + test "can run nested syncs that alert the parent when complete" do + family = families(:dylan_family) + plaid_item = plaid_items(:one) + account = accounts(:connected) + + family_sync = Sync.create!(syncable: family) + plaid_item_sync = Sync.create!(syncable: plaid_item, parent: family_sync) + account_sync = Sync.create!(syncable: account, parent: plaid_item_sync) + + assert_equal "pending", family_sync.status + assert_equal "pending", plaid_item_sync.status + assert_equal "pending", account_sync.status + + family.expects(:perform_sync).with(family_sync).once + + family_sync.perform + + assert_equal "syncing", family_sync.reload.status + + plaid_item.expects(:perform_sync).with(plaid_item_sync).once + + plaid_item_sync.perform + + assert_equal "syncing", family_sync.reload.status + assert_equal "syncing", plaid_item_sync.reload.status + + account.expects(:perform_sync).with(account_sync).once + + # Since these are accessed through `parent`, they won't necessarily be the same + # instance we configured above + Account.any_instance.expects(:perform_post_sync).once + Account.any_instance.expects(:broadcast_sync_complete).once + PlaidItem.any_instance.expects(:perform_post_sync).once + PlaidItem.any_instance.expects(:broadcast_sync_complete).once + Family.any_instance.expects(:perform_post_sync).once + Family.any_instance.expects(:broadcast_sync_complete).once + + account_sync.perform + + assert_equal "completed", plaid_item_sync.reload.status + assert_equal "completed", account_sync.reload.status + assert_equal "completed", family_sync.reload.status + end + + test "failures propagate up the chain" do + family = families(:dylan_family) + plaid_item = plaid_items(:one) + account = accounts(:connected) + + family_sync = Sync.create!(syncable: family) + plaid_item_sync = Sync.create!(syncable: plaid_item, parent: family_sync) + account_sync = Sync.create!(syncable: account, parent: plaid_item_sync) + + assert_equal "pending", family_sync.status + assert_equal "pending", plaid_item_sync.status + assert_equal "pending", account_sync.status + + family.expects(:perform_sync).with(family_sync).once + + family_sync.perform + + assert_equal "syncing", family_sync.reload.status + + plaid_item.expects(:perform_sync).with(plaid_item_sync).once + + plaid_item_sync.perform + + assert_equal "syncing", family_sync.reload.status + assert_equal "syncing", plaid_item_sync.reload.status + + # This error should "bubble up" to the PlaidItem and Family sync results + account.expects(:perform_sync).with(account_sync).raises(StandardError.new("test account sync error")) + + # Since these are accessed through `parent`, they won't necessarily be the same + # instance we configured above + Account.any_instance.expects(:perform_post_sync).once + PlaidItem.any_instance.expects(:perform_post_sync).once + Family.any_instance.expects(:perform_post_sync).once + + Account.any_instance.expects(:broadcast_sync_complete).once + PlaidItem.any_instance.expects(:broadcast_sync_complete).once + Family.any_instance.expects(:broadcast_sync_complete).once + + account_sync.perform + + assert_equal "failed", plaid_item_sync.reload.status + assert_equal "failed", account_sync.reload.status + assert_equal "failed", family_sync.reload.status + end + + test "parent failure should not change status if child succeeds" do + family = families(:dylan_family) + plaid_item = plaid_items(:one) + account = accounts(:connected) + + family_sync = Sync.create!(syncable: family) + plaid_item_sync = Sync.create!(syncable: plaid_item, parent: family_sync) + account_sync = Sync.create!(syncable: account, parent: plaid_item_sync) + + assert_equal "pending", family_sync.status + assert_equal "pending", plaid_item_sync.status + assert_equal "pending", account_sync.status + + family.expects(:perform_sync).with(family_sync).raises(StandardError.new("test family sync error")) + + family_sync.perform + + assert_equal "failed", family_sync.reload.status + + plaid_item.expects(:perform_sync).with(plaid_item_sync).raises(StandardError.new("test plaid item sync error")) + + plaid_item_sync.perform + + assert_equal "failed", family_sync.reload.status + assert_equal "failed", plaid_item_sync.reload.status + + # Leaf level sync succeeds, but shouldn't change the status of the already-failed parent syncs + account.expects(:perform_sync).with(account_sync).once + + # Since these are accessed through `parent`, they won't necessarily be the same + # instance we configured above + Account.any_instance.expects(:perform_post_sync).once + PlaidItem.any_instance.expects(:perform_post_sync).once + Family.any_instance.expects(:perform_post_sync).once + + Account.any_instance.expects(:broadcast_sync_complete).once + PlaidItem.any_instance.expects(:broadcast_sync_complete).once + Family.any_instance.expects(:broadcast_sync_complete).once + + account_sync.perform + + assert_equal "failed", plaid_item_sync.reload.status + assert_equal "failed", family_sync.reload.status + assert_equal "completed", account_sync.reload.status + end + + test "clean marks stale incomplete rows" do + stale_pending = Sync.create!( + syncable: accounts(:depository), + status: :pending, + created_at: 25.hours.ago + ) + + stale_syncing = Sync.create!( + syncable: accounts(:depository), + status: :syncing, + created_at: 25.hours.ago, + pending_at: 24.hours.ago, + syncing_at: 23.hours.ago + ) + + Sync.clean + + assert_equal "stale", stale_pending.reload.status + assert_equal "stale", stale_syncing.reload.status + end + + test "expand_window_if_needed widens start and end dates on a pending sync" do + initial_start = 1.day.ago.to_date + initial_end = 1.day.ago.to_date + + sync = Sync.create!( + syncable: accounts(:depository), + window_start_date: initial_start, + window_end_date: initial_end + ) + + new_start = 5.days.ago.to_date + new_end = Date.current + + sync.expand_window_if_needed(new_start, new_end) + sync.reload + + assert_equal new_start, sync.window_start_date + assert_equal new_end, sync.window_end_date end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 23f98faa..7eac9dde 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -17,6 +17,7 @@ require "rails/test_help" require "minitest/mock" require "minitest/autorun" require "mocha/minitest" +require "aasm/minitest" VCR.configure do |config| config.cassette_library_dir = "test/vcr_cassettes" diff --git a/test/vcr_cassettes/synth/exchange_rate.yml b/test/vcr_cassettes/synth/exchange_rate.yml index be1fa77a..adc22263 100644 --- a/test/vcr_cassettes/synth/exchange_rate.yml +++ b/test/vcr_cassettes/synth/exchange_rate.yml @@ -14,7 +14,7 @@ http_interactions: X-Source-Type: - managed User-Agent: - - Faraday v2.12.2 + - Faraday v2.13.1 Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -25,7 +25,7 @@ http_interactions: message: OK headers: Date: - - Sat, 15 Mar 2025 22:18:46 GMT + - Fri, 16 May 2025 13:01:38 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -35,11 +35,11 @@ http_interactions: Cache-Control: - max-age=0, private, must-revalidate Etag: - - W/"b0b21c870fe53492404cc5ac258fa465" + - W/"0c93a67d0c68e6f206e2954a41aa2933" Referrer-Policy: - strict-origin-when-cross-origin Rndr-Id: - - 44367fcb-e5b4-457d + - 146e30b2-e03b-47e3 Strict-Transport-Security: - max-age=63072000; includeSubDomains Vary: @@ -53,15 +53,15 @@ http_interactions: X-Render-Origin-Server: - Render X-Request-Id: - - 8ce9dc85-afbd-437c-b18d-ec788b712334 + - 3cf7ade1-8066-422a-97c7-5f8b99e24296 X-Runtime: - - '0.031963' + - '0.024284' X-Xss-Protection: - '0' Cf-Cache-Status: - DYNAMIC Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=SwRPS1vBsrKtk%2Ftb7Ix8j%2FCWYw9tZgbJxR1FCmotWn%2FIZAE3Ri%2FUwHtvkOSqBq6HN5pLVetfem5hp%2BkqWmD5GRCVho0mp3VgRr3J1tBMwrVK2p50tfpmb3X22Jj%2BOfapq1C22PnN"}],"group":"cf-nel","max_age":604800}' + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ih8sEFqAOWyINqAEtKGKPKO2lr1qAYSVeipyB5F8g2umPODXvCD4hN3G6wTTs2Q7H8CDWsqiOlYkmVvmr%2BWvl2ojOtBwO25Ahk9TbhlcgRO9nT6mEIXOSdVXJpzpRn5Ov%2FMGigpQ"}],"group":"cf-nel","max_age":604800}' Nel: - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' Speculation-Rules: @@ -69,13 +69,13 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 920f6378fe582237-ORD + - 940b109b5df1a3d7-ORD Alt-Svc: - h3=":443"; ma=86400 Server-Timing: - - cfL4;desc="?proto=TCP&rtt=26670&min_rtt=26569&rtt_var=10167&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2829&recv_bytes=922&delivery_rate=105759&cwnd=181&unsent_bytes=0&cid=f0a872e0b2909c59&ts=188&x=0" + - cfL4;desc="?proto=TCP&rtt=25865&min_rtt=25683&rtt_var=9996&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=922&delivery_rate=106690&cwnd=219&unsent_bytes=0&cid=e48ae188d1f86721&ts=190&x=0" body: encoding: ASCII-8BIT - string: '{"data":{"date":"2024-01-01","source":"USD","rates":{"GBP":0.785476}},"meta":{"total_records":1,"credits_used":1,"credits_remaining":249830,"date":"2024-01-01"}}' - recorded_at: Sat, 15 Mar 2025 22:18:46 GMT + string: '{"data":{"date":"2024-01-01","source":"USD","rates":{"GBP":0.785476}},"meta":{"total_records":1,"credits_used":1,"credits_remaining":249734,"date":"2024-01-01"}}' + recorded_at: Fri, 16 May 2025 13:01:38 GMT recorded_with: VCR 6.3.1 diff --git a/test/vcr_cassettes/synth/exchange_rates.yml b/test/vcr_cassettes/synth/exchange_rates.yml index ffb7b69a..87071b8f 100644 --- a/test/vcr_cassettes/synth/exchange_rates.yml +++ b/test/vcr_cassettes/synth/exchange_rates.yml @@ -14,7 +14,7 @@ http_interactions: X-Source-Type: - managed User-Agent: - - Faraday v2.12.2 + - Faraday v2.13.1 Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -25,7 +25,7 @@ http_interactions: message: OK headers: Date: - - Sat, 15 Mar 2025 21:48:33 GMT + - Fri, 16 May 2025 13:01:35 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -35,11 +35,11 @@ http_interactions: Cache-Control: - max-age=0, private, must-revalidate Etag: - - W/"8081859271e9ca46ee021f706a0cc683" + - W/"ad21b1fba71fe0b149fe37b483a60438" Referrer-Policy: - strict-origin-when-cross-origin Rndr-Id: - - 6d036078-7f2f-4037 + - 28bc6622-47b8-4aeb Strict-Transport-Security: - max-age=63072000; includeSubDomains Vary: @@ -53,15 +53,15 @@ http_interactions: X-Render-Origin-Server: - Render X-Request-Id: - - 9ec8d111-aa67-4fb9-8885-7de64e1b1219 + - fcf251a3-f850-4464-9592-ced9de5e0c86 X-Runtime: - - '0.025769' + - '0.080857' X-Xss-Protection: - '0' Cf-Cache-Status: - DYNAMIC Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=3PGbjN13Yz7GFiZNw1N13jCnLyMkC1O69nVw4k9Y0Iif7pu0H1eBKZxhkRTGzeECSRtzryqMRpzh9lG11e9SVXA9PNTSTR1%2BC%2FZkOMTsFUk%2Fajh29RmkkGeYrQgCAPEWBST36B3V"}],"group":"cf-nel","max_age":604800}' + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=622lysAXubNaj3TsuhR9RYZRXPc%2BgnyMWj52fxy%2BptvXoPr%2FxVJgJZ0g02mOUjCywdAymkMpawfWCaZVQOIaPVpocco3g4Y%2B0FB667ilf3UtCyiHwqCosUq0T99JabIsgFFJ%2FhP4"}],"group":"cf-nel","max_age":604800}' Nel: - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' Speculation-Rules: @@ -69,13 +69,13 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 920f37347b14e7f9-ORD + - 940b108a2921607d-ORD Alt-Svc: - h3=":443"; ma=86400 Server-Timing: - - cfL4;desc="?proto=TCP&rtt=27528&min_rtt=26760&rtt_var=11571&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2828&recv_bytes=961&delivery_rate=88005&cwnd=248&unsent_bytes=0&cid=28a3fac05fc0df52&ts=177&x=0" + - cfL4;desc="?proto=TCP&rtt=25729&min_rtt=25575&rtt_var=9899&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2825&recv_bytes=961&delivery_rate=108019&cwnd=251&unsent_bytes=0&cid=ca574e4a637aba29&ts=241&x=0" body: encoding: ASCII-8BIT - string: '{"data":[{"date":"2024-01-01","source":"USD","rates":{"GBP":0.785476}},{"date":"2024-01-02","source":"USD","rates":{"GBP":0.785644}},{"date":"2024-01-03","source":"USD","rates":{"GBP":0.792232}},{"date":"2024-01-04","source":"USD","rates":{"GBP":0.789053}},{"date":"2024-01-05","source":"USD","rates":{"GBP":0.788487}},{"date":"2024-01-06","source":"USD","rates":{"GBP":0.785787}},{"date":"2024-01-07","source":"USD","rates":{"GBP":0.785994}},{"date":"2024-01-08","source":"USD","rates":{"GBP":0.786378}},{"date":"2024-01-09","source":"USD","rates":{"GBP":0.784775}},{"date":"2024-01-10","source":"USD","rates":{"GBP":0.786769}},{"date":"2024-01-11","source":"USD","rates":{"GBP":0.784633}},{"date":"2024-01-12","source":"USD","rates":{"GBP":0.782576}},{"date":"2024-01-13","source":"USD","rates":{"GBP":0.78447}},{"date":"2024-01-14","source":"USD","rates":{"GBP":0.784423}},{"date":"2024-01-15","source":"USD","rates":{"GBP":0.785204}},{"date":"2024-01-16","source":"USD","rates":{"GBP":0.786438}},{"date":"2024-01-17","source":"USD","rates":{"GBP":0.791264}},{"date":"2024-01-18","source":"USD","rates":{"GBP":0.788852}},{"date":"2024-01-19","source":"USD","rates":{"GBP":0.786744}},{"date":"2024-01-20","source":"USD","rates":{"GBP":0.787186}},{"date":"2024-01-21","source":"USD","rates":{"GBP":0.787166}},{"date":"2024-01-22","source":"USD","rates":{"GBP":0.787487}},{"date":"2024-01-23","source":"USD","rates":{"GBP":0.786985}},{"date":"2024-01-24","source":"USD","rates":{"GBP":0.787961}},{"date":"2024-01-25","source":"USD","rates":{"GBP":0.786236}},{"date":"2024-01-26","source":"USD","rates":{"GBP":0.786961}},{"date":"2024-01-27","source":"USD","rates":{"GBP":0.786935}},{"date":"2024-01-28","source":"USD","rates":{"GBP":0.787014}},{"date":"2024-01-29","source":"USD","rates":{"GBP":0.78761}},{"date":"2024-01-30","source":"USD","rates":{"GBP":0.786652}},{"date":"2024-01-31","source":"USD","rates":{"GBP":0.787736}},{"date":"2024-02-01","source":"USD","rates":{"GBP":0.788759}},{"date":"2024-02-02","source":"USD","rates":{"GBP":0.784546}},{"date":"2024-02-03","source":"USD","rates":{"GBP":0.791634}},{"date":"2024-02-04","source":"USD","rates":{"GBP":0.791637}},{"date":"2024-02-05","source":"USD","rates":{"GBP":0.792205}},{"date":"2024-02-06","source":"USD","rates":{"GBP":0.797836}},{"date":"2024-02-07","source":"USD","rates":{"GBP":0.79341}},{"date":"2024-02-08","source":"USD","rates":{"GBP":0.791971}},{"date":"2024-02-09","source":"USD","rates":{"GBP":0.792371}},{"date":"2024-02-10","source":"USD","rates":{"GBP":0.791997}},{"date":"2024-02-11","source":"USD","rates":{"GBP":0.792019}},{"date":"2024-02-12","source":"USD","rates":{"GBP":0.791339}},{"date":"2024-02-13","source":"USD","rates":{"GBP":0.791977}},{"date":"2024-02-14","source":"USD","rates":{"GBP":0.794262}},{"date":"2024-02-15","source":"USD","rates":{"GBP":0.795709}},{"date":"2024-02-16","source":"USD","rates":{"GBP":0.793714}},{"date":"2024-02-17","source":"USD","rates":{"GBP":0.793499}},{"date":"2024-02-18","source":"USD","rates":{"GBP":0.79367}},{"date":"2024-02-19","source":"USD","rates":{"GBP":0.792968}},{"date":"2024-02-20","source":"USD","rates":{"GBP":0.794437}},{"date":"2024-02-21","source":"USD","rates":{"GBP":0.791988}},{"date":"2024-02-22","source":"USD","rates":{"GBP":0.791262}},{"date":"2024-02-23","source":"USD","rates":{"GBP":0.789749}},{"date":"2024-02-24","source":"USD","rates":{"GBP":0.78886}},{"date":"2024-02-25","source":"USD","rates":{"GBP":0.789107}},{"date":"2024-02-26","source":"USD","rates":{"GBP":0.78917}},{"date":"2024-02-27","source":"USD","rates":{"GBP":0.788381}},{"date":"2024-02-28","source":"USD","rates":{"GBP":0.78861}},{"date":"2024-02-29","source":"USD","rates":{"GBP":0.789837}},{"date":"2024-03-01","source":"USD","rates":{"GBP":0.792028}},{"date":"2024-03-02","source":"USD","rates":{"GBP":0.790312}},{"date":"2024-03-03","source":"USD","rates":{"GBP":0.790258}},{"date":"2024-03-04","source":"USD","rates":{"GBP":0.789891}},{"date":"2024-03-05","source":"USD","rates":{"GBP":0.788025}},{"date":"2024-03-06","source":"USD","rates":{"GBP":0.787136}},{"date":"2024-03-07","source":"USD","rates":{"GBP":0.785219}},{"date":"2024-03-08","source":"USD","rates":{"GBP":0.780438}},{"date":"2024-03-09","source":"USD","rates":{"GBP":0.777772}},{"date":"2024-03-10","source":"USD","rates":{"GBP":0.777884}},{"date":"2024-03-11","source":"USD","rates":{"GBP":0.77786}},{"date":"2024-03-12","source":"USD","rates":{"GBP":0.780067}},{"date":"2024-03-13","source":"USD","rates":{"GBP":0.781535}},{"date":"2024-03-14","source":"USD","rates":{"GBP":0.781184}},{"date":"2024-03-15","source":"USD","rates":{"GBP":0.784604}},{"date":"2024-03-16","source":"USD","rates":{"GBP":0.785537}},{"date":"2024-03-17","source":"USD","rates":{"GBP":0.785147}},{"date":"2024-03-18","source":"USD","rates":{"GBP":0.785457}},{"date":"2024-03-19","source":"USD","rates":{"GBP":0.785746}},{"date":"2024-03-20","source":"USD","rates":{"GBP":0.786238}},{"date":"2024-03-21","source":"USD","rates":{"GBP":0.781351}},{"date":"2024-03-22","source":"USD","rates":{"GBP":0.789841}},{"date":"2024-03-23","source":"USD","rates":{"GBP":0.793659}},{"date":"2024-03-24","source":"USD","rates":{"GBP":0.793385}},{"date":"2024-03-25","source":"USD","rates":{"GBP":0.793673}},{"date":"2024-03-26","source":"USD","rates":{"GBP":0.791344}},{"date":"2024-03-27","source":"USD","rates":{"GBP":0.791899}},{"date":"2024-03-28","source":"USD","rates":{"GBP":0.792585}},{"date":"2024-03-29","source":"USD","rates":{"GBP":0.792205}},{"date":"2024-03-30","source":"USD","rates":{"GBP":0.792228}},{"date":"2024-03-31","source":"USD","rates":{"GBP":0.792057}},{"date":"2024-04-01","source":"USD","rates":{"GBP":0.79134}},{"date":"2024-04-02","source":"USD","rates":{"GBP":0.797058}},{"date":"2024-04-03","source":"USD","rates":{"GBP":0.795147}},{"date":"2024-04-04","source":"USD","rates":{"GBP":0.790398}},{"date":"2024-04-05","source":"USD","rates":{"GBP":0.791151}},{"date":"2024-04-06","source":"USD","rates":{"GBP":0.791314}},{"date":"2024-04-07","source":"USD","rates":{"GBP":0.791273}},{"date":"2024-04-08","source":"USD","rates":{"GBP":0.792111}},{"date":"2024-04-09","source":"USD","rates":{"GBP":0.790047}},{"date":"2024-04-10","source":"USD","rates":{"GBP":0.788828}},{"date":"2024-04-11","source":"USD","rates":{"GBP":0.797646}},{"date":"2024-04-12","source":"USD","rates":{"GBP":0.796524}},{"date":"2024-04-13","source":"USD","rates":{"GBP":0.803024}},{"date":"2024-04-14","source":"USD","rates":{"GBP":0.802912}},{"date":"2024-04-15","source":"USD","rates":{"GBP":0.8025}},{"date":"2024-04-16","source":"USD","rates":{"GBP":0.80344}},{"date":"2024-04-17","source":"USD","rates":{"GBP":0.804505}},{"date":"2024-04-18","source":"USD","rates":{"GBP":0.80301}},{"date":"2024-04-19","source":"USD","rates":{"GBP":0.804145}},{"date":"2024-04-20","source":"USD","rates":{"GBP":0.80845}},{"date":"2024-04-21","source":"USD","rates":{"GBP":0.808199}},{"date":"2024-04-22","source":"USD","rates":{"GBP":0.808004}},{"date":"2024-04-23","source":"USD","rates":{"GBP":0.809734}},{"date":"2024-04-24","source":"USD","rates":{"GBP":0.802955}},{"date":"2024-04-25","source":"USD","rates":{"GBP":0.80264}},{"date":"2024-04-26","source":"USD","rates":{"GBP":0.799526}},{"date":"2024-04-27","source":"USD","rates":{"GBP":0.80053}},{"date":"2024-04-28","source":"USD","rates":{"GBP":0.800761}},{"date":"2024-04-29","source":"USD","rates":{"GBP":0.799397}},{"date":"2024-04-30","source":"USD","rates":{"GBP":0.796217}},{"date":"2024-05-01","source":"USD","rates":{"GBP":0.800703}},{"date":"2024-05-02","source":"USD","rates":{"GBP":0.797562}},{"date":"2024-05-03","source":"USD","rates":{"GBP":0.797457}},{"date":"2024-05-04","source":"USD","rates":{"GBP":0.797001}},{"date":"2024-05-05","source":"USD","rates":{"GBP":0.797107}},{"date":"2024-05-06","source":"USD","rates":{"GBP":0.797363}},{"date":"2024-05-07","source":"USD","rates":{"GBP":0.796218}},{"date":"2024-05-08","source":"USD","rates":{"GBP":0.799915}},{"date":"2024-05-09","source":"USD","rates":{"GBP":0.800422}},{"date":"2024-05-10","source":"USD","rates":{"GBP":0.798411}},{"date":"2024-05-11","source":"USD","rates":{"GBP":0.798489}},{"date":"2024-05-12","source":"USD","rates":{"GBP":0.798475}},{"date":"2024-05-13","source":"USD","rates":{"GBP":0.79853}},{"date":"2024-05-14","source":"USD","rates":{"GBP":0.796122}},{"date":"2024-05-15","source":"USD","rates":{"GBP":0.794614}},{"date":"2024-05-16","source":"USD","rates":{"GBP":0.78804}},{"date":"2024-05-17","source":"USD","rates":{"GBP":0.789188}},{"date":"2024-05-18","source":"USD","rates":{"GBP":0.787162}},{"date":"2024-05-19","source":"USD","rates":{"GBP":0.787194}},{"date":"2024-05-20","source":"USD","rates":{"GBP":0.787022}},{"date":"2024-05-21","source":"USD","rates":{"GBP":0.786793}},{"date":"2024-05-22","source":"USD","rates":{"GBP":0.786723}},{"date":"2024-05-23","source":"USD","rates":{"GBP":0.786132}},{"date":"2024-05-24","source":"USD","rates":{"GBP":0.78778}},{"date":"2024-05-25","source":"USD","rates":{"GBP":0.785013}},{"date":"2024-05-26","source":"USD","rates":{"GBP":0.785081}},{"date":"2024-05-27","source":"USD","rates":{"GBP":0.78526}},{"date":"2024-05-28","source":"USD","rates":{"GBP":0.78296}},{"date":"2024-05-29","source":"USD","rates":{"GBP":0.783808}},{"date":"2024-05-30","source":"USD","rates":{"GBP":0.787552}},{"date":"2024-05-31","source":"USD","rates":{"GBP":0.785599}},{"date":"2024-06-01","source":"USD","rates":{"GBP":0.785113}},{"date":"2024-06-02","source":"USD","rates":{"GBP":0.785019}},{"date":"2024-06-03","source":"USD","rates":{"GBP":0.784657}},{"date":"2024-06-04","source":"USD","rates":{"GBP":0.780649}},{"date":"2024-06-05","source":"USD","rates":{"GBP":0.782934}},{"date":"2024-06-06","source":"USD","rates":{"GBP":0.781631}},{"date":"2024-06-07","source":"USD","rates":{"GBP":0.781732}},{"date":"2024-06-08","source":"USD","rates":{"GBP":0.785947}},{"date":"2024-06-09","source":"USD","rates":{"GBP":0.785767}},{"date":"2024-06-10","source":"USD","rates":{"GBP":0.785588}},{"date":"2024-06-11","source":"USD","rates":{"GBP":0.785791}},{"date":"2024-06-12","source":"USD","rates":{"GBP":0.784932}},{"date":"2024-06-13","source":"USD","rates":{"GBP":0.781472}},{"date":"2024-06-14","source":"USD","rates":{"GBP":0.784041}},{"date":"2024-06-15","source":"USD","rates":{"GBP":0.789096}},{"date":"2024-06-16","source":"USD","rates":{"GBP":0.788449}},{"date":"2024-06-17","source":"USD","rates":{"GBP":0.788479}},{"date":"2024-06-18","source":"USD","rates":{"GBP":0.786542}},{"date":"2024-06-19","source":"USD","rates":{"GBP":0.786916}},{"date":"2024-06-20","source":"USD","rates":{"GBP":0.786107}},{"date":"2024-06-21","source":"USD","rates":{"GBP":0.789875}},{"date":"2024-06-22","source":"USD","rates":{"GBP":0.79058}},{"date":"2024-06-23","source":"USD","rates":{"GBP":0.790546}},{"date":"2024-06-24","source":"USD","rates":{"GBP":0.791248}},{"date":"2024-06-25","source":"USD","rates":{"GBP":0.788496}},{"date":"2024-06-26","source":"USD","rates":{"GBP":0.788395}},{"date":"2024-06-27","source":"USD","rates":{"GBP":0.792298}},{"date":"2024-06-28","source":"USD","rates":{"GBP":0.79087}},{"date":"2024-06-29","source":"USD","rates":{"GBP":0.790726}},{"date":"2024-06-30","source":"USD","rates":{"GBP":0.790719}},{"date":"2024-07-01","source":"USD","rates":{"GBP":0.790622}},{"date":"2024-07-02","source":"USD","rates":{"GBP":0.790812}},{"date":"2024-07-03","source":"USD","rates":{"GBP":0.78816}},{"date":"2024-07-04","source":"USD","rates":{"GBP":0.784451}},{"date":"2024-07-05","source":"USD","rates":{"GBP":0.783992}},{"date":"2024-07-06","source":"USD","rates":{"GBP":0.780243}},{"date":"2024-07-07","source":"USD","rates":{"GBP":0.780594}},{"date":"2024-07-08","source":"USD","rates":{"GBP":0.780827}},{"date":"2024-07-09","source":"USD","rates":{"GBP":0.780333}},{"date":"2024-07-10","source":"USD","rates":{"GBP":0.781936}},{"date":"2024-07-11","source":"USD","rates":{"GBP":0.777992}},{"date":"2024-07-12","source":"USD","rates":{"GBP":0.773816}},{"date":"2024-07-13","source":"USD","rates":{"GBP":0.770374}},{"date":"2024-07-14","source":"USD","rates":{"GBP":0.770294}},{"date":"2024-07-15","source":"USD","rates":{"GBP":0.771174}},{"date":"2024-07-16","source":"USD","rates":{"GBP":0.771041}},{"date":"2024-07-17","source":"USD","rates":{"GBP":0.770574}},{"date":"2024-07-18","source":"USD","rates":{"GBP":0.768775}},{"date":"2024-07-19","source":"USD","rates":{"GBP":0.772195}},{"date":"2024-07-20","source":"USD","rates":{"GBP":0.774311}},{"date":"2024-07-21","source":"USD","rates":{"GBP":0.774096}},{"date":"2024-07-22","source":"USD","rates":{"GBP":0.773251}},{"date":"2024-07-23","source":"USD","rates":{"GBP":0.773304}},{"date":"2024-07-24","source":"USD","rates":{"GBP":0.775165}},{"date":"2024-07-25","source":"USD","rates":{"GBP":0.775289}},{"date":"2024-07-26","source":"USD","rates":{"GBP":0.777882}},{"date":"2024-07-27","source":"USD","rates":{"GBP":0.777203}},{"date":"2024-07-28","source":"USD","rates":{"GBP":0.776969}},{"date":"2024-07-29","source":"USD","rates":{"GBP":0.777176}},{"date":"2024-07-30","source":"USD","rates":{"GBP":0.777613}},{"date":"2024-07-31","source":"USD","rates":{"GBP":0.778999}}],"paging":{"prev":"/rates/historical-range?date_end=2024-07-31\u0026date_start=2024-01-01\u0026from=USD\u0026page=\u0026to=GBP","next":"/rates/historical-range?date_end=2024-07-31\u0026date_start=2024-01-01\u0026from=USD\u0026page=\u0026to=GBP","total_records":213,"current_page":1,"per_page":500,"total_pages":1},"meta":{"credits_used":1,"credits_remaining":249832,"date_start":"2024-01-01","date_end":"2024-07-31"}}' - recorded_at: Sat, 15 Mar 2025 21:48:33 GMT + string: '{"data":[{"date":"2024-01-01","source":"USD","rates":{"GBP":0.785476}},{"date":"2024-01-02","source":"USD","rates":{"GBP":0.785644}},{"date":"2024-01-03","source":"USD","rates":{"GBP":0.792232}},{"date":"2024-01-04","source":"USD","rates":{"GBP":0.789053}},{"date":"2024-01-05","source":"USD","rates":{"GBP":0.788487}},{"date":"2024-01-06","source":"USD","rates":{"GBP":0.785787}},{"date":"2024-01-07","source":"USD","rates":{"GBP":0.785994}},{"date":"2024-01-08","source":"USD","rates":{"GBP":0.786378}},{"date":"2024-01-09","source":"USD","rates":{"GBP":0.784775}},{"date":"2024-01-10","source":"USD","rates":{"GBP":0.786769}},{"date":"2024-01-11","source":"USD","rates":{"GBP":0.784633}},{"date":"2024-01-12","source":"USD","rates":{"GBP":0.782576}},{"date":"2024-01-13","source":"USD","rates":{"GBP":0.78447}},{"date":"2024-01-14","source":"USD","rates":{"GBP":0.784423}},{"date":"2024-01-15","source":"USD","rates":{"GBP":0.785204}},{"date":"2024-01-16","source":"USD","rates":{"GBP":0.786438}},{"date":"2024-01-17","source":"USD","rates":{"GBP":0.791264}},{"date":"2024-01-18","source":"USD","rates":{"GBP":0.788852}},{"date":"2024-01-19","source":"USD","rates":{"GBP":0.786744}},{"date":"2024-01-20","source":"USD","rates":{"GBP":0.787186}},{"date":"2024-01-21","source":"USD","rates":{"GBP":0.787166}},{"date":"2024-01-22","source":"USD","rates":{"GBP":0.787487}},{"date":"2024-01-23","source":"USD","rates":{"GBP":0.786985}},{"date":"2024-01-24","source":"USD","rates":{"GBP":0.787961}},{"date":"2024-01-25","source":"USD","rates":{"GBP":0.786236}},{"date":"2024-01-26","source":"USD","rates":{"GBP":0.786961}},{"date":"2024-01-27","source":"USD","rates":{"GBP":0.786935}},{"date":"2024-01-28","source":"USD","rates":{"GBP":0.787014}},{"date":"2024-01-29","source":"USD","rates":{"GBP":0.78761}},{"date":"2024-01-30","source":"USD","rates":{"GBP":0.786652}},{"date":"2024-01-31","source":"USD","rates":{"GBP":0.787736}},{"date":"2024-02-01","source":"USD","rates":{"GBP":0.788759}},{"date":"2024-02-02","source":"USD","rates":{"GBP":0.784546}},{"date":"2024-02-03","source":"USD","rates":{"GBP":0.791634}},{"date":"2024-02-04","source":"USD","rates":{"GBP":0.791637}},{"date":"2024-02-05","source":"USD","rates":{"GBP":0.792205}},{"date":"2024-02-06","source":"USD","rates":{"GBP":0.797836}},{"date":"2024-02-07","source":"USD","rates":{"GBP":0.79341}},{"date":"2024-02-08","source":"USD","rates":{"GBP":0.791971}},{"date":"2024-02-09","source":"USD","rates":{"GBP":0.792371}},{"date":"2024-02-10","source":"USD","rates":{"GBP":0.791997}},{"date":"2024-02-11","source":"USD","rates":{"GBP":0.792019}},{"date":"2024-02-12","source":"USD","rates":{"GBP":0.791339}},{"date":"2024-02-13","source":"USD","rates":{"GBP":0.791977}},{"date":"2024-02-14","source":"USD","rates":{"GBP":0.794262}},{"date":"2024-02-15","source":"USD","rates":{"GBP":0.795709}},{"date":"2024-02-16","source":"USD","rates":{"GBP":0.793714}},{"date":"2024-02-17","source":"USD","rates":{"GBP":0.793499}},{"date":"2024-02-18","source":"USD","rates":{"GBP":0.79367}},{"date":"2024-02-19","source":"USD","rates":{"GBP":0.792968}},{"date":"2024-02-20","source":"USD","rates":{"GBP":0.794437}},{"date":"2024-02-21","source":"USD","rates":{"GBP":0.791988}},{"date":"2024-02-22","source":"USD","rates":{"GBP":0.791262}},{"date":"2024-02-23","source":"USD","rates":{"GBP":0.789749}},{"date":"2024-02-24","source":"USD","rates":{"GBP":0.78886}},{"date":"2024-02-25","source":"USD","rates":{"GBP":0.789107}},{"date":"2024-02-26","source":"USD","rates":{"GBP":0.78917}},{"date":"2024-02-27","source":"USD","rates":{"GBP":0.788381}},{"date":"2024-02-28","source":"USD","rates":{"GBP":0.78861}},{"date":"2024-02-29","source":"USD","rates":{"GBP":0.789837}},{"date":"2024-03-01","source":"USD","rates":{"GBP":0.792028}},{"date":"2024-03-02","source":"USD","rates":{"GBP":0.790312}},{"date":"2024-03-03","source":"USD","rates":{"GBP":0.790258}},{"date":"2024-03-04","source":"USD","rates":{"GBP":0.789891}},{"date":"2024-03-05","source":"USD","rates":{"GBP":0.788025}},{"date":"2024-03-06","source":"USD","rates":{"GBP":0.787136}},{"date":"2024-03-07","source":"USD","rates":{"GBP":0.785219}},{"date":"2024-03-08","source":"USD","rates":{"GBP":0.780438}},{"date":"2024-03-09","source":"USD","rates":{"GBP":0.777772}},{"date":"2024-03-10","source":"USD","rates":{"GBP":0.777884}},{"date":"2024-03-11","source":"USD","rates":{"GBP":0.77786}},{"date":"2024-03-12","source":"USD","rates":{"GBP":0.780067}},{"date":"2024-03-13","source":"USD","rates":{"GBP":0.781535}},{"date":"2024-03-14","source":"USD","rates":{"GBP":0.781184}},{"date":"2024-03-15","source":"USD","rates":{"GBP":0.784604}},{"date":"2024-03-16","source":"USD","rates":{"GBP":0.785537}},{"date":"2024-03-17","source":"USD","rates":{"GBP":0.785147}},{"date":"2024-03-18","source":"USD","rates":{"GBP":0.785457}},{"date":"2024-03-19","source":"USD","rates":{"GBP":0.785746}},{"date":"2024-03-20","source":"USD","rates":{"GBP":0.786238}},{"date":"2024-03-21","source":"USD","rates":{"GBP":0.781351}},{"date":"2024-03-22","source":"USD","rates":{"GBP":0.789841}},{"date":"2024-03-23","source":"USD","rates":{"GBP":0.793659}},{"date":"2024-03-24","source":"USD","rates":{"GBP":0.793385}},{"date":"2024-03-25","source":"USD","rates":{"GBP":0.793673}},{"date":"2024-03-26","source":"USD","rates":{"GBP":0.791344}},{"date":"2024-03-27","source":"USD","rates":{"GBP":0.791899}},{"date":"2024-03-28","source":"USD","rates":{"GBP":0.792585}},{"date":"2024-03-29","source":"USD","rates":{"GBP":0.792205}},{"date":"2024-03-30","source":"USD","rates":{"GBP":0.792228}},{"date":"2024-03-31","source":"USD","rates":{"GBP":0.792057}},{"date":"2024-04-01","source":"USD","rates":{"GBP":0.79134}},{"date":"2024-04-02","source":"USD","rates":{"GBP":0.797058}},{"date":"2024-04-03","source":"USD","rates":{"GBP":0.795147}},{"date":"2024-04-04","source":"USD","rates":{"GBP":0.790398}},{"date":"2024-04-05","source":"USD","rates":{"GBP":0.791151}},{"date":"2024-04-06","source":"USD","rates":{"GBP":0.791314}},{"date":"2024-04-07","source":"USD","rates":{"GBP":0.791273}},{"date":"2024-04-08","source":"USD","rates":{"GBP":0.792111}},{"date":"2024-04-09","source":"USD","rates":{"GBP":0.790047}},{"date":"2024-04-10","source":"USD","rates":{"GBP":0.788828}},{"date":"2024-04-11","source":"USD","rates":{"GBP":0.797646}},{"date":"2024-04-12","source":"USD","rates":{"GBP":0.796524}},{"date":"2024-04-13","source":"USD","rates":{"GBP":0.803024}},{"date":"2024-04-14","source":"USD","rates":{"GBP":0.802912}},{"date":"2024-04-15","source":"USD","rates":{"GBP":0.8025}},{"date":"2024-04-16","source":"USD","rates":{"GBP":0.80344}},{"date":"2024-04-17","source":"USD","rates":{"GBP":0.804505}},{"date":"2024-04-18","source":"USD","rates":{"GBP":0.80301}},{"date":"2024-04-19","source":"USD","rates":{"GBP":0.804145}},{"date":"2024-04-20","source":"USD","rates":{"GBP":0.80845}},{"date":"2024-04-21","source":"USD","rates":{"GBP":0.808199}},{"date":"2024-04-22","source":"USD","rates":{"GBP":0.808004}},{"date":"2024-04-23","source":"USD","rates":{"GBP":0.809734}},{"date":"2024-04-24","source":"USD","rates":{"GBP":0.802955}},{"date":"2024-04-25","source":"USD","rates":{"GBP":0.80264}},{"date":"2024-04-26","source":"USD","rates":{"GBP":0.799526}},{"date":"2024-04-27","source":"USD","rates":{"GBP":0.80053}},{"date":"2024-04-28","source":"USD","rates":{"GBP":0.800761}},{"date":"2024-04-29","source":"USD","rates":{"GBP":0.799397}},{"date":"2024-04-30","source":"USD","rates":{"GBP":0.796217}},{"date":"2024-05-01","source":"USD","rates":{"GBP":0.800703}},{"date":"2024-05-02","source":"USD","rates":{"GBP":0.797562}},{"date":"2024-05-03","source":"USD","rates":{"GBP":0.797457}},{"date":"2024-05-04","source":"USD","rates":{"GBP":0.797001}},{"date":"2024-05-05","source":"USD","rates":{"GBP":0.797107}},{"date":"2024-05-06","source":"USD","rates":{"GBP":0.797363}},{"date":"2024-05-07","source":"USD","rates":{"GBP":0.796218}},{"date":"2024-05-08","source":"USD","rates":{"GBP":0.799915}},{"date":"2024-05-09","source":"USD","rates":{"GBP":0.800422}},{"date":"2024-05-10","source":"USD","rates":{"GBP":0.798411}},{"date":"2024-05-11","source":"USD","rates":{"GBP":0.798489}},{"date":"2024-05-12","source":"USD","rates":{"GBP":0.798475}},{"date":"2024-05-13","source":"USD","rates":{"GBP":0.79853}},{"date":"2024-05-14","source":"USD","rates":{"GBP":0.796122}},{"date":"2024-05-15","source":"USD","rates":{"GBP":0.794614}},{"date":"2024-05-16","source":"USD","rates":{"GBP":0.78804}},{"date":"2024-05-17","source":"USD","rates":{"GBP":0.789188}},{"date":"2024-05-18","source":"USD","rates":{"GBP":0.787162}},{"date":"2024-05-19","source":"USD","rates":{"GBP":0.787194}},{"date":"2024-05-20","source":"USD","rates":{"GBP":0.787022}},{"date":"2024-05-21","source":"USD","rates":{"GBP":0.786793}},{"date":"2024-05-22","source":"USD","rates":{"GBP":0.786723}},{"date":"2024-05-23","source":"USD","rates":{"GBP":0.786132}},{"date":"2024-05-24","source":"USD","rates":{"GBP":0.78778}},{"date":"2024-05-25","source":"USD","rates":{"GBP":0.785013}},{"date":"2024-05-26","source":"USD","rates":{"GBP":0.785081}},{"date":"2024-05-27","source":"USD","rates":{"GBP":0.78526}},{"date":"2024-05-28","source":"USD","rates":{"GBP":0.78296}},{"date":"2024-05-29","source":"USD","rates":{"GBP":0.783808}},{"date":"2024-05-30","source":"USD","rates":{"GBP":0.787552}},{"date":"2024-05-31","source":"USD","rates":{"GBP":0.785599}},{"date":"2024-06-01","source":"USD","rates":{"GBP":0.785113}},{"date":"2024-06-02","source":"USD","rates":{"GBP":0.785019}},{"date":"2024-06-03","source":"USD","rates":{"GBP":0.784657}},{"date":"2024-06-04","source":"USD","rates":{"GBP":0.780649}},{"date":"2024-06-05","source":"USD","rates":{"GBP":0.782934}},{"date":"2024-06-06","source":"USD","rates":{"GBP":0.781631}},{"date":"2024-06-07","source":"USD","rates":{"GBP":0.781732}},{"date":"2024-06-08","source":"USD","rates":{"GBP":0.785947}},{"date":"2024-06-09","source":"USD","rates":{"GBP":0.785767}},{"date":"2024-06-10","source":"USD","rates":{"GBP":0.785588}},{"date":"2024-06-11","source":"USD","rates":{"GBP":0.785791}},{"date":"2024-06-12","source":"USD","rates":{"GBP":0.784932}},{"date":"2024-06-13","source":"USD","rates":{"GBP":0.781472}},{"date":"2024-06-14","source":"USD","rates":{"GBP":0.784041}},{"date":"2024-06-15","source":"USD","rates":{"GBP":0.789096}},{"date":"2024-06-16","source":"USD","rates":{"GBP":0.788449}},{"date":"2024-06-17","source":"USD","rates":{"GBP":0.788479}},{"date":"2024-06-18","source":"USD","rates":{"GBP":0.786542}},{"date":"2024-06-19","source":"USD","rates":{"GBP":0.786916}},{"date":"2024-06-20","source":"USD","rates":{"GBP":0.786107}},{"date":"2024-06-21","source":"USD","rates":{"GBP":0.789875}},{"date":"2024-06-22","source":"USD","rates":{"GBP":0.79058}},{"date":"2024-06-23","source":"USD","rates":{"GBP":0.790546}},{"date":"2024-06-24","source":"USD","rates":{"GBP":0.791248}},{"date":"2024-06-25","source":"USD","rates":{"GBP":0.788496}},{"date":"2024-06-26","source":"USD","rates":{"GBP":0.788395}},{"date":"2024-06-27","source":"USD","rates":{"GBP":0.792298}},{"date":"2024-06-28","source":"USD","rates":{"GBP":0.79087}},{"date":"2024-06-29","source":"USD","rates":{"GBP":0.790726}},{"date":"2024-06-30","source":"USD","rates":{"GBP":0.790719}},{"date":"2024-07-01","source":"USD","rates":{"GBP":0.790622}},{"date":"2024-07-02","source":"USD","rates":{"GBP":0.790812}},{"date":"2024-07-03","source":"USD","rates":{"GBP":0.78816}},{"date":"2024-07-04","source":"USD","rates":{"GBP":0.784451}},{"date":"2024-07-05","source":"USD","rates":{"GBP":0.783992}},{"date":"2024-07-06","source":"USD","rates":{"GBP":0.780243}},{"date":"2024-07-07","source":"USD","rates":{"GBP":0.780594}},{"date":"2024-07-08","source":"USD","rates":{"GBP":0.780827}},{"date":"2024-07-09","source":"USD","rates":{"GBP":0.780333}},{"date":"2024-07-10","source":"USD","rates":{"GBP":0.781936}},{"date":"2024-07-11","source":"USD","rates":{"GBP":0.777992}},{"date":"2024-07-12","source":"USD","rates":{"GBP":0.773816}},{"date":"2024-07-13","source":"USD","rates":{"GBP":0.770374}},{"date":"2024-07-14","source":"USD","rates":{"GBP":0.770294}},{"date":"2024-07-15","source":"USD","rates":{"GBP":0.771174}},{"date":"2024-07-16","source":"USD","rates":{"GBP":0.771041}},{"date":"2024-07-17","source":"USD","rates":{"GBP":0.770574}},{"date":"2024-07-18","source":"USD","rates":{"GBP":0.768775}},{"date":"2024-07-19","source":"USD","rates":{"GBP":0.772195}},{"date":"2024-07-20","source":"USD","rates":{"GBP":0.774311}},{"date":"2024-07-21","source":"USD","rates":{"GBP":0.774096}},{"date":"2024-07-22","source":"USD","rates":{"GBP":0.773251}},{"date":"2024-07-23","source":"USD","rates":{"GBP":0.773304}},{"date":"2024-07-24","source":"USD","rates":{"GBP":0.775165}},{"date":"2024-07-25","source":"USD","rates":{"GBP":0.775289}},{"date":"2024-07-26","source":"USD","rates":{"GBP":0.777882}},{"date":"2024-07-27","source":"USD","rates":{"GBP":0.777203}},{"date":"2024-07-28","source":"USD","rates":{"GBP":0.776969}},{"date":"2024-07-29","source":"USD","rates":{"GBP":0.777176}},{"date":"2024-07-30","source":"USD","rates":{"GBP":0.777613}},{"date":"2024-07-31","source":"USD","rates":{"GBP":0.778999}}],"paging":{"prev":"/rates/historical-range?date_end=2024-07-31\u0026date_start=2024-01-01\u0026from=USD\u0026page=\u0026to=GBP","next":"/rates/historical-range?date_end=2024-07-31\u0026date_start=2024-01-01\u0026from=USD\u0026page=\u0026to=GBP","total_records":213,"current_page":1,"per_page":500,"total_pages":1},"meta":{"credits_used":1,"credits_remaining":249739,"date_start":"2024-01-01","date_end":"2024-07-31"}}' + recorded_at: Fri, 16 May 2025 13:01:35 GMT recorded_with: VCR 6.3.1 diff --git a/test/vcr_cassettes/synth/health.yml b/test/vcr_cassettes/synth/health.yml index 4d8ca054..24e3e67b 100644 --- a/test/vcr_cassettes/synth/health.yml +++ b/test/vcr_cassettes/synth/health.yml @@ -14,7 +14,7 @@ http_interactions: X-Source-Type: - managed User-Agent: - - Faraday v2.12.2 + - Faraday v2.13.1 Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -25,7 +25,7 @@ http_interactions: message: OK headers: Date: - - Sat, 15 Mar 2025 22:18:47 GMT + - Fri, 16 May 2025 13:01:39 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -35,11 +35,11 @@ http_interactions: Cache-Control: - max-age=0, private, must-revalidate Etag: - - W/"4ec3e0a20895d90b1e1241ca67f10ca3" + - W/"c5c1d51b68b499d00936c9eb1e8bfdbb" Referrer-Policy: - strict-origin-when-cross-origin Rndr-Id: - - 0cab64c9-e312-4bec + - 3abc1256-5517-44a7 Strict-Transport-Security: - max-age=63072000; includeSubDomains Vary: @@ -53,15 +53,15 @@ http_interactions: X-Render-Origin-Server: - Render X-Request-Id: - - 1958563c-7c18-4201-a03c-a4b343dc68ab + - aaf85301-dd16-4b9b-a3a4-c4fbcf1d3f55 X-Runtime: - - '0.014938' + - '0.014386' X-Xss-Protection: - '0' Cf-Cache-Status: - DYNAMIC Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=P3OWn4c8LFFWI0Dwr2CSYwHLaNhf9iD9TfAhqdx5PtLoWZ0pSImebfUsh00ZbOmh4r2cRJEQOmvy67wAwl6p0W%2Fx9017EkCnCaXibBBCKqJTBOdGnsSuV%2B45LrHsQmg%2BGeBwrw4b"}],"group":"cf-nel","max_age":604800}' + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=OaVSdNPSl6CQ8gbhnDzkCisX2ILOEWAwweMW3rXXP5rBKuxZoDT024srQWmHKGLsCEhpt4G9mqCthDwlHu2%2BuZ3AyTJQcnBONtE%2FNQ7fKT9x8nLz4mnqL8iyynLuRWQSUJ8SWMj5"}],"group":"cf-nel","max_age":604800}' Nel: - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' Speculation-Rules: @@ -69,14 +69,14 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 920f637aa8cf1152-ORD + - 940b109d086eb4b8-ORD Alt-Svc: - h3=":443"; ma=86400 Server-Timing: - - cfL4;desc="?proto=TCP&rtt=25627&min_rtt=25594&rtt_var=9664&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=878&delivery_rate=111991&cwnd=248&unsent_bytes=0&cid=c8e4c4e269114d14&ts=263&x=0" + - cfL4;desc="?proto=TCP&rtt=32457&min_rtt=26792&rtt_var=14094&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2826&recv_bytes=878&delivery_rate=108091&cwnd=229&unsent_bytes=0&cid=a6f330e4d5f16682&ts=309&x=0" body: encoding: ASCII-8BIT - string: '{"id":"user_3208c49393f54b3e974795e4bea5b864","email":"test@maybe.co","name":"Test - User","plan":"Business","api_calls_remaining":249830,"api_limit":250000,"credits_reset_at":"2025-04-01T00:00:00.000-04:00","current_period_start":"2025-03-01T00:00:00.000-05:00"}' - recorded_at: Sat, 15 Mar 2025 22:18:47 GMT + string: '{"id":"user_3208c49393f54b3e974795e4bea5b864","email":"zach@maybe.co","name":"Zach + Gollwitzer","plan":"Business","api_calls_remaining":249733,"api_limit":250000,"credits_reset_at":"2025-06-01T00:00:00.000-04:00","current_period_start":"2025-05-01T00:00:00.000-04:00"}' + recorded_at: Fri, 16 May 2025 13:01:39 GMT recorded_with: VCR 6.3.1 diff --git a/test/vcr_cassettes/synth/security_info.yml b/test/vcr_cassettes/synth/security_info.yml index 8122b882..8dbb76d1 100644 --- a/test/vcr_cassettes/synth/security_info.yml +++ b/test/vcr_cassettes/synth/security_info.yml @@ -14,7 +14,7 @@ http_interactions: X-Source-Type: - managed User-Agent: - - Faraday v2.12.2 + - Faraday v2.13.1 Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -25,7 +25,7 @@ http_interactions: message: OK headers: Date: - - Sun, 16 Mar 2025 12:04:12 GMT + - Fri, 16 May 2025 13:01:37 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -35,11 +35,11 @@ http_interactions: Cache-Control: - max-age=0, private, must-revalidate Etag: - - W/"a9deeb6437d359f080be449b9b2c547b" + - W/"75f336ad88e262c72044e8b865265298" Referrer-Policy: - strict-origin-when-cross-origin Rndr-Id: - - 1e77ae49-050a-45fc + - ba973abf-7d96-4a9a Strict-Transport-Security: - max-age=63072000; includeSubDomains Vary: @@ -53,15 +53,15 @@ http_interactions: X-Render-Origin-Server: - Render X-Request-Id: - - 222dacf1-37f3-4eb8-91d5-edf13d732d46 + - 76cb13a6-0d7e-4c36-8df9-bb63110d9e2a X-Runtime: - - '0.059222' + - '0.099716' X-Xss-Protection: - '0' Cf-Cache-Status: - DYNAMIC Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=%2BLW%2Fd%2BbcNg4%2FleO6ECyB4RJBMbm6vWG3%2FX4oKQXfn1ROSPVrISc3ZFVlXfITGW4XYJSPyUDF%2FXrrRF6p3Wzow07QamOrsux7sxBMvtWmcubgpCMFI4zgnhESklW6KcmAefwrgj9i"}],"group":"cf-nel","max_age":604800}' + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=aDn7ApAO9Ma86gZ%2BJKCUCFjH2Re%2BtXdB5gcqYj2KTGXJKNpgf5TNgzbrp5%2Bw%2FGL5nTvtp%2B7cxT8MMcLWjAV6Ne1r6z5YBFq1K4W7Zw5m1lhMiqYLnTnEs2Oq85TjzOvpsE%2BmC33d"}],"group":"cf-nel","max_age":604800}' Nel: - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' Speculation-Rules: @@ -69,11 +69,11 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 92141c97bfd9124c-ORD + - 940b10910abdd2ec-ORD Alt-Svc: - h3=":443"; ma=86400 Server-Timing: - - cfL4;desc="?proto=TCP&rtt=27459&min_rtt=26850&rtt_var=11288&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2828&recv_bytes=905&delivery_rate=91272&cwnd=104&unsent_bytes=0&cid=ccd6aa7e48e4b0eb&ts=287&x=0" + - cfL4;desc="?proto=TCP&rtt=28163&min_rtt=27237&rtt_var=12066&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=905&delivery_rate=83590&cwnd=239&unsent_bytes=0&cid=7ef62bd693b52ccd&ts=240&x=0" body: encoding: ASCII-8BIT string: '{"data":{"ticker":"AAPL","name":"Apple Inc.","links":{"homepage_url":"https://www.apple.com"},"logo_url":"https://logo.synthfinance.com/ticker/AAPL","description":"Apple @@ -100,6 +100,6 @@ http_interactions: Apple Park Way","city":"Cupertino","state":"CA","postal_code":"95014"},"exchange":{"name":"Nasdaq/Ngs (Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United States","country_code":"US","timezone":"America/New_York"},"ceo":"Mr. Timothy - D. Cook","founding_year":1976,"industry":"Consumer Electronics","sector":"Technology","phone":"408-996-1010","total_employees":161000,"composite_figi":"BBG000B9Y5X2","market_data":{"high_today":213.95,"low_today":209.58,"open_today":211.25,"close_today":213.49,"volume_today":60060200.0,"fifty_two_week_high":260.1,"fifty_two_week_low":164.08,"average_volume":62848099.37313433,"price_change":0.0,"percent_change":0.0}},"meta":{"credits_used":1,"credits_remaining":249808}}' - recorded_at: Sun, 16 Mar 2025 12:04:12 GMT + D. Cook","founding_year":1976,"industry":"Consumer Electronics","sector":"Technology","phone":"408-996-1010","total_employees":161000,"composite_figi":"BBG000B9Y5X2","market_data":{"high_today":212.96,"low_today":209.54,"open_today":210.95,"close_today":211.45,"volume_today":44979900.0,"fifty_two_week_high":260.1,"fifty_two_week_low":169.21,"average_volume":61769396.875,"price_change":0.0,"percent_change":0.0}},"meta":{"credits_used":1,"credits_remaining":249737}}' + recorded_at: Fri, 16 May 2025 13:01:37 GMT recorded_with: VCR 6.3.1 diff --git a/test/vcr_cassettes/synth/security_price.yml b/test/vcr_cassettes/synth/security_price.yml index 2e6d1dfb..5cd435d4 100644 --- a/test/vcr_cassettes/synth/security_price.yml +++ b/test/vcr_cassettes/synth/security_price.yml @@ -14,7 +14,7 @@ http_interactions: X-Source-Type: - managed User-Agent: - - Faraday v2.12.2 + - Faraday v2.13.1 Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -25,7 +25,7 @@ http_interactions: message: OK headers: Date: - - Sun, 16 Mar 2025 12:08:00 GMT + - Fri, 16 May 2025 13:01:36 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -35,11 +35,11 @@ http_interactions: Cache-Control: - max-age=0, private, must-revalidate Etag: - - W/"cdf04c2cd77e230c03117dd13d0921f9" + - W/"72340d82266397447b865407dda15492" Referrer-Policy: - strict-origin-when-cross-origin Rndr-Id: - - e74b3425-0b7c-447d + - 4c3462aa-2471-40b4 Strict-Transport-Security: - max-age=63072000; includeSubDomains Vary: @@ -53,15 +53,15 @@ http_interactions: X-Render-Origin-Server: - Render X-Request-Id: - - b906c5e1-18cc-44cc-9085-313ff066a6ce + - bdbc757d-2528-44c3-ae08-9788e8ee15f7 X-Runtime: - - '0.544708' + - '0.034898' X-Xss-Protection: - '0' Cf-Cache-Status: - DYNAMIC Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=dZNe6qCGGI2XGXgByLr69%2FYrDQdy2FLtnXafxJnlsvyVjrRFiCvmbbIzgF5CDgtj9HZ8RC5Rh9jbuEI6hPokpa3Al4FEIAZB5AbfZ9toP%2Bc5muG%2FuBgHR%2FnIZpsWG%2BQKmBPu9MBa"}],"group":"cf-nel","max_age":604800}' + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=2Mu4PK4XTsAq%2Bn1%2F2yxy%2Blj7kz3ZCiQ9t8ikr2m19BrhQhrqfeUQfPwxbLc1WIgGMIxpPInKYtDVIX3En%2FGpTNQLAeu%2FpuLKv%2BRmCx%2B7u28od5L%2F9%2BLmEhFWqJjs8Y6C1O2a3SKv"}],"group":"cf-nel","max_age":604800}' Nel: - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' Speculation-Rules: @@ -69,15 +69,15 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 921422292d0feacc-ORD + - 940b108f29129d03-ORD Alt-Svc: - h3=":443"; ma=86400 Server-Timing: - - cfL4;desc="?proto=TCP&rtt=30826&min_rtt=26727&rtt_var=12950&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=970&delivery_rate=108354&cwnd=219&unsent_bytes=0&cid=43c717161effdc57&ts=695&x=0" + - cfL4;desc="?proto=TCP&rtt=27793&min_rtt=26182&rtt_var=13041&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=970&delivery_rate=74111&cwnd=244&unsent_bytes=0&cid=9bcc030369a615fb&ts=210&x=0" body: encoding: ASCII-8BIT string: '{"ticker":"AAPL","currency":"USD","exchange":{"name":"Nasdaq/Ngs (Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United - States","country_code":"US","timezone":"America/New_York"},"prices":[{"date":"2024-08-01","open":224.37,"close":218.36,"high":224.48,"low":217.02,"volume":62501000}],"paging":{"prev":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-08-01","next":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-08-01","total_records":1,"current_page":1,"per_page":100,"total_pages":1},"meta":{"credits_used":1,"credits_remaining":249807}}' - recorded_at: Sun, 16 Mar 2025 12:08:00 GMT + States","country_code":"US","timezone":"America/New_York"},"prices":[{"date":"2024-08-01","open":224.37,"close":218.36,"high":224.48,"low":217.02,"volume":62501000}],"paging":{"prev":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-08-01","next":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-08-01","total_records":1,"current_page":1,"per_page":100,"total_pages":1},"meta":{"total_records":1,"credits_used":1,"credits_remaining":249738}}' + recorded_at: Fri, 16 May 2025 13:01:36 GMT recorded_with: VCR 6.3.1 diff --git a/test/vcr_cassettes/synth/security_prices.yml b/test/vcr_cassettes/synth/security_prices.yml index 1da82461..8f41f56a 100644 --- a/test/vcr_cassettes/synth/security_prices.yml +++ b/test/vcr_cassettes/synth/security_prices.yml @@ -14,7 +14,7 @@ http_interactions: X-Source-Type: - managed User-Agent: - - Faraday v2.12.2 + - Faraday v2.13.1 Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -25,7 +25,7 @@ http_interactions: message: OK headers: Date: - - Sun, 16 Mar 2025 12:02:51 GMT + - Fri, 16 May 2025 13:01:37 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -35,11 +35,11 @@ http_interactions: Cache-Control: - max-age=0, private, must-revalidate Etag: - - W/"eb6f73b7cb267ae753291839d20c72e4" + - W/"909e48e0b9ed1f892c1a1e1b4abd3082" Referrer-Policy: - strict-origin-when-cross-origin Rndr-Id: - - e0119cf5-873c-4315 + - 63af1418-59b9-4111 Strict-Transport-Security: - max-age=63072000; includeSubDomains Vary: @@ -53,15 +53,15 @@ http_interactions: X-Render-Origin-Server: - Render X-Request-Id: - - 590af10c-b7c1-47a1-9a9a-7f8a5f031734 + - 74da6a68-0bbd-48fb-b52a-0c5a750bd925 X-Runtime: - - '0.511130' + - '0.044404' X-Xss-Protection: - '0' Cf-Cache-Status: - DYNAMIC Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=BgSpKtsSHqPqVuO8FUQ0Zb4nT2VXJ9Q%2F3QrLGiZyq1%2FvGm4KnPL2jbgehp8fTKMHqK64Dm4aoEfwI6iK22Gz%2BG9Kq8wpHPGugon0YRBz1tiYLnq5QVoyvNJi6HV%2B6IBWQ5jCK5wA"}],"group":"cf-nel","max_age":604800}' + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=nwA0tsfR9it%2B9%2BtfHGjyzyfiSqPdNGxQqOLNF%2BIqlTeT1wJT6gLDCtbd1WFpOc1f8UXm2Zjn%2FJDOf7jOKWmGN6SKUBBjZvFLlBq%2FWyC7DN55NJwwyO77vD%2F5nf%2FaqduWCdPx7n7m"}],"group":"cf-nel","max_age":604800}' Nel: - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' Speculation-Rules: @@ -69,17 +69,17 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 92141a99ed232306-ORD + - 940b10932ec92305-ORD Alt-Svc: - h3=":443"; ma=86400 Server-Timing: - - cfL4;desc="?proto=TCP&rtt=26790&min_rtt=26357&rtt_var=10751&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2828&recv_bytes=970&delivery_rate=97096&cwnd=126&unsent_bytes=0&cid=5ac523d87c018022&ts=666&x=0" + - cfL4;desc="?proto=TCP&rtt=27451&min_rtt=26715&rtt_var=11491&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2825&recv_bytes=970&delivery_rate=88818&cwnd=249&unsent_bytes=0&cid=63105dfc059c15ef&ts=344&x=0" body: encoding: ASCII-8BIT string: '{"ticker":"AAPL","currency":"USD","exchange":{"name":"Nasdaq/Ngs (Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United - States","country_code":"US","timezone":"America/New_York"},"prices":[{"date":"2024-01-02","open":187.15,"close":185.64,"high":188.44,"low":183.89,"volume":82488700},{"date":"2024-01-03","open":184.22,"close":184.25,"high":185.88,"low":183.43,"volume":58414500},{"date":"2024-01-04","open":182.15,"close":181.91,"high":183.09,"low":180.88,"volume":71983600},{"date":"2024-01-05","open":181.99,"close":181.18,"high":182.76,"low":180.17,"volume":62303300},{"date":"2024-01-08","open":182.09,"close":185.56,"high":185.6,"low":181.5,"volume":59144500},{"date":"2024-01-09","open":183.92,"close":185.14,"high":185.15,"low":182.73,"volume":42841800},{"date":"2024-01-10","open":184.35,"close":186.19,"high":186.4,"low":183.92,"volume":46792900},{"date":"2024-01-11","open":186.54,"close":185.59,"high":187.05,"low":183.62,"volume":49128400},{"date":"2024-01-12","open":186.06,"close":185.92,"high":186.74,"low":185.19,"volume":40444700},{"date":"2024-01-16","open":182.16,"close":183.63,"high":184.26,"low":180.93,"volume":65603000},{"date":"2024-01-17","open":181.27,"close":182.68,"high":182.93,"low":180.3,"volume":47317400},{"date":"2024-01-18","open":186.09,"close":188.63,"high":189.14,"low":185.83,"volume":78005800},{"date":"2024-01-19","open":189.33,"close":191.56,"high":191.95,"low":188.82,"volume":68741000},{"date":"2024-01-22","open":192.3,"close":193.89,"high":195.33,"low":192.26,"volume":60133900},{"date":"2024-01-23","open":195.02,"close":195.18,"high":195.75,"low":193.83,"volume":42355600},{"date":"2024-01-24","open":195.42,"close":194.5,"high":196.38,"low":194.34,"volume":53631300},{"date":"2024-01-25","open":195.22,"close":194.17,"high":196.27,"low":193.11,"volume":54822100},{"date":"2024-01-26","open":194.27,"close":192.42,"high":194.76,"low":191.94,"volume":44594000},{"date":"2024-01-29","open":192.01,"close":191.73,"high":192.2,"low":189.58,"volume":47145600},{"date":"2024-01-30","open":190.94,"close":188.04,"high":191.8,"low":187.47,"volume":55859400},{"date":"2024-01-31","open":187.04,"close":184.4,"high":187.1,"low":184.35,"volume":55467800},{"date":"2024-02-01","open":183.99,"close":186.86,"high":186.95,"low":183.82,"volume":64885400},{"date":"2024-02-02","open":179.86,"close":185.85,"high":187.33,"low":179.25,"volume":102518000},{"date":"2024-02-05","open":188.15,"close":187.68,"high":189.25,"low":185.84,"volume":69668800},{"date":"2024-02-06","open":186.86,"close":189.3,"high":189.31,"low":186.77,"volume":43490800},{"date":"2024-02-07","open":190.64,"close":189.41,"high":191.05,"low":188.61,"volume":53439000},{"date":"2024-02-08","open":189.39,"close":188.32,"high":189.54,"low":187.35,"volume":40962000},{"date":"2024-02-09","open":188.65,"close":188.85,"high":189.99,"low":188.0,"volume":45155200},{"date":"2024-02-12","open":188.42,"close":187.15,"high":188.67,"low":186.79,"volume":41781900},{"date":"2024-02-13","open":185.77,"close":185.04,"high":186.21,"low":183.51,"volume":56529500},{"date":"2024-02-14","open":185.32,"close":184.15,"high":185.53,"low":182.44,"volume":54630500},{"date":"2024-02-15","open":183.55,"close":183.86,"high":184.49,"low":181.35,"volume":65434500},{"date":"2024-02-16","open":183.42,"close":182.31,"high":184.85,"low":181.67,"volume":49701400},{"date":"2024-02-20","open":181.79,"close":181.56,"high":182.43,"low":180.0,"volume":53665600},{"date":"2024-02-21","open":181.94,"close":182.32,"high":182.89,"low":180.66,"volume":41529700},{"date":"2024-02-22","open":183.48,"close":184.37,"high":184.96,"low":182.46,"volume":52292200},{"date":"2024-02-23","open":185.01,"close":182.52,"high":185.04,"low":182.23,"volume":45119700},{"date":"2024-02-26","open":182.24,"close":181.16,"high":182.76,"low":180.65,"volume":40867400},{"date":"2024-02-27","open":181.1,"close":182.63,"high":183.92,"low":179.56,"volume":54318900},{"date":"2024-02-28","open":182.51,"close":181.42,"high":183.12,"low":180.13,"volume":48953900},{"date":"2024-02-29","open":181.27,"close":180.75,"high":182.57,"low":179.53,"volume":136682600},{"date":"2024-03-01","open":179.55,"close":179.66,"high":180.53,"low":177.38,"volume":73488000},{"date":"2024-03-04","open":176.15,"close":175.1,"high":176.9,"low":173.79,"volume":81510100},{"date":"2024-03-05","open":170.76,"close":170.12,"high":172.04,"low":169.62,"volume":95132400},{"date":"2024-03-06","open":171.06,"close":169.12,"high":171.24,"low":168.68,"volume":68587700},{"date":"2024-03-07","open":169.15,"close":169.0,"high":170.73,"low":168.49,"volume":71765100},{"date":"2024-03-08","open":169.0,"close":170.73,"high":173.7,"low":168.94,"volume":76114600},{"date":"2024-03-11","open":172.94,"close":172.75,"high":174.38,"low":172.05,"volume":60139500},{"date":"2024-03-12","open":173.15,"close":173.23,"high":174.03,"low":171.01,"volume":59825400},{"date":"2024-03-13","open":172.77,"close":171.13,"high":173.19,"low":170.76,"volume":52488700},{"date":"2024-03-14","open":172.91,"close":173.0,"high":174.31,"low":172.05,"volume":72913500},{"date":"2024-03-15","open":171.17,"close":172.62,"high":172.62,"low":170.29,"volume":121664700},{"date":"2024-03-18","open":175.57,"close":173.72,"high":177.71,"low":173.52,"volume":75604200},{"date":"2024-03-19","open":174.34,"close":176.08,"high":176.61,"low":173.03,"volume":55215200},{"date":"2024-03-20","open":175.72,"close":178.67,"high":178.67,"low":175.09,"volume":53423100},{"date":"2024-03-21","open":177.05,"close":171.37,"high":177.49,"low":170.84,"volume":106181300},{"date":"2024-03-22","open":171.76,"close":172.28,"high":173.05,"low":170.06,"volume":71106600},{"date":"2024-03-25","open":170.57,"close":170.85,"high":171.94,"low":169.45,"volume":54288300},{"date":"2024-03-26","open":170.0,"close":169.71,"high":171.42,"low":169.58,"volume":57388400},{"date":"2024-03-27","open":170.41,"close":173.31,"high":173.6,"low":170.11,"volume":60273300},{"date":"2024-03-28","open":171.75,"close":171.48,"high":172.23,"low":170.51,"volume":65672700},{"date":"2024-04-01","open":171.19,"close":170.03,"high":171.25,"low":169.48,"volume":46240500},{"date":"2024-04-02","open":169.08,"close":168.84,"high":169.34,"low":168.23,"volume":49329500},{"date":"2024-04-03","open":168.79,"close":169.65,"high":170.68,"low":168.58,"volume":47691700},{"date":"2024-04-04","open":170.29,"close":168.82,"high":171.92,"low":168.82,"volume":53704400},{"date":"2024-04-05","open":169.59,"close":169.58,"high":170.39,"low":168.95,"volume":42055200},{"date":"2024-04-08","open":169.03,"close":168.45,"high":169.2,"low":168.24,"volume":37425500},{"date":"2024-04-09","open":168.7,"close":169.67,"high":170.08,"low":168.35,"volume":42451200},{"date":"2024-04-10","open":168.8,"close":167.78,"high":169.09,"low":167.11,"volume":49709300},{"date":"2024-04-11","open":168.34,"close":175.04,"high":175.46,"low":168.16,"volume":91070300},{"date":"2024-04-12","open":174.26,"close":176.55,"high":178.36,"low":174.21,"volume":101593300},{"date":"2024-04-15","open":175.36,"close":172.69,"high":176.63,"low":172.5,"volume":73531800},{"date":"2024-04-16","open":171.75,"close":169.38,"high":173.76,"low":168.27,"volume":73711200},{"date":"2024-04-17","open":169.61,"close":168.0,"high":170.65,"low":168.0,"volume":50901200},{"date":"2024-04-18","open":168.03,"close":167.04,"high":168.64,"low":166.55,"volume":43122900},{"date":"2024-04-19","open":166.21,"close":165.0,"high":166.4,"low":164.08,"volume":67772100},{"date":"2024-04-22","open":165.52,"close":165.84,"high":167.26,"low":164.77,"volume":48116400},{"date":"2024-04-23","open":165.35,"close":166.9,"high":167.05,"low":164.92,"volume":49537800},{"date":"2024-04-24","open":166.54,"close":169.02,"high":169.3,"low":166.21,"volume":48251800},{"date":"2024-04-25","open":169.53,"close":169.89,"high":170.61,"low":168.15,"volume":50558300},{"date":"2024-04-26","open":169.88,"close":169.3,"high":171.34,"low":169.18,"volume":44838400},{"date":"2024-04-29","open":173.37,"close":173.5,"high":176.03,"low":173.1,"volume":68169400},{"date":"2024-04-30","open":173.33,"close":170.33,"high":174.99,"low":170.0,"volume":65934800},{"date":"2024-05-01","open":169.58,"close":169.3,"high":172.71,"low":169.11,"volume":50383100},{"date":"2024-05-02","open":172.51,"close":173.03,"high":173.42,"low":170.89,"volume":94214900},{"date":"2024-05-03","open":186.65,"close":183.38,"high":187.0,"low":182.66,"volume":163224100},{"date":"2024-05-06","open":182.35,"close":181.71,"high":184.2,"low":180.42,"volume":78569700},{"date":"2024-05-07","open":183.45,"close":182.4,"high":184.9,"low":181.32,"volume":77305800},{"date":"2024-05-08","open":182.85,"close":182.74,"high":183.07,"low":181.45,"volume":45057100},{"date":"2024-05-09","open":182.56,"close":184.57,"high":184.66,"low":182.11,"volume":48983000},{"date":"2024-05-10","open":184.9,"close":183.05,"high":185.09,"low":182.13,"volume":50759500},{"date":"2024-05-13","open":185.44,"close":186.28,"high":187.1,"low":184.62,"volume":72044800},{"date":"2024-05-14","open":187.51,"close":187.43,"high":188.3,"low":186.29,"volume":52393600},{"date":"2024-05-15","open":187.91,"close":189.72,"high":190.65,"low":187.37,"volume":70400000},{"date":"2024-05-16","open":190.47,"close":189.84,"high":191.1,"low":189.66,"volume":52845200},{"date":"2024-05-17","open":189.51,"close":189.87,"high":190.81,"low":189.18,"volume":41282900},{"date":"2024-05-20","open":189.33,"close":191.04,"high":191.92,"low":189.01,"volume":44361300},{"date":"2024-05-21","open":191.09,"close":192.35,"high":192.73,"low":190.92,"volume":42309400},{"date":"2024-05-22","open":192.27,"close":190.9,"high":192.82,"low":190.27,"volume":34648500},{"date":"2024-05-23","open":190.98,"close":186.88,"high":191.0,"low":186.63,"volume":51005900}],"paging":{"prev":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-01-01","next":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=2\u0026start_date=2024-01-01","total_records":147,"current_page":1,"per_page":100,"total_pages":2},"meta":{"credits_used":1,"credits_remaining":249810}}' - recorded_at: Sun, 16 Mar 2025 12:02:51 GMT + States","country_code":"US","timezone":"America/New_York"},"prices":[{"date":"2024-01-02","open":187.15,"close":185.64,"high":188.44,"low":183.89,"volume":82488700},{"date":"2024-01-03","open":184.22,"close":184.25,"high":185.88,"low":183.43,"volume":58414500},{"date":"2024-01-04","open":182.15,"close":181.91,"high":183.09,"low":180.88,"volume":71983600},{"date":"2024-01-05","open":181.99,"close":181.18,"high":182.76,"low":180.17,"volume":62303300},{"date":"2024-01-08","open":182.09,"close":185.56,"high":185.6,"low":181.5,"volume":59144500},{"date":"2024-01-09","open":183.92,"close":185.14,"high":185.15,"low":182.73,"volume":42841800},{"date":"2024-01-10","open":184.35,"close":186.19,"high":186.4,"low":183.92,"volume":46792900},{"date":"2024-01-11","open":186.54,"close":185.59,"high":187.05,"low":183.62,"volume":49128400},{"date":"2024-01-12","open":186.06,"close":185.92,"high":186.74,"low":185.19,"volume":40444700},{"date":"2024-01-16","open":182.16,"close":183.63,"high":184.26,"low":180.93,"volume":65603000},{"date":"2024-01-17","open":181.27,"close":182.68,"high":182.93,"low":180.3,"volume":47317400},{"date":"2024-01-18","open":186.09,"close":188.63,"high":189.14,"low":185.83,"volume":78005800},{"date":"2024-01-19","open":189.33,"close":191.56,"high":191.95,"low":188.82,"volume":68741000},{"date":"2024-01-22","open":192.3,"close":193.89,"high":195.33,"low":192.26,"volume":60133900},{"date":"2024-01-23","open":195.02,"close":195.18,"high":195.75,"low":193.83,"volume":42355600},{"date":"2024-01-24","open":195.42,"close":194.5,"high":196.38,"low":194.34,"volume":53631300},{"date":"2024-01-25","open":195.22,"close":194.17,"high":196.27,"low":193.11,"volume":54822100},{"date":"2024-01-26","open":194.27,"close":192.42,"high":194.76,"low":191.94,"volume":44594000},{"date":"2024-01-29","open":192.01,"close":191.73,"high":192.2,"low":189.58,"volume":47145600},{"date":"2024-01-30","open":190.94,"close":188.04,"high":191.8,"low":187.47,"volume":55859400},{"date":"2024-01-31","open":187.04,"close":184.4,"high":187.1,"low":184.35,"volume":55467800},{"date":"2024-02-01","open":183.99,"close":186.86,"high":186.95,"low":183.82,"volume":64885400},{"date":"2024-02-02","open":179.86,"close":185.85,"high":187.33,"low":179.25,"volume":102518000},{"date":"2024-02-05","open":188.15,"close":187.68,"high":189.25,"low":185.84,"volume":69668800},{"date":"2024-02-06","open":186.86,"close":189.3,"high":189.31,"low":186.77,"volume":43490800},{"date":"2024-02-07","open":190.64,"close":189.41,"high":191.05,"low":188.61,"volume":53439000},{"date":"2024-02-08","open":189.39,"close":188.32,"high":189.54,"low":187.35,"volume":40962000},{"date":"2024-02-09","open":188.65,"close":188.85,"high":189.99,"low":188.0,"volume":45155200},{"date":"2024-02-12","open":188.42,"close":187.15,"high":188.67,"low":186.79,"volume":41781900},{"date":"2024-02-13","open":185.77,"close":185.04,"high":186.21,"low":183.51,"volume":56529500},{"date":"2024-02-14","open":185.32,"close":184.15,"high":185.53,"low":182.44,"volume":54630500},{"date":"2024-02-15","open":183.55,"close":183.86,"high":184.49,"low":181.35,"volume":65434500},{"date":"2024-02-16","open":183.42,"close":182.31,"high":184.85,"low":181.67,"volume":49701400},{"date":"2024-02-20","open":181.79,"close":181.56,"high":182.43,"low":180.0,"volume":53665600},{"date":"2024-02-21","open":181.94,"close":182.32,"high":182.89,"low":180.66,"volume":41529700},{"date":"2024-02-22","open":183.48,"close":184.37,"high":184.96,"low":182.46,"volume":52292200},{"date":"2024-02-23","open":185.01,"close":182.52,"high":185.04,"low":182.23,"volume":45119700},{"date":"2024-02-26","open":182.24,"close":181.16,"high":182.76,"low":180.65,"volume":40867400},{"date":"2024-02-27","open":181.1,"close":182.63,"high":183.92,"low":179.56,"volume":54318900},{"date":"2024-02-28","open":182.51,"close":181.42,"high":183.12,"low":180.13,"volume":48953900},{"date":"2024-02-29","open":181.27,"close":180.75,"high":182.57,"low":179.53,"volume":136682600},{"date":"2024-03-01","open":179.55,"close":179.66,"high":180.53,"low":177.38,"volume":73488000},{"date":"2024-03-04","open":176.15,"close":175.1,"high":176.9,"low":173.79,"volume":81510100},{"date":"2024-03-05","open":170.76,"close":170.12,"high":172.04,"low":169.62,"volume":95132400},{"date":"2024-03-06","open":171.06,"close":169.12,"high":171.24,"low":168.68,"volume":68587700},{"date":"2024-03-07","open":169.15,"close":169.0,"high":170.73,"low":168.49,"volume":71765100},{"date":"2024-03-08","open":169.0,"close":170.73,"high":173.7,"low":168.94,"volume":76114600},{"date":"2024-03-11","open":172.94,"close":172.75,"high":174.38,"low":172.05,"volume":60139500},{"date":"2024-03-12","open":173.15,"close":173.23,"high":174.03,"low":171.01,"volume":59825400},{"date":"2024-03-13","open":172.77,"close":171.13,"high":173.19,"low":170.76,"volume":52488700},{"date":"2024-03-14","open":172.91,"close":173.0,"high":174.31,"low":172.05,"volume":72913500},{"date":"2024-03-15","open":171.17,"close":172.62,"high":172.62,"low":170.29,"volume":121664700},{"date":"2024-03-18","open":175.57,"close":173.72,"high":177.71,"low":173.52,"volume":75604200},{"date":"2024-03-19","open":174.34,"close":176.08,"high":176.61,"low":173.03,"volume":55215200},{"date":"2024-03-20","open":175.72,"close":178.67,"high":178.67,"low":175.09,"volume":53423100},{"date":"2024-03-21","open":177.05,"close":171.37,"high":177.49,"low":170.84,"volume":106181300},{"date":"2024-03-22","open":171.76,"close":172.28,"high":173.05,"low":170.06,"volume":71106600},{"date":"2024-03-25","open":170.57,"close":170.85,"high":171.94,"low":169.45,"volume":54288300},{"date":"2024-03-26","open":170.0,"close":169.71,"high":171.42,"low":169.58,"volume":57388400},{"date":"2024-03-27","open":170.41,"close":173.31,"high":173.6,"low":170.11,"volume":60273300},{"date":"2024-03-28","open":171.75,"close":171.48,"high":172.23,"low":170.51,"volume":65672700},{"date":"2024-04-01","open":171.19,"close":170.03,"high":171.25,"low":169.48,"volume":46240500},{"date":"2024-04-02","open":169.08,"close":168.84,"high":169.34,"low":168.23,"volume":49329500},{"date":"2024-04-03","open":168.79,"close":169.65,"high":170.68,"low":168.58,"volume":47691700},{"date":"2024-04-04","open":170.29,"close":168.82,"high":171.92,"low":168.82,"volume":53704400},{"date":"2024-04-05","open":169.59,"close":169.58,"high":170.39,"low":168.95,"volume":42055200},{"date":"2024-04-08","open":169.03,"close":168.45,"high":169.2,"low":168.24,"volume":37425500},{"date":"2024-04-09","open":168.7,"close":169.67,"high":170.08,"low":168.35,"volume":42451200},{"date":"2024-04-10","open":168.8,"close":167.78,"high":169.09,"low":167.11,"volume":49709300},{"date":"2024-04-11","open":168.34,"close":175.04,"high":175.46,"low":168.16,"volume":91070300},{"date":"2024-04-12","open":174.26,"close":176.55,"high":178.36,"low":174.21,"volume":101593300},{"date":"2024-04-15","open":175.36,"close":172.69,"high":176.63,"low":172.5,"volume":73531800},{"date":"2024-04-16","open":171.75,"close":169.38,"high":173.76,"low":168.27,"volume":73711200},{"date":"2024-04-17","open":169.61,"close":168.0,"high":170.65,"low":168.0,"volume":50901200},{"date":"2024-04-18","open":168.03,"close":167.04,"high":168.64,"low":166.55,"volume":43122900},{"date":"2024-04-19","open":166.21,"close":165.0,"high":166.4,"low":164.08,"volume":67772100},{"date":"2024-04-22","open":165.52,"close":165.84,"high":167.26,"low":164.77,"volume":48116400},{"date":"2024-04-23","open":165.35,"close":166.9,"high":167.05,"low":164.92,"volume":49537800},{"date":"2024-04-24","open":166.54,"close":169.02,"high":169.3,"low":166.21,"volume":48251800},{"date":"2024-04-25","open":169.53,"close":169.89,"high":170.61,"low":168.15,"volume":50558300},{"date":"2024-04-26","open":169.88,"close":169.3,"high":171.34,"low":169.18,"volume":44838400},{"date":"2024-04-29","open":173.37,"close":173.5,"high":176.03,"low":173.1,"volume":68169400},{"date":"2024-04-30","open":173.33,"close":170.33,"high":174.99,"low":170.0,"volume":65934800},{"date":"2024-05-01","open":169.58,"close":169.3,"high":172.71,"low":169.11,"volume":50383100},{"date":"2024-05-02","open":172.51,"close":173.03,"high":173.42,"low":170.89,"volume":94214900},{"date":"2024-05-03","open":186.65,"close":183.38,"high":187.0,"low":182.66,"volume":163224100},{"date":"2024-05-06","open":182.35,"close":181.71,"high":184.2,"low":180.42,"volume":78569700},{"date":"2024-05-07","open":183.45,"close":182.4,"high":184.9,"low":181.32,"volume":77305800},{"date":"2024-05-08","open":182.85,"close":182.74,"high":183.07,"low":181.45,"volume":45057100},{"date":"2024-05-09","open":182.56,"close":184.57,"high":184.66,"low":182.11,"volume":48983000},{"date":"2024-05-10","open":184.9,"close":183.05,"high":185.09,"low":182.13,"volume":50759500},{"date":"2024-05-13","open":185.44,"close":186.28,"high":187.1,"low":184.62,"volume":72044800},{"date":"2024-05-14","open":187.51,"close":187.43,"high":188.3,"low":186.29,"volume":52393600},{"date":"2024-05-15","open":187.91,"close":189.72,"high":190.65,"low":187.37,"volume":70400000},{"date":"2024-05-16","open":190.47,"close":189.84,"high":191.1,"low":189.66,"volume":52845200},{"date":"2024-05-17","open":189.51,"close":189.87,"high":190.81,"low":189.18,"volume":41282900},{"date":"2024-05-20","open":189.33,"close":191.04,"high":191.92,"low":189.01,"volume":44361300},{"date":"2024-05-21","open":191.09,"close":192.35,"high":192.73,"low":190.92,"volume":42309400},{"date":"2024-05-22","open":192.27,"close":190.9,"high":192.82,"low":190.27,"volume":34648500},{"date":"2024-05-23","open":190.98,"close":186.88,"high":191.0,"low":186.63,"volume":51005900}],"paging":{"prev":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-01-01","next":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=2\u0026start_date=2024-01-01","total_records":147,"current_page":1,"per_page":100,"total_pages":2},"meta":{"total_records":147,"credits_used":1,"credits_remaining":249736}}' + recorded_at: Fri, 16 May 2025 13:01:37 GMT - request: method: get uri: https://api.synthfinance.com/tickers/AAPL/open-close?end_date=2024-08-01&operating_mic_code=XNAS&page=2&start_date=2024-01-01 @@ -94,7 +94,7 @@ http_interactions: X-Source-Type: - managed User-Agent: - - Faraday v2.12.2 + - Faraday v2.13.1 Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -105,7 +105,7 @@ http_interactions: message: OK headers: Date: - - Sun, 16 Mar 2025 12:02:52 GMT + - Fri, 16 May 2025 13:01:37 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -115,11 +115,11 @@ http_interactions: Cache-Control: - max-age=0, private, must-revalidate Etag: - - W/"78f6663a1523295a82d0ded13df426e4" + - W/"bbc82ef9591694561dd9992a9c06d491" Referrer-Policy: - strict-origin-when-cross-origin Rndr-Id: - - b0cd3704-937c-4017 + - 63ebee52-f1b2-4e81 Strict-Transport-Security: - max-age=63072000; includeSubDomains Vary: @@ -133,15 +133,15 @@ http_interactions: X-Render-Origin-Server: - Render X-Request-Id: - - 59a55ec3-49af-4fa1-a104-77480fa6914e + - dd95cb59-aead-4d1e-b1a2-881696e742fb X-Runtime: - - '0.583469' + - '0.031482' X-Xss-Protection: - '0' Cf-Cache-Status: - DYNAMIC Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ze9Rfqww2cEeTSTiP5axby5TPvYyBZDoEHRZKniMMybJrqYiBI1oGlCViODsaOXisw23njq1YaO%2Fhc0yGlPaqYdTcMXc6bQbVnWANjASqMS%2BQoVmPBFPr3nvSqeU99huB4BKWGlY"}],"group":"cf-nel","max_age":604800}' + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ebrOdAId1yoCYepT0CPKImNIA%2BOe8V3W3BHYheEOkVQFLsffFpfl%2B%2BYXfEHL21wczvW5dkZSd3OrF%2FklB%2FwGGDahXpveXzf497azY1Ho4YJrtDJeghxyZV6J%2BALPYwwpGrfUpv%2F1"}],"group":"cf-nel","max_age":604800}' Nel: - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' Speculation-Rules: @@ -149,15 +149,15 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 92141a9edbeee15f-ORD + - 940b1095ccd41156-ORD Alt-Svc: - h3=":443"; ma=86400 Server-Timing: - - cfL4;desc="?proto=TCP&rtt=32376&min_rtt=31508&rtt_var=12435&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2828&recv_bytes=970&delivery_rate=91913&cwnd=171&unsent_bytes=0&cid=d782914cf2ed620a&ts=758&x=0" + - cfL4;desc="?proto=TCP&rtt=26344&min_rtt=26162&rtt_var=10175&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2825&recv_bytes=970&delivery_rate=104847&cwnd=237&unsent_bytes=0&cid=9603fe0eb1df39aa&ts=212&x=0" body: encoding: ASCII-8BIT string: '{"ticker":"AAPL","currency":"USD","exchange":{"name":"Nasdaq/Ngs (Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United - States","country_code":"US","timezone":"America/New_York"},"prices":[{"date":"2024-05-24","open":188.82,"close":189.98,"high":190.58,"low":188.04,"volume":36294600},{"date":"2024-05-28","open":191.51,"close":189.99,"high":193.0,"low":189.1,"volume":52280100},{"date":"2024-05-29","open":189.61,"close":190.29,"high":192.25,"low":189.51,"volume":53068000},{"date":"2024-05-30","open":190.76,"close":191.29,"high":192.18,"low":190.63,"volume":49947900},{"date":"2024-05-31","open":191.44,"close":192.25,"high":192.57,"low":189.91,"volume":75158300},{"date":"2024-06-03","open":192.9,"close":194.03,"high":194.99,"low":192.52,"volume":50080500},{"date":"2024-06-04","open":194.64,"close":194.35,"high":195.32,"low":193.03,"volume":47471400},{"date":"2024-06-05","open":195.4,"close":195.87,"high":196.9,"low":194.87,"volume":54156800},{"date":"2024-06-06","open":195.69,"close":194.48,"high":196.5,"low":194.17,"volume":41181800},{"date":"2024-06-07","open":194.65,"close":196.89,"high":196.94,"low":194.14,"volume":53103900},{"date":"2024-06-10","open":196.9,"close":193.12,"high":197.3,"low":192.15,"volume":97262100},{"date":"2024-06-11","open":193.65,"close":207.15,"high":207.16,"low":193.63,"volume":172373300},{"date":"2024-06-12","open":207.37,"close":213.07,"high":220.2,"low":206.9,"volume":198134300},{"date":"2024-06-13","open":214.74,"close":214.24,"high":216.75,"low":211.6,"volume":97862700},{"date":"2024-06-14","open":213.85,"close":212.49,"high":215.17,"low":211.3,"volume":70122700},{"date":"2024-06-17","open":213.37,"close":216.67,"high":218.95,"low":212.72,"volume":93728300},{"date":"2024-06-18","open":217.59,"close":214.29,"high":218.63,"low":213.0,"volume":79943300},{"date":"2024-06-20","open":213.93,"close":209.68,"high":214.24,"low":208.85,"volume":86172500},{"date":"2024-06-21","open":210.39,"close":207.49,"high":211.89,"low":207.11,"volume":246421400},{"date":"2024-06-24","open":207.72,"close":208.14,"high":212.7,"low":206.59,"volume":80727000},{"date":"2024-06-25","open":209.15,"close":209.07,"high":211.38,"low":208.61,"volume":56713900},{"date":"2024-06-26","open":211.5,"close":213.25,"high":214.86,"low":210.64,"volume":66213200},{"date":"2024-06-27","open":214.69,"close":214.1,"high":215.74,"low":212.35,"volume":49772700},{"date":"2024-06-28","open":215.77,"close":210.62,"high":216.07,"low":210.3,"volume":82542700},{"date":"2024-07-01","open":212.09,"close":216.75,"high":217.51,"low":211.92,"volume":60402900},{"date":"2024-07-02","open":216.15,"close":220.27,"high":220.38,"low":215.1,"volume":58046200},{"date":"2024-07-03","open":220.0,"close":221.55,"high":221.55,"low":219.03,"volume":37369800},{"date":"2024-07-05","open":221.65,"close":226.34,"high":226.45,"low":221.65,"volume":60412400},{"date":"2024-07-08","open":227.09,"close":227.82,"high":227.85,"low":223.25,"volume":59085900},{"date":"2024-07-09","open":227.93,"close":228.68,"high":229.4,"low":226.37,"volume":48076100},{"date":"2024-07-10","open":229.3,"close":232.98,"high":233.08,"low":229.25,"volume":62627700},{"date":"2024-07-11","open":231.39,"close":227.57,"high":232.39,"low":225.77,"volume":64710600},{"date":"2024-07-12","open":228.92,"close":230.54,"high":232.64,"low":228.68,"volume":53046500},{"date":"2024-07-15","open":236.48,"close":234.4,"high":237.23,"low":233.09,"volume":62631300},{"date":"2024-07-16","open":235.0,"close":234.82,"high":236.27,"low":232.33,"volume":43234300},{"date":"2024-07-17","open":229.45,"close":228.88,"high":231.46,"low":226.64,"volume":57345900},{"date":"2024-07-18","open":230.28,"close":224.18,"high":230.44,"low":222.27,"volume":66034600},{"date":"2024-07-19","open":224.82,"close":224.31,"high":226.8,"low":223.28,"volume":49151500},{"date":"2024-07-22","open":227.01,"close":223.96,"high":227.78,"low":223.09,"volume":48201800},{"date":"2024-07-23","open":224.37,"close":225.01,"high":226.94,"low":222.68,"volume":39960300},{"date":"2024-07-24","open":224.0,"close":218.54,"high":224.8,"low":217.13,"volume":61777600},{"date":"2024-07-25","open":218.93,"close":217.49,"high":220.85,"low":214.62,"volume":51391200},{"date":"2024-07-26","open":218.7,"close":217.96,"high":219.49,"low":216.01,"volume":41601300},{"date":"2024-07-29","open":216.96,"close":218.24,"high":219.3,"low":215.75,"volume":36311800},{"date":"2024-07-30","open":219.19,"close":218.8,"high":220.33,"low":216.12,"volume":41643800},{"date":"2024-07-31","open":221.44,"close":222.08,"high":223.82,"low":220.63,"volume":50036300},{"date":"2024-08-01","open":224.37,"close":218.36,"high":224.48,"low":217.02,"volume":62501000}],"paging":{"prev":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=1\u0026start_date=2024-01-01","next":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-01-01","total_records":147,"current_page":2,"per_page":100,"total_pages":2},"meta":{"credits_used":1,"credits_remaining":249809}}' - recorded_at: Sun, 16 Mar 2025 12:02:52 GMT + States","country_code":"US","timezone":"America/New_York"},"prices":[{"date":"2024-05-24","open":188.82,"close":189.98,"high":190.58,"low":188.04,"volume":36294600},{"date":"2024-05-28","open":191.51,"close":189.99,"high":193.0,"low":189.1,"volume":52280100},{"date":"2024-05-29","open":189.61,"close":190.29,"high":192.25,"low":189.51,"volume":53068000},{"date":"2024-05-30","open":190.76,"close":191.29,"high":192.18,"low":190.63,"volume":49947900},{"date":"2024-05-31","open":191.44,"close":192.25,"high":192.57,"low":189.91,"volume":75158300},{"date":"2024-06-03","open":192.9,"close":194.03,"high":194.99,"low":192.52,"volume":50080500},{"date":"2024-06-04","open":194.64,"close":194.35,"high":195.32,"low":193.03,"volume":47471400},{"date":"2024-06-05","open":195.4,"close":195.87,"high":196.9,"low":194.87,"volume":54156800},{"date":"2024-06-06","open":195.69,"close":194.48,"high":196.5,"low":194.17,"volume":41181800},{"date":"2024-06-07","open":194.65,"close":196.89,"high":196.94,"low":194.14,"volume":53103900},{"date":"2024-06-10","open":196.9,"close":193.12,"high":197.3,"low":192.15,"volume":97262100},{"date":"2024-06-11","open":193.65,"close":207.15,"high":207.16,"low":193.63,"volume":172373300},{"date":"2024-06-12","open":207.37,"close":213.07,"high":220.2,"low":206.9,"volume":198134300},{"date":"2024-06-13","open":214.74,"close":214.24,"high":216.75,"low":211.6,"volume":97862700},{"date":"2024-06-14","open":213.85,"close":212.49,"high":215.17,"low":211.3,"volume":70122700},{"date":"2024-06-17","open":213.37,"close":216.67,"high":218.95,"low":212.72,"volume":93728300},{"date":"2024-06-18","open":217.59,"close":214.29,"high":218.63,"low":213.0,"volume":79943300},{"date":"2024-06-20","open":213.93,"close":209.68,"high":214.24,"low":208.85,"volume":86172500},{"date":"2024-06-21","open":210.39,"close":207.49,"high":211.89,"low":207.11,"volume":246421400},{"date":"2024-06-24","open":207.72,"close":208.14,"high":212.7,"low":206.59,"volume":80727000},{"date":"2024-06-25","open":209.15,"close":209.07,"high":211.38,"low":208.61,"volume":56713900},{"date":"2024-06-26","open":211.5,"close":213.25,"high":214.86,"low":210.64,"volume":66213200},{"date":"2024-06-27","open":214.69,"close":214.1,"high":215.74,"low":212.35,"volume":49772700},{"date":"2024-06-28","open":215.77,"close":210.62,"high":216.07,"low":210.3,"volume":82542700},{"date":"2024-07-01","open":212.09,"close":216.75,"high":217.51,"low":211.92,"volume":60402900},{"date":"2024-07-02","open":216.15,"close":220.27,"high":220.38,"low":215.1,"volume":58046200},{"date":"2024-07-03","open":220.0,"close":221.55,"high":221.55,"low":219.03,"volume":37369800},{"date":"2024-07-05","open":221.65,"close":226.34,"high":226.45,"low":221.65,"volume":60412400},{"date":"2024-07-08","open":227.09,"close":227.82,"high":227.85,"low":223.25,"volume":59085900},{"date":"2024-07-09","open":227.93,"close":228.68,"high":229.4,"low":226.37,"volume":48076100},{"date":"2024-07-10","open":229.3,"close":232.98,"high":233.08,"low":229.25,"volume":62627700},{"date":"2024-07-11","open":231.39,"close":227.57,"high":232.39,"low":225.77,"volume":64710600},{"date":"2024-07-12","open":228.92,"close":230.54,"high":232.64,"low":228.68,"volume":53046500},{"date":"2024-07-15","open":236.48,"close":234.4,"high":237.23,"low":233.09,"volume":62631300},{"date":"2024-07-16","open":235.0,"close":234.82,"high":236.27,"low":232.33,"volume":43234300},{"date":"2024-07-17","open":229.45,"close":228.88,"high":231.46,"low":226.64,"volume":57345900},{"date":"2024-07-18","open":230.28,"close":224.18,"high":230.44,"low":222.27,"volume":66034600},{"date":"2024-07-19","open":224.82,"close":224.31,"high":226.8,"low":223.28,"volume":49151500},{"date":"2024-07-22","open":227.01,"close":223.96,"high":227.78,"low":223.09,"volume":48201800},{"date":"2024-07-23","open":224.37,"close":225.01,"high":226.94,"low":222.68,"volume":39960300},{"date":"2024-07-24","open":224.0,"close":218.54,"high":224.8,"low":217.13,"volume":61777600},{"date":"2024-07-25","open":218.93,"close":217.49,"high":220.85,"low":214.62,"volume":51391200},{"date":"2024-07-26","open":218.7,"close":217.96,"high":219.49,"low":216.01,"volume":41601300},{"date":"2024-07-29","open":216.96,"close":218.24,"high":219.3,"low":215.75,"volume":36311800},{"date":"2024-07-30","open":219.19,"close":218.8,"high":220.33,"low":216.12,"volume":41643800},{"date":"2024-07-31","open":221.44,"close":222.08,"high":223.82,"low":220.63,"volume":50036300},{"date":"2024-08-01","open":224.37,"close":218.36,"high":224.48,"low":217.02,"volume":62501000}],"paging":{"prev":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=1\u0026start_date=2024-01-01","next":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-01-01","total_records":147,"current_page":2,"per_page":100,"total_pages":2},"meta":{"total_records":147,"credits_used":1,"credits_remaining":249735}}' + recorded_at: Fri, 16 May 2025 13:01:37 GMT recorded_with: VCR 6.3.1 diff --git a/test/vcr_cassettes/synth/security_search.yml b/test/vcr_cassettes/synth/security_search.yml index f9504804..c06c0f42 100644 --- a/test/vcr_cassettes/synth/security_search.yml +++ b/test/vcr_cassettes/synth/security_search.yml @@ -14,7 +14,7 @@ http_interactions: X-Source-Type: - managed User-Agent: - - Faraday v2.12.2 + - Faraday v2.13.1 Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -25,7 +25,7 @@ http_interactions: message: OK headers: Date: - - Sun, 16 Mar 2025 12:01:58 GMT + - Fri, 16 May 2025 13:01:38 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -39,7 +39,7 @@ http_interactions: Referrer-Policy: - strict-origin-when-cross-origin Rndr-Id: - - 2effb56b-f67f-402d + - 701ae22a-18c8-4e62 Strict-Transport-Security: - max-age=63072000; includeSubDomains Vary: @@ -53,15 +53,15 @@ http_interactions: X-Render-Origin-Server: - Render X-Request-Id: - - 33470619-5119-4923-b4e0-e9a0eeb532a1 + - edb55bc6-e3ea-470b-b7af-9b4d9883420b X-Runtime: - - '0.453770' + - '0.355152' X-Xss-Protection: - '0' Cf-Cache-Status: - DYNAMIC Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ayZOlXkCwLgUl%2FrB2%2BlqtqR5HCllubf4HLDipEt3klWKyHS4nilHi9XZ1fiEQWx7xwiRMJZ5EW0Xzm7ISoHWTtEbkgMQHWYQwSTeg30ahFFHK1pkOOnET1fuW1UxiZwlJtq1XZGB"}],"group":"cf-nel","max_age":604800}' + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=QGeBWdYED%2F%2FgT9BzborFAnM%2FG6UiNmI0ej212XGHWdFwYXUvTJ2GyqA9hMJrpYIvgbHdQ9Ed0MsQUv3KFb57VXQq0T6UXTNPa%2BFRPepK0hsXeGDLxch04v6KnkTATqcw2M8HuYHS"}],"group":"cf-nel","max_age":604800}' Nel: - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' Speculation-Rules: @@ -69,11 +69,11 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 921419514e0a6399-ORD + - 940b1097a830f856-ORD Alt-Svc: - h3=":443"; ma=86400 Server-Timing: - - cfL4;desc="?proto=TCP&rtt=25809&min_rtt=25801&rtt_var=9692&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2829&recv_bytes=939&delivery_rate=111952&cwnd=121&unsent_bytes=0&cid=2beb787f15cd8ab9&ts=610&x=0" + - cfL4;desc="?proto=TCP&rtt=26401&min_rtt=25556&rtt_var=11273&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2825&recv_bytes=939&delivery_rate=89615&cwnd=244&unsent_bytes=0&cid=cf6d0758d165295d&ts=500&x=0" body: encoding: ASCII-8BIT string: '{"data":[{"symbol":"AAPL","name":"Apple Inc.","logo_url":"https://logo.synthfinance.com/ticker/AAPL","currency":"USD","exchange":{"name":"Nasdaq/Ngs @@ -100,5 +100,5 @@ http_interactions: Inc.","logo_url":"https://logo.synthfinance.com/ticker/AAPJ","currency":"USD","exchange":{"name":"Otc Pink Marketplace","mic_code":"PINX","operating_mic_code":"OTCM","acronym":"","country":"United States","country_code":"US","timezone":"America/New_York"}}]}' - recorded_at: Sun, 16 Mar 2025 12:01:58 GMT + recorded_at: Fri, 16 May 2025 13:01:38 GMT recorded_with: VCR 6.3.1 diff --git a/test/vcr_cassettes/synth/transaction_enrich.yml b/test/vcr_cassettes/synth/transaction_enrich.yml deleted file mode 100644 index 08463ed7..00000000 --- a/test/vcr_cassettes/synth/transaction_enrich.yml +++ /dev/null @@ -1,82 +0,0 @@ ---- -http_interactions: -- request: - method: get - uri: https://api.synthfinance.com/enrich?amount=25.5&city=San%20Francisco&country=US&date=2025-03-16&description=UBER%20EATS&state=CA - body: - encoding: US-ASCII - string: '' - headers: - Authorization: - - Bearer - X-Source: - - maybe_app - X-Source-Type: - - managed - User-Agent: - - Faraday v2.12.2 - Accept-Encoding: - - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - Accept: - - "*/*" - response: - status: - code: 200 - message: OK - headers: - Date: - - Sun, 16 Mar 2025 12:09:33 GMT - Content-Type: - - application/json; charset=utf-8 - Transfer-Encoding: - - chunked - Connection: - - keep-alive - Cache-Control: - - max-age=0, private, must-revalidate - Etag: - - W/"00411c83cfeaade519bcc3e57d9e461e" - Referrer-Policy: - - strict-origin-when-cross-origin - Rndr-Id: - - 56a8791d-85ed-4342 - Strict-Transport-Security: - - max-age=63072000; includeSubDomains - Vary: - - Accept-Encoding - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Permitted-Cross-Domain-Policies: - - none - X-Render-Origin-Server: - - Render - X-Request-Id: - - 1b35b9c1-0092-40b1-8b70-2bce7c5796af - X-Runtime: - - '0.884634' - X-Xss-Protection: - - '0' - Cf-Cache-Status: - - DYNAMIC - Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=qUtB0aWbK%2Fh5W7cV%2FugsUGbWKtJzsf%2FXd5i8cm8KlepEtLyuVPH7XX0fqwzHp43OCWQkGr9r8hRBBSEcx9LWW5vS7%2B1kXCJaKPaTRn%2BWtsEymHg78OHqDcMahwSuy%2FkpSGLWo0or"}],"group":"cf-nel","max_age":604800}' - Nel: - - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' - Speculation-Rules: - - '"/cdn-cgi/speculation"' - Server: - - cloudflare - Cf-Ray: - - 921424681aa4acab-ORD - Alt-Svc: - - h3=":443"; ma=86400 - Server-Timing: - - cfL4;desc="?proto=TCP&rtt=26975&min_rtt=26633&rtt_var=10231&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2829&recv_bytes=969&delivery_rate=108737&cwnd=210&unsent_bytes=0&cid=318ff675628918e1&ts=1035&x=0" - body: - encoding: ASCII-8BIT - string: '{"merchant":"Uber Eats","merchant_id":"mer_aea41e7f29ce47b5873f3caf49d5972d","category":"Dining - Out","website":"ubereats.com","icon":"https://logo.synthfinance.com/ubereats.com","meta":{"credits_used":1,"credits_remaining":249806}}' - recorded_at: Sun, 16 Mar 2025 12:09:33 GMT -recorded_with: VCR 6.3.1 diff --git a/test/vcr_cassettes/synth/usage.yml b/test/vcr_cassettes/synth/usage.yml index 27e5300f..d60922db 100644 --- a/test/vcr_cassettes/synth/usage.yml +++ b/test/vcr_cassettes/synth/usage.yml @@ -14,7 +14,7 @@ http_interactions: X-Source-Type: - managed User-Agent: - - Faraday v2.12.2 + - Faraday v2.13.1 Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -25,7 +25,7 @@ http_interactions: message: OK headers: Date: - - Sat, 15 Mar 2025 22:18:47 GMT + - Fri, 16 May 2025 13:01:36 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -35,11 +35,11 @@ http_interactions: Cache-Control: - max-age=0, private, must-revalidate Etag: - - W/"4ec3e0a20895d90b1e1241ca67f10ca3" + - W/"7b8c2bf0cba54bc26b78bdc6e611dcbd" Referrer-Policy: - strict-origin-when-cross-origin Rndr-Id: - - 54c8ecf9-6858-4db6 + - 1b53adf6-b391-45b2 Strict-Transport-Security: - max-age=63072000; includeSubDomains Vary: @@ -53,15 +53,15 @@ http_interactions: X-Render-Origin-Server: - Render X-Request-Id: - - a4112cfb-0eac-4e3e-a880-7536d90dcba0 + - f88670a2-81d2-48b6-8d73-a911c846e330 X-Runtime: - - '0.007036' + - '0.018749' X-Xss-Protection: - '0' Cf-Cache-Status: - DYNAMIC Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=Rt0BTtrgXzYjWOQFgb%2Bg6N4xKvXtPI66Q251bq9nWtqUhGHo17GmVVAPkutwN7Gisw1RmvYfxYUiMCCxlc4%2BjuHxbU1%2BXr9KHy%2F5pUpLhgLNNrtkqqKOCW4GduODnDbw2I38Rocu"}],"group":"cf-nel","max_age":604800}' + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=oH4OsWB6itK0jpi%2FPs%2BswVyCZIbkJGPfyJaoR4TKFtTAfmnqa8Lp6aZhv22WKzotXJuAKbh99VdYdZIOkeIPWbYTc6j4rGw%2BkQB3Hw%2Fc44QxDBJFdIo6wJNe8TGiPAZ%2BvgoBVHWn"}],"group":"cf-nel","max_age":604800}' Nel: - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' Speculation-Rules: @@ -69,14 +69,14 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 920f637d1fe8eb68-ORD + - 940b108c38f66392-ORD Alt-Svc: - h3=":443"; ma=86400 Server-Timing: - - cfL4;desc="?proto=TCP&rtt=28779&min_rtt=27036&rtt_var=11384&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2828&recv_bytes=878&delivery_rate=107116&cwnd=203&unsent_bytes=0&cid=52bc39ad09dd9eff&ts=145&x=0" + - cfL4;desc="?proto=TCP&rtt=33369&min_rtt=25798&rtt_var=15082&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2826&recv_bytes=878&delivery_rate=112256&cwnd=205&unsent_bytes=0&cid=1b13324eb0768fd3&ts=285&x=0" body: encoding: ASCII-8BIT - string: '{"id":"user_3208c49393f54b3e974795e4bea5b864","email":"test@maybe.co","name":"Test - User","plan":"Business","api_calls_remaining":1200,"api_limit":5000,"credits_reset_at":"2025-04-01T00:00:00.000-04:00","current_period_start":"2025-03-01T00:00:00.000-05:00"}' - recorded_at: Sat, 15 Mar 2025 22:18:47 GMT + string: '{"id":"user_3208c49393f54b3e974795e4bea5b864","email":"zach@maybe.co","name":"Zach + Gollwitzer","plan":"Business","api_calls_remaining":249738,"api_limit":250000,"credits_reset_at":"2025-06-01T00:00:00.000-04:00","current_period_start":"2025-05-01T00:00:00.000-04:00"}' + recorded_at: Fri, 16 May 2025 13:01:36 GMT recorded_with: VCR 6.3.1 diff --git a/vendor/javascript/@floating-ui--utils--dom.js b/vendor/javascript/@floating-ui--utils--dom.js index 098e5ac3..82db906e 100644 --- a/vendor/javascript/@floating-ui--utils--dom.js +++ b/vendor/javascript/@floating-ui--utils--dom.js @@ -1,2 +1,4 @@ -function getNodeName(e){return isNode(e)?(e.nodeName||"").toLowerCase():"#document"}function getWindow(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function getDocumentElement(e){var t;return(t=(isNode(e)?e.ownerDocument:e.document)||window.document)==null?void 0:t.documentElement}function isNode(e){return e instanceof Node||e instanceof getWindow(e).Node}function isElement(e){return e instanceof Element||e instanceof getWindow(e).Element}function isHTMLElement(e){return e instanceof HTMLElement||e instanceof getWindow(e).HTMLElement}function isShadowRoot(e){return typeof ShadowRoot!=="undefined"&&(e instanceof ShadowRoot||e instanceof getWindow(e).ShadowRoot)}function isOverflowElement(e){const{overflow:t,overflowX:n,overflowY:o,display:r}=getComputedStyle(e);return/auto|scroll|overlay|hidden|clip/.test(t+o+n)&&!["inline","contents"].includes(r)}function isTableElement(e){return["table","td","th"].includes(getNodeName(e))}function isTopLayer(e){return[":popover-open",":modal"].some((t=>{try{return e.matches(t)}catch(e){return false}}))}function isContainingBlock(e){const t=isWebKit();const n=isElement(e)?getComputedStyle(e):e;return n.transform!=="none"||n.perspective!=="none"||!!n.containerType&&n.containerType!=="normal"||!t&&!!n.backdropFilter&&n.backdropFilter!=="none"||!t&&!!n.filter&&n.filter!=="none"||["transform","perspective","filter"].some((e=>(n.willChange||"").includes(e)))||["paint","layout","strict","content"].some((e=>(n.contain||"").includes(e)))}function getContainingBlock(e){let t=getParentNode(e);while(isHTMLElement(t)&&!isLastTraversableNode(t)){if(isContainingBlock(t))return t;if(isTopLayer(t))return null;t=getParentNode(t)}return null}function isWebKit(){return!(typeof CSS==="undefined"||!CSS.supports)&&CSS.supports("-webkit-backdrop-filter","none")}function isLastTraversableNode(e){return["html","body","#document"].includes(getNodeName(e))}function getComputedStyle(e){return getWindow(e).getComputedStyle(e)}function getNodeScroll(e){return isElement(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function getParentNode(e){if(getNodeName(e)==="html")return e;const t=e.assignedSlot||e.parentNode||isShadowRoot(e)&&e.host||getDocumentElement(e);return isShadowRoot(t)?t.host:t}function getNearestOverflowAncestor(e){const t=getParentNode(e);return isLastTraversableNode(t)?e.ownerDocument?e.ownerDocument.body:e.body:isHTMLElement(t)&&isOverflowElement(t)?t:getNearestOverflowAncestor(t)}function getOverflowAncestors(e,t,n){var o;t===void 0&&(t=[]);n===void 0&&(n=true);const r=getNearestOverflowAncestor(e);const i=r===((o=e.ownerDocument)==null?void 0:o.body);const l=getWindow(r);if(i){const e=getFrameElement(l);return t.concat(l,l.visualViewport||[],isOverflowElement(r)?r:[],e&&n?getOverflowAncestors(e):[])}return t.concat(r,getOverflowAncestors(r,[],n))}function getFrameElement(e){return Object.getPrototypeOf(e.parent)?e.frameElement:null}export{getComputedStyle,getContainingBlock,getDocumentElement,getFrameElement,getNearestOverflowAncestor,getNodeName,getNodeScroll,getOverflowAncestors,getParentNode,getWindow,isContainingBlock,isElement,isHTMLElement,isLastTraversableNode,isNode,isOverflowElement,isShadowRoot,isTableElement,isTopLayer,isWebKit}; +// @floating-ui/utils/dom@0.2.9 downloaded from https://ga.jspm.io/npm:@floating-ui/utils@0.2.9/dist/floating-ui.utils.dom.mjs + +function hasWindow(){return typeof window!=="undefined"}function getNodeName(e){return isNode(e)?(e.nodeName||"").toLowerCase():"#document"}function getWindow(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function getDocumentElement(e){var t;return(t=(isNode(e)?e.ownerDocument:e.document)||window.document)==null?void 0:t.documentElement}function isNode(e){return!!hasWindow()&&(e instanceof Node||e instanceof getWindow(e).Node)}function isElement(e){return!!hasWindow()&&(e instanceof Element||e instanceof getWindow(e).Element)}function isHTMLElement(e){return!!hasWindow()&&(e instanceof HTMLElement||e instanceof getWindow(e).HTMLElement)}function isShadowRoot(e){return!(!hasWindow()||typeof ShadowRoot==="undefined")&&(e instanceof ShadowRoot||e instanceof getWindow(e).ShadowRoot)}function isOverflowElement(e){const{overflow:t,overflowX:n,overflowY:o,display:r}=getComputedStyle(e);return/auto|scroll|overlay|hidden|clip/.test(t+o+n)&&!["inline","contents"].includes(r)}function isTableElement(e){return["table","td","th"].includes(getNodeName(e))}function isTopLayer(e){return[":popover-open",":modal"].some((t=>{try{return e.matches(t)}catch(e){return false}}))}function isContainingBlock(e){const t=isWebKit();const n=isElement(e)?getComputedStyle(e):e;return["transform","translate","scale","rotate","perspective"].some((e=>!!n[e]&&n[e]!=="none"))||!!n.containerType&&n.containerType!=="normal"||!t&&!!n.backdropFilter&&n.backdropFilter!=="none"||!t&&!!n.filter&&n.filter!=="none"||["transform","translate","scale","rotate","perspective","filter"].some((e=>(n.willChange||"").includes(e)))||["paint","layout","strict","content"].some((e=>(n.contain||"").includes(e)))}function getContainingBlock(e){let t=getParentNode(e);while(isHTMLElement(t)&&!isLastTraversableNode(t)){if(isContainingBlock(t))return t;if(isTopLayer(t))return null;t=getParentNode(t)}return null}function isWebKit(){return!(typeof CSS==="undefined"||!CSS.supports)&&CSS.supports("-webkit-backdrop-filter","none")}function isLastTraversableNode(e){return["html","body","#document"].includes(getNodeName(e))}function getComputedStyle(e){return getWindow(e).getComputedStyle(e)}function getNodeScroll(e){return isElement(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function getParentNode(e){if(getNodeName(e)==="html")return e;const t=e.assignedSlot||e.parentNode||isShadowRoot(e)&&e.host||getDocumentElement(e);return isShadowRoot(t)?t.host:t}function getNearestOverflowAncestor(e){const t=getParentNode(e);return isLastTraversableNode(t)?e.ownerDocument?e.ownerDocument.body:e.body:isHTMLElement(t)&&isOverflowElement(t)?t:getNearestOverflowAncestor(t)}function getOverflowAncestors(e,t,n){var o;t===void 0&&(t=[]);n===void 0&&(n=true);const r=getNearestOverflowAncestor(e);const i=r===((o=e.ownerDocument)==null?void 0:o.body);const l=getWindow(r);if(i){const e=getFrameElement(l);return t.concat(l,l.visualViewport||[],isOverflowElement(r)?r:[],e&&n?getOverflowAncestors(e):[])}return t.concat(r,getOverflowAncestors(r,[],n))}function getFrameElement(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}export{getComputedStyle,getContainingBlock,getDocumentElement,getFrameElement,getNearestOverflowAncestor,getNodeName,getNodeScroll,getOverflowAncestors,getParentNode,getWindow,isContainingBlock,isElement,isHTMLElement,isLastTraversableNode,isNode,isOverflowElement,isShadowRoot,isTableElement,isTopLayer,isWebKit}; diff --git a/vendor/javascript/d3-array.js b/vendor/javascript/d3-array.js index 4b35f1e6..4b53af52 100644 --- a/vendor/javascript/d3-array.js +++ b/vendor/javascript/d3-array.js @@ -1,2 +1,4 @@ +// d3-array@3.2.4 downloaded from https://ga.jspm.io/npm:d3-array@3.2.4/src/index.js + import{InternMap as t,InternSet as n}from"internmap";export{InternMap,InternSet}from"internmap";function ascending(t,n){return null==t||null==n?NaN:tn?1:t>=n?0:NaN}function descending(t,n){return null==t||null==n?NaN:nt?1:n>=t?0:NaN}function bisector(t){let n,e,r;if(2!==t.length){n=ascending;e=(n,e)=>ascending(t(n),e);r=(n,e)=>t(n)-e}else{n=t===ascending||t===descending?t:zero;e=t;r=t}function left(t,r,o=0,i=t.length){if(o>>1;e(t[n],r)<0?o=n+1:i=n}while(o>>1;e(t[n],r)<=0?o=n+1:i=n}while(oe&&r(t[i-1],n)>-r(t[i],n)?i-1:i}return{left:left,center:center,right:right}}function zero(){return 0}function number(t){return null===t?NaN:+t}function*numbers(t,n){if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(yield n);else{let e=-1;for(let r of t)null!=(r=n(r,++e,t))&&(r=+r)>=r&&(yield r)}}const e=bisector(ascending);const r=e.right;const o=e.left;const i=bisector(number).center;function blur(t,n){if(!((n=+n)>=0))throw new RangeError("invalid r");let e=t.length;if(!((e=Math.floor(e))>=0))throw new RangeError("invalid length");if(!e||!n)return t;const r=blurf(n);const o=t.slice();r(t,o,0,e,1);r(o,t,0,e,1);r(t,o,0,e,1);return t}const f=Blur2(blurf);const u=Blur2(blurfImage);function Blur2(t){return function(n,e,r=e){if(!((e=+e)>=0))throw new RangeError("invalid rx");if(!((r=+r)>=0))throw new RangeError("invalid ry");let{data:o,width:i,height:f}=n;if(!((i=Math.floor(i))>=0))throw new RangeError("invalid width");if(!((f=Math.floor(void 0!==f?f:o.length/i))>=0))throw new RangeError("invalid height");if(!i||!f||!e&&!r)return n;const u=e&&t(e);const l=r&&t(r);const c=o.slice();if(u&&l){blurh(u,c,o,i,f);blurh(u,o,c,i,f);blurh(u,c,o,i,f);blurv(l,o,c,i,f);blurv(l,c,o,i,f);blurv(l,o,c,i,f)}else if(u){blurh(u,o,c,i,f);blurh(u,c,o,i,f);blurh(u,o,c,i,f)}else if(l){blurv(l,o,c,i,f);blurv(l,c,o,i,f);blurv(l,o,c,i,f)}return n}}function blurh(t,n,e,r,o){for(let i=0,f=r*o;i{r<<=2,o<<=2,i<<=2;n(t,e,r+0,o+0,i);n(t,e,r+1,o+1,i);n(t,e,r+2,o+2,i);n(t,e,r+3,o+3,i)}}function blurf(t){const n=Math.floor(t);if(n===t)return bluri(t);const e=t-n;const r=2*t+1;return(t,o,i,f,u)=>{if(!((f-=u)>=i))return;let l=n*o[i];const c=u*n;const s=c+u;for(let t=i,n=i+c;t{if(!((i-=f)>=o))return;let u=t*r[o];const l=f*t;for(let t=o,n=o+l;t=n&&++e;else{let r=-1;for(let o of t)null!=(o=n(o,++r,t))&&(o=+o)>=o&&++e}return e}function length$1(t){return 0|t.length}function empty(t){return!(t>0)}function arrayify(t){return"object"!==typeof t||"length"in t?t:Array.from(t)}function reducer(t){return n=>t(...n)}function cross(...t){const n="function"===typeof t[t.length-1]&&reducer(t.pop());t=t.map(arrayify);const e=t.map(length$1);const r=t.length-1;const o=new Array(r+1).fill(0);const i=[];if(r<0||e.some(empty))return i;while(true){i.push(o.map(((n,e)=>t[e][n])));let f=r;while(++o[f]===e[f]){if(0===f)return n?i.map(n):i;o[f--]=0}}}function cumsum(t,n){var e=0,r=0;return Float64Array.from(t,void 0===n?t=>e+=+t||0:o=>e+=+n(o,r++,t)||0)}function variance(t,n){let e=0;let r;let o=0;let i=0;if(void 0===n){for(let n of t)if(null!=n&&(n=+n)>=n){r=n-o;o+=r/++e;i+=r*(n-o)}}else{let f=-1;for(let u of t)if(null!=(u=n(u,++f,t))&&(u=+u)>=u){r=u-o;o+=r/++e;i+=r*(u-o)}}if(e>1)return i/(e-1)}function deviation(t,n){const e=variance(t,n);return e?Math.sqrt(e):e}function extent(t,n){let e;let r;if(void 0===n){for(const n of t)if(null!=n)if(void 0===e)n>=n&&(e=r=n);else{e>n&&(e=n);r=i&&(e=r=i);else{e>i&&(e=i);r0){i=t[--o];while(o>0){n=i;e=t[--o];i=n+e;r=e-(i-n);if(r)break}if(o>0&&(r<0&&t[o-1]<0||r>0&&t[o-1]>0)){e=2*r;n=i+e;e==n-i&&(i=n)}}return i}}function fsum(t,n){const e=new Adder;if(void 0===n)for(let n of t)(n=+n)&&e.add(n);else{let r=-1;for(let o of t)(o=+n(o,++r,t))&&e.add(o)}return+e}function fcumsum(t,n){const e=new Adder;let r=-1;return Float64Array.from(t,void 0===n?t=>e.add(+t||0):o=>e.add(+n(o,++r,t)||0))}function identity(t){return t}function group(t,...n){return nest(t,identity,identity,n)}function groups(t,...n){return nest(t,Array.from,identity,n)}function flatten$1(t,n){for(let e=1,r=n.length;et.pop().map((([n,e])=>[...t,n,e]))));return t}function flatGroup(t,...n){return flatten$1(groups(t,...n),n)}function flatRollup(t,n,...e){return flatten$1(rollups(t,n,...e),e)}function rollup(t,n,...e){return nest(t,identity,n,e)}function rollups(t,n,...e){return nest(t,Array.from,n,e)}function index(t,...n){return nest(t,identity,unique,n)}function indexes(t,...n){return nest(t,Array.from,unique,n)}function unique(t){if(1!==t.length)throw new Error("duplicate key");return t[0]}function nest(n,e,r,o){return function regroup(n,i){if(i>=o.length)return r(n);const f=new t;const u=o[i++];let l=-1;for(const t of n){const e=u(t,++l,n);const r=f.get(e);r?r.push(t):f.set(e,[t])}for(const[t,n]of f)f.set(t,regroup(n,i));return e(f)}(n,0)}function permute(t,n){return Array.from(n,(n=>t[n]))}function sort(t,...n){if("function"!==typeof t[Symbol.iterator])throw new TypeError("values is not iterable");t=Array.from(t);let[e]=n;if(e&&2!==e.length||n.length>1){const r=Uint32Array.from(t,((t,n)=>n));if(n.length>1){n=n.map((n=>t.map(n)));r.sort(((t,e)=>{for(const r of n){const n=ascendingDefined(r[t],r[e]);if(n)return n}}))}else{e=t.map(e);r.sort(((t,n)=>ascendingDefined(e[t],e[n])))}return permute(t,r)}return t.sort(compareDefined(e))}function compareDefined(t=ascending){if(t===ascending)return ascendingDefined;if("function"!==typeof t)throw new TypeError("compare is not a function");return(n,e)=>{const r=t(n,e);return r||0===r?r:(0===t(e,e))-(0===t(n,n))}}function ascendingDefined(t,n){return(null==t||!(t>=t))-(null==n||!(n>=n))||(tn?1:0)}function groupSort(t,n,e){return(2!==n.length?sort(rollup(t,n,e),(([t,n],[e,r])=>ascending(n,r)||ascending(t,e))):sort(group(t,e),(([t,e],[r,o])=>n(e,o)||ascending(t,r)))).map((([t])=>t))}var l=Array.prototype;var c=l.slice;l.map;function constant(t){return()=>t}const s=Math.sqrt(50),a=Math.sqrt(10),h=Math.sqrt(2);function tickSpec(t,n,e){const r=(n-t)/Math.max(0,e),o=Math.floor(Math.log10(r)),i=r/Math.pow(10,o),f=i>=s?10:i>=a?5:i>=h?2:1;let u,l,c;if(o<0){c=Math.pow(10,-o)/f;u=Math.round(t*c);l=Math.round(n*c);u/cn&&--l;c=-c}else{c=Math.pow(10,o)*f;u=Math.round(t/c);l=Math.round(n/c);u*cn&&--l}return l0))return[];if(t===n)return[t];const r=n=o))return[];const u=i-o+1,l=new Array(u);if(r)if(f<0)for(let t=0;t0){t=Math.floor(t/o)*o;n=Math.ceil(n/o)*o}else if(o<0){t=Math.ceil(t*o)/o;n=Math.floor(n*o)/o}r=o}}function thresholdSturges(t){return Math.max(1,Math.ceil(Math.log(count(t))/Math.LN2)+1)}function bin(){var t=identity,n=extent,e=thresholdSturges;function histogram(o){Array.isArray(o)||(o=Array.from(o));var i,f,u,l=o.length,c=new Array(l);for(i=0;i=h)if(t>=h&&n===extent){const t=tickIncrement(a,h,e);isFinite(t)&&(t>0?h=(Math.floor(h/t)+1)*t:t<0&&(h=(Math.ceil(h*-t)+1)/-t))}else d.pop()}var m=d.length,p=0,g=m;while(d[p]<=a)++p;while(d[g-1]>h)--g;(p||g0?d[i-1]:a;y.x1=i0)for(i=0;i=n)&&(e=n);else{let r=-1;for(let o of t)null!=(o=n(o,++r,t))&&(e=o)&&(e=o)}return e}function maxIndex(t,n){let e;let r=-1;let o=-1;if(void 0===n)for(const n of t){++o;null!=n&&(e=n)&&(e=n,r=o)}else for(let i of t)null!=(i=n(i,++o,t))&&(e=i)&&(e=i,r=o);return r}function min(t,n){let e;if(void 0===n)for(const n of t)null!=n&&(e>n||void 0===e&&n>=n)&&(e=n);else{let r=-1;for(let o of t)null!=(o=n(o,++r,t))&&(e>o||void 0===e&&o>=o)&&(e=o)}return e}function minIndex(t,n){let e;let r=-1;let o=-1;if(void 0===n)for(const n of t){++o;null!=n&&(e>n||void 0===e&&n>=n)&&(e=n,r=o)}else for(let i of t)null!=(i=n(i,++o,t))&&(e>i||void 0===e&&i>=i)&&(e=i,r=o);return r}function quickselect(t,n,e=0,r=Infinity,o){n=Math.floor(n);e=Math.floor(Math.max(0,e));r=Math.floor(Math.min(t.length-1,r));if(!(e<=n&&n<=r))return t;o=void 0===o?ascendingDefined:compareDefined(o);while(r>e){if(r-e>600){const i=r-e+1;const f=n-e+1;const u=Math.log(i);const l=.5*Math.exp(2*u/3);const c=.5*Math.sqrt(u*l*(i-l)/i)*(f-i/2<0?-1:1);const s=Math.max(e,Math.floor(n-f*l/i+c));const a=Math.min(r,Math.floor(n+(i-f)*l/i+c));quickselect(t,n,s,a,o)}const i=t[n];let f=e;let u=r;swap(t,e,n);o(t[r],i)>0&&swap(t,e,r);while(f0)--u}0===o(t[e],i)?swap(t,e,u):(++u,swap(t,u,r));u<=n&&(e=u+1);n<=u&&(r=u-1)}return t}function swap(t,n,e){const r=t[n];t[n]=t[e];t[e]=r}function greatest(t,n=ascending){let e;let r=false;if(1===n.length){let o;for(const i of t){const t=n(i);if(r?ascending(t,o)>0:0===ascending(t,t)){e=i;o=t;r=true}}}else for(const o of t)if(r?n(o,e)>0:0===n(o,o)){e=o;r=true}return e}function quantile(t,n,e){t=Float64Array.from(numbers(t,e));if((r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return min(t);if(n>=1)return max(t);var r,o=(r-1)*n,i=Math.floor(o),f=max(quickselect(t,i).subarray(0,i+1)),u=min(t.subarray(i+1));return f+(u-f)*(o-i)}}function quantileSorted(t,n,e=number){if((r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return+e(t[0],0,t);if(n>=1)return+e(t[r-1],r-1,t);var r,o=(r-1)*n,i=Math.floor(o),f=+e(t[i],i,t),u=+e(t[i+1],i+1,t);return f+(u-f)*(o-i)}}function quantileIndex(t,n,e=number){if(!isNaN(n=+n)){r=Float64Array.from(t,((n,r)=>number(e(t[r],r,t))));if(n<=0)return minIndex(r);if(n>=1)return maxIndex(r);var r,o=Uint32Array.from(t,((t,n)=>n)),i=r.length-1,f=Math.floor(i*n);quickselect(o,f,0,i,((t,n)=>ascendingDefined(r[t],r[n])));f=greatest(o.subarray(0,f+1),(t=>r[t]));return f>=0?f:-1}}function thresholdFreedmanDiaconis(t,n,e){const r=count(t),o=quantile(t,.75)-quantile(t,.25);return r&&o?Math.ceil((e-n)/(2*o*Math.pow(r,-1/3))):1}function thresholdScott(t,n,e){const r=count(t),o=deviation(t);return r&&o?Math.ceil((e-n)*Math.cbrt(r)/(3.49*o)):1}function mean(t,n){let e=0;let r=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(++e,r+=n);else{let o=-1;for(let i of t)null!=(i=n(i,++o,t))&&(i=+i)>=i&&(++e,r+=i)}if(e)return r/e}function median(t,n){return quantile(t,.5,n)}function medianIndex(t,n){return quantileIndex(t,.5,n)}function*flatten(t){for(const n of t)yield*n}function merge(t){return Array.from(flatten(t))}function mode(n,e){const r=new t;if(void 0===e)for(let t of n)null!=t&&t>=t&&r.set(t,(r.get(t)||0)+1);else{let t=-1;for(let o of n)null!=(o=e(o,++t,n))&&o>=o&&r.set(o,(r.get(o)||0)+1)}let o;let i=0;for(const[t,n]of r)if(n>i){i=n;o=t}return o}function pairs(t,n=pair){const e=[];let r;let o=false;for(const i of t){o&&e.push(n(r,i));r=i;o=true}return e}function pair(t,n){return[t,n]}function range(t,n,e){t=+t,n=+n,e=(o=arguments.length)<2?(n=t,t=0,1):o<3?1:+e;var r=-1,o=0|Math.max(0,Math.ceil((n-t)/e)),i=new Array(o);while(++rn(e[t],e[r]);let o,i;t=Uint32Array.from(e,((t,n)=>n));t.sort(n===ascending?(t,n)=>ascendingDefined(e[t],e[n]):compareDefined(compareIndex));t.forEach(((t,n)=>{const e=compareIndex(t,void 0===o?t:o);if(e>=0){(void 0===o||e>0)&&(o=t,i=n);r[t]=i}else r[t]=NaN}));return r}function least(t,n=ascending){let e;let r=false;if(1===n.length){let o;for(const i of t){const t=n(i);if(r?ascending(t,o)<0:0===ascending(t,t)){e=i;o=t;r=true}}}else for(const o of t)if(r?n(o,e)<0:0===n(o,o)){e=o;r=true}return e}function leastIndex(t,n=ascending){if(1===n.length)return minIndex(t,n);let e;let r=-1;let o=-1;for(const i of t){++o;if(r<0?0===n(i,i):n(i,e)<0){e=i;r=o}}return r}function greatestIndex(t,n=ascending){if(1===n.length)return maxIndex(t,n);let e;let r=-1;let o=-1;for(const i of t){++o;if(r<0?0===n(i,i):n(i,e)>0){e=i;r=o}}return r}function scan(t,n){const e=leastIndex(t,n);return e<0?void 0:e}var d=shuffler(Math.random);function shuffler(t){return function shuffle(n,e=0,r=n.length){let o=r-(e=+e);while(o){const r=t()*o--|0,i=n[o+e];n[o+e]=n[r+e];n[r+e]=i}return n}}function sum(t,n){let e=0;if(void 0===n)for(let n of t)(n=+n)&&(e+=n);else{let r=-1;for(let o of t)(o=+n(o,++r,t))&&(e+=o)}return e}function transpose(t){if(!(o=t.length))return[];for(var n=-1,e=min(t,length),r=new Array(e);++nn(e,r,t)))}function reduce(t,n,e){if("function"!==typeof n)throw new TypeError("reducer is not a function");const r=t[Symbol.iterator]();let o,i,f=-1;if(arguments.length<3){({done:o,value:e}=r.next());if(o)return;++f}while(({done:o,value:i}=r.next()),!o)e=n(e,i,++f,t);return e}function reverse(t){if("function"!==typeof t[Symbol.iterator])throw new TypeError("values is not iterable");return Array.from(t).reverse()}function difference(t,...e){t=new n(t);for(const n of e)for(const e of n)t.delete(e);return t}function disjoint(t,e){const r=e[Symbol.iterator](),o=new n;for(const n of t){if(o.has(n))return false;let t,e;while(({value:t,done:e}=r.next())){if(e)break;if(Object.is(n,t))return false;o.add(t)}}return true}function intersection(t,...e){t=new n(t);e=e.map(set);t:for(const n of t)for(const r of e)if(!r.has(n)){t.delete(n);continue t}return t}function set(t){return t instanceof n?t:new n(t)}function superset(t,n){const e=t[Symbol.iterator](),r=new Set;for(const t of n){const n=intern(t);if(r.has(n))continue;let o,i;while(({value:o,done:i}=e.next())){if(i)return false;const t=intern(o);r.add(t);if(Object.is(n,t))break}}return true}function intern(t){return null!==t&&"object"===typeof t?t.valueOf():t}function subset(t,n){return superset(n,t)}function union(...t){const e=new n;for(const n of t)for(const t of n)e.add(t);return e}export{Adder,ascending,bin,r as bisect,i as bisectCenter,o as bisectLeft,r as bisectRight,bisector,blur,f as blur2,u as blurImage,count,cross,cumsum,descending,deviation,difference,disjoint,every,extent,fcumsum,filter,flatGroup,flatRollup,fsum,greatest,greatestIndex,group,groupSort,groups,bin as histogram,index,indexes,intersection,least,leastIndex,map,max,maxIndex,mean,median,medianIndex,merge,min,minIndex,mode,nice,pairs,permute,quantile,quantileIndex,quantileSorted,quickselect,range,rank,reduce,reverse,rollup,rollups,scan,d as shuffle,shuffler,some,sort,subset,sum,superset,thresholdFreedmanDiaconis,thresholdScott,thresholdSturges,tickIncrement,tickStep,ticks,transpose,union,variance,zip}; diff --git a/vendor/javascript/d3-axis.js b/vendor/javascript/d3-axis.js index b8621a79..b541237b 100644 --- a/vendor/javascript/d3-axis.js +++ b/vendor/javascript/d3-axis.js @@ -1,2 +1,4 @@ +// d3-axis@3.0.0 downloaded from https://ga.jspm.io/npm:d3-axis@3.0.0/src/index.js + function identity(t){return t}var t=1,n=2,r=3,i=4,e=1e-6;function translateX(t){return"translate("+t+",0)"}function translateY(t){return"translate(0,"+t+")"}function number(t){return n=>+t(n)}function center(t,n){n=Math.max(0,t.bandwidth()-2*n)/2;t.round()&&(n=Math.round(n));return r=>+t(r)+n}function entering(){return!this.__axis}function axis(a,s){var o=[],u=null,c=null,l=6,x=6,f=3,d="undefined"!==typeof window&&window.devicePixelRatio>1?0:.5,m=a===t||a===i?-1:1,h=a===i||a===n?"x":"y",g=a===t||a===r?translateX:translateY;function axis(p){var k=null==u?s.ticks?s.ticks.apply(s,o):s.domain():u,y=null==c?s.tickFormat?s.tickFormat.apply(s,o):identity:c,A=Math.max(l,0)+f,M=s.range(),v=+M[0]+d,w=+M[M.length-1]+d,_=(s.bandwidth?center:number)(s.copy(),d),b=p.selection?p.selection():p,F=b.selectAll(".domain").data([null]),V=b.selectAll(".tick").data(k,s).order(),z=V.exit(),H=V.enter().append("g").attr("class","tick"),C=V.select("line"),R=V.select("text");F=F.merge(F.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor"));V=V.merge(H);C=C.merge(H.append("line").attr("stroke","currentColor").attr(h+"2",m*l));R=R.merge(H.append("text").attr("fill","currentColor").attr(h,m*A).attr("dy",a===t?"0em":a===r?"0.71em":"0.32em"));if(p!==b){F=F.transition(p);V=V.transition(p);C=C.transition(p);R=R.transition(p);z=z.transition(p).attr("opacity",e).attr("transform",(function(t){return isFinite(t=_(t))?g(t+d):this.getAttribute("transform")}));H.attr("opacity",e).attr("transform",(function(t){var n=this.parentNode.__axis;return g((n&&isFinite(n=n(t))?n:_(t))+d)}))}z.remove();F.attr("d",a===i||a===n?x?"M"+m*x+","+v+"H"+d+"V"+w+"H"+m*x:"M"+d+","+v+"V"+w:x?"M"+v+","+m*x+"V"+d+"H"+w+"V"+m*x:"M"+v+","+d+"H"+w);V.attr("opacity",1).attr("transform",(function(t){return g(_(t)+d)}));C.attr(h+"2",m*l);R.attr(h,m*A).text(y);b.filter(entering).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",a===n?"start":a===i?"end":"middle");b.each((function(){this.__axis=_}))}axis.scale=function(t){return arguments.length?(s=t,axis):s};axis.ticks=function(){return o=Array.from(arguments),axis};axis.tickArguments=function(t){return arguments.length?(o=null==t?[]:Array.from(t),axis):o.slice()};axis.tickValues=function(t){return arguments.length?(u=null==t?null:Array.from(t),axis):u&&u.slice()};axis.tickFormat=function(t){return arguments.length?(c=t,axis):c};axis.tickSize=function(t){return arguments.length?(l=x=+t,axis):l};axis.tickSizeInner=function(t){return arguments.length?(l=+t,axis):l};axis.tickSizeOuter=function(t){return arguments.length?(x=+t,axis):x};axis.tickPadding=function(t){return arguments.length?(f=+t,axis):f};axis.offset=function(t){return arguments.length?(d=+t,axis):d};return axis}function axisTop(n){return axis(t,n)}function axisRight(t){return axis(n,t)}function axisBottom(t){return axis(r,t)}function axisLeft(t){return axis(i,t)}export{axisBottom,axisLeft,axisRight,axisTop}; diff --git a/vendor/javascript/d3-brush.js b/vendor/javascript/d3-brush.js index 53cbc53a..d1270ace 100644 --- a/vendor/javascript/d3-brush.js +++ b/vendor/javascript/d3-brush.js @@ -1,2 +1,4 @@ +// d3-brush@3.0.0 downloaded from https://ga.jspm.io/npm:d3-brush@3.0.0/src/index.js + import{dispatch as e}from"d3-dispatch";import{dragDisable as t,dragEnable as n}from"d3-drag";import{interpolate as r}from"d3-interpolate";import{select as u,pointer as i}from"d3-selection";import{interrupt as s}from"d3-transition";var constant=e=>()=>e;function BrushEvent(e,{sourceEvent:t,target:n,selection:r,mode:u,dispatch:i}){Object.defineProperties(this,{type:{value:e,enumerable:true,configurable:true},sourceEvent:{value:t,enumerable:true,configurable:true},target:{value:n,enumerable:true,configurable:true},selection:{value:r,enumerable:true,configurable:true},mode:{value:u,enumerable:true,configurable:true},_:{value:i}})}function nopropagation(e){e.stopImmediatePropagation()}function noevent(e){e.preventDefault();e.stopImmediatePropagation()}var o={name:"drag"},a={name:"space"},l={name:"handle"},c={name:"center"};const{abs:h,max:f,min:d}=Math;function number1(e){return[+e[0],+e[1]]}function number2(e){return[number1(e[0]),number1(e[1])]}var b={name:"x",handles:["w","e"].map(type),input:function(e,t){return null==e?null:[[+e[0],t[0][1]],[+e[1],t[1][1]]]},output:function(e){return e&&[e[0][0],e[1][0]]}};var p={name:"y",handles:["n","s"].map(type),input:function(e,t){return null==e?null:[[t[0][0],+e[0]],[t[1][0],+e[1]]]},output:function(e){return e&&[e[0][1],e[1][1]]}};var m={name:"xy",handles:["n","w","e","s","nw","ne","sw","se"].map(type),input:function(e){return null==e?null:number2(e)},output:function(e){return e}};var v={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"};var y={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"};var w={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"};var g={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1};var _={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1};function type(e){return{type:e}}function defaultFilter(e){return!e.ctrlKey&&!e.button}function defaultExtent(){var e=this.ownerSVGElement||this;if(e.hasAttribute("viewBox")){e=e.viewBox.baseVal;return[[e.x,e.y],[e.x+e.width,e.y+e.height]]}return[[0,0],[e.width.baseVal.value,e.height.baseVal.value]]}function defaultTouchable(){return navigator.maxTouchPoints||"ontouchstart"in this}function local(e){while(!e.__brush)if(!(e=e.parentNode))return;return e.__brush}function empty(e){return e[0][0]===e[1][0]||e[0][1]===e[1][1]}function brushSelection(e){var t=e.__brush;return t?t.dim.output(t.selection):null}function brushX(){return brush$1(b)}function brushY(){return brush$1(p)}function brush(){return brush$1(m)}function brush$1(m){var k,x=defaultExtent,E=defaultFilter,z=defaultTouchable,A=true,T=e("start","brush","end"),K=6;function brush(e){var t=e.property("__brush",initialize).selectAll(".overlay").data([type("overlay")]);t.enter().append("rect").attr("class","overlay").attr("pointer-events","all").attr("cursor",v.overlay).merge(t).each((function(){var e=local(this).extent;u(this).attr("x",e[0][0]).attr("y",e[0][1]).attr("width",e[1][0]-e[0][0]).attr("height",e[1][1]-e[0][1])}));e.selectAll(".selection").data([type("selection")]).enter().append("rect").attr("class","selection").attr("cursor",v.selection).attr("fill","#777").attr("fill-opacity",.3).attr("stroke","#fff").attr("shape-rendering","crispEdges");var n=e.selectAll(".handle").data(m.handles,(function(e){return e.type}));n.exit().remove();n.enter().append("rect").attr("class",(function(e){return"handle handle--"+e.type})).attr("cursor",(function(e){return v[e.type]}));e.each(redraw).attr("fill","none").attr("pointer-events","all").on("mousedown.brush",started).filter(z).on("touchstart.brush",started).on("touchmove.brush",touchmoved).on("touchend.brush touchcancel.brush",touchended).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}brush.move=function(e,t,n){e.tween?e.on("start.brush",(function(e){emitter(this,arguments).beforestart().start(e)})).on("interrupt.brush end.brush",(function(e){emitter(this,arguments).end(e)})).tween("brush",(function(){var e=this,n=e.__brush,u=emitter(e,arguments),i=n.selection,s=m.input("function"===typeof t?t.apply(this,arguments):t,n.extent),o=r(i,s);function tween(t){n.selection=1===t&&null===s?null:o(t);redraw.call(e);u.brush()}return null!==i&&null!==s?tween:tween(1)})):e.each((function(){var e=this,r=arguments,u=e.__brush,i=m.input("function"===typeof t?t.apply(e,r):t,u.extent),o=emitter(e,r).beforestart();s(e);u.selection=null===i?null:i;redraw.call(e);o.start(n).brush(n).end(n)}))};brush.clear=function(e,t){brush.move(e,null,t)};function redraw(){var e=u(this),t=local(this).selection;if(t){e.selectAll(".selection").style("display",null).attr("x",t[0][0]).attr("y",t[0][1]).attr("width",t[1][0]-t[0][0]).attr("height",t[1][1]-t[0][1]);e.selectAll(".handle").style("display",null).attr("x",(function(e){return"e"===e.type[e.type.length-1]?t[1][0]-K/2:t[0][0]-K/2})).attr("y",(function(e){return"s"===e.type[0]?t[1][1]-K/2:t[0][1]-K/2})).attr("width",(function(e){return"n"===e.type||"s"===e.type?t[1][0]-t[0][0]+K:K})).attr("height",(function(e){return"e"===e.type||"w"===e.type?t[1][1]-t[0][1]+K:K}))}else e.selectAll(".selection,.handle").style("display","none").attr("x",null).attr("y",null).attr("width",null).attr("height",null)}function emitter(e,t,n){var r=e.__brush.emitter;return!r||n&&r.clean?new Emitter(e,t,n):r}function Emitter(e,t,n){this.that=e;this.args=t;this.state=e.__brush;this.active=0;this.clean=n}Emitter.prototype={beforestart:function(){1===++this.active&&(this.state.emitter=this,this.starting=true);return this},start:function(e,t){this.starting?(this.starting=false,this.emit("start",e,t)):this.emit("brush",e);return this},brush:function(e,t){this.emit("brush",e,t);return this},end:function(e,t){0===--this.active&&(delete this.state.emitter,this.emit("end",e,t));return this},emit:function(e,t,n){var r=u(this.that).datum();T.call(e,this.that,new BrushEvent(e,{sourceEvent:t,target:brush,selection:m.output(this.state.selection),mode:n,dispatch:T}),r)}};function started(e){if((!k||e.touches)&&E.apply(this,arguments)){var r,x,z,T,K,B,P,S,V,$,C,F=this,I=e.target.__data__.type,M="selection"===(A&&e.metaKey?I="overlay":I)?o:A&&e.altKey?c:l,X=m===p?null:g[I],Y=m===b?null:_[I],j=local(F),D=j.extent,G=j.selection,N=D[0][0],O=D[0][1],q=D[1][0],H=D[1][1],J=0,L=0,Q=X&&Y&&A&&e.shiftKey,R=Array.from(e.touches||[e],(e=>{const t=e.identifier;e=i(e,F);e.point0=e.slice();e.identifier=t;return e}));s(F);var U=emitter(F,arguments,true).beforestart();if("overlay"===I){G&&(V=true);const t=[R[0],R[1]||R[0]];j.selection=G=[[r=m===p?N:d(t[0][0],t[1][0]),z=m===b?O:d(t[0][1],t[1][1])],[K=m===p?q:f(t[0][0],t[1][0]),P=m===b?H:f(t[0][1],t[1][1])]];R.length>1&&move(e)}else{r=G[0][0];z=G[0][1];K=G[1][0];P=G[1][1]}x=r;T=z;B=K;S=P;var W=u(F).attr("pointer-events","none");var Z=W.selectAll(".overlay").attr("cursor",v[I]);if(e.touches){U.moved=moved;U.ended=ended}else{var ee=u(e.view).on("mousemove.brush",moved,true).on("mouseup.brush",ended,true);A&&ee.on("keydown.brush",keydowned,true).on("keyup.brush",keyupped,true);t(e.view)}redraw.call(F);U.start(e,M.name)}function moved(e){for(const t of e.changedTouches||[e])for(const e of R)e.identifier===t.identifier&&(e.cur=i(t,F));if(Q&&!$&&!C&&1===R.length){const e=R[0];h(e.cur[0]-e[0])>h(e.cur[1]-e[1])?C=true:$=true}for(const e of R)e.cur&&(e[0]=e.cur[0],e[1]=e.cur[1]);V=true;noevent(e);move(e)}function move(e){const t=R[0],n=t.point0;var u;J=t[0]-n[0];L=t[1]-n[1];switch(M){case a:case o:X&&(J=f(N-r,d(q-K,J)),x=r+J,B=K+J);Y&&(L=f(O-z,d(H-P,L)),T=z+L,S=P+L);break;case l:if(R[1]){X&&(x=f(N,d(q,R[0][0])),B=f(N,d(q,R[1][0])),X=1);Y&&(T=f(O,d(H,R[0][1])),S=f(O,d(H,R[1][1])),Y=1)}else{X<0?(J=f(N-r,d(q-r,J)),x=r+J,B=K):X>0&&(J=f(N-K,d(q-K,J)),x=r,B=K+J);Y<0?(L=f(O-z,d(H-z,L)),T=z+L,S=P):Y>0&&(L=f(O-P,d(H-P,L)),T=z,S=P+L)}break;case c:X&&(x=f(N,d(q,r-J*X)),B=f(N,d(q,K+J*X)));Y&&(T=f(O,d(H,z-L*Y)),S=f(O,d(H,P+L*Y)));break}if(B0&&(r=x-J);Y<0?P=S-L:Y>0&&(z=T-L);M=a;Z.attr("cursor",v.selection);move(e)}break;default:return}noevent(e)}function keyupped(e){switch(e.keyCode){case 16:if(Q){$=C=Q=false;move(e)}break;case 18:if(M===c){X<0?K=B:X>0&&(r=x);Y<0?P=S:Y>0&&(z=T);M=l;move(e)}break;case 32:if(M===a){if(e.altKey){X&&(K=B-J*X,r=x+J*X);Y&&(P=S-L*Y,z=T+L*Y);M=c}else{X<0?K=B:X>0&&(r=x);Y<0?P=S:Y>0&&(z=T);M=l}Z.attr("cursor",v[I]);move(e)}break;default:return}noevent(e)}}function touchmoved(e){emitter(this,arguments).moved(e)}function touchended(e){emitter(this,arguments).ended(e)}function initialize(){var e=this.__brush||{selection:null};e.extent=number2(x.apply(this,arguments));e.dim=m;return e}brush.extent=function(e){return arguments.length?(x="function"===typeof e?e:constant(number2(e)),brush):x};brush.filter=function(e){return arguments.length?(E="function"===typeof e?e:constant(!!e),brush):E};brush.touchable=function(e){return arguments.length?(z="function"===typeof e?e:constant(!!e),brush):z};brush.handleSize=function(e){return arguments.length?(K=+e,brush):K};brush.keyModifiers=function(e){return arguments.length?(A=!!e,brush):A};brush.on=function(){var e=T.on.apply(T,arguments);return e===T?brush:e};return brush}export{brush,brushSelection,brushX,brushY}; diff --git a/vendor/javascript/d3-chord.js b/vendor/javascript/d3-chord.js index ae586d74..8b0c622c 100644 --- a/vendor/javascript/d3-chord.js +++ b/vendor/javascript/d3-chord.js @@ -1,2 +1,4 @@ +// d3-chord@3.0.1 downloaded from https://ga.jspm.io/npm:d3-chord@3.0.1/src/index.js + import{path as n}from"d3-path";var r=Math.abs;var t=Math.cos;var e=Math.sin;var o=Math.PI;var u=o/2;var a=2*o;var l=Math.max;var i=1e-12;function range(n,r){return Array.from({length:r-n},((r,t)=>n+t))}function compareValue(n){return function(r,t){return n(r.source.value+r.target.value,t.source.value+t.target.value)}}function chord(){return chord$1(false,false)}function chordTranspose(){return chord$1(false,true)}function chordDirected(){return chord$1(true,false)}function chord$1(n,r){var t=0,e=null,o=null,u=null;function chord(i){var c,s=i.length,f=new Array(s),d=range(0,s),g=new Array(s*s),b=new Array(s),h=0;i=Float64Array.from({length:s*s},r?(n,r)=>i[r%s][r/s|0]:(n,r)=>i[r/s|0][r%s]);for(let r=0;re(f[n],f[r])));for(const t of d){const e=r;if(n){const n=range(1+~s,s).filter((n=>n<0?i[~n*s+t]:i[t*s+n]));o&&n.sort(((n,r)=>o(n<0?-i[~n*s+t]:i[t*s+n],r<0?-i[~r*s+t]:i[t*s+r])));for(const e of n)if(e<0){const n=g[~e*s+t]||(g[~e*s+t]={source:null,target:null});n.target={index:t,startAngle:r,endAngle:r+=i[~e*s+t]*h,value:i[~e*s+t]}}else{const n=g[t*s+e]||(g[t*s+e]={source:null,target:null});n.source={index:t,startAngle:r,endAngle:r+=i[t*s+e]*h,value:i[t*s+e]}}b[t]={index:t,startAngle:e,endAngle:r,value:f[t]}}else{const n=range(0,s).filter((n=>i[t*s+n]||i[n*s+t]));o&&n.sort(((n,r)=>o(i[t*s+n],i[t*s+r])));for(const e of n){let n;if(ti){r(R-m)>2*y+i?R>m?(m+=y,R-=y):(m-=y,R+=y):m=R=(m+R)/2;r(M-$)>2*y+i?M>$?($+=y,M-=y):($-=y,M+=y):$=M=($+M)/2}h.moveTo(x*t(m),x*e(m));h.arc(0,0,x,m,R);if(m!==$||R!==M)if(o){var S=+o.apply(this,arguments),C=w-S,P=($+M)/2;h.quadraticCurveTo(0,0,C*t($),C*e($));h.lineTo(w*t(P),w*e(P));h.lineTo(C*t(M),C*e(M))}else{h.quadraticCurveTo(0,0,w*t($),w*e($));h.arc(0,0,w,$,M)}h.quadraticCurveTo(0,0,x*t(m),x*e(m));h.closePath();if(p)return h=null,p+""||null}o&&(ribbon.headRadius=function(n){return arguments.length?(o="function"===typeof n?n:constant(+n),ribbon):o});ribbon.radius=function(n){return arguments.length?(s=f="function"===typeof n?n:constant(+n),ribbon):s};ribbon.sourceRadius=function(n){return arguments.length?(s="function"===typeof n?n:constant(+n),ribbon):s};ribbon.targetRadius=function(n){return arguments.length?(f="function"===typeof n?n:constant(+n),ribbon):f};ribbon.startAngle=function(n){return arguments.length?(d="function"===typeof n?n:constant(+n),ribbon):d};ribbon.endAngle=function(n){return arguments.length?(g="function"===typeof n?n:constant(+n),ribbon):g};ribbon.padAngle=function(n){return arguments.length?(b="function"===typeof n?n:constant(+n),ribbon):b};ribbon.source=function(n){return arguments.length?(a=n,ribbon):a};ribbon.target=function(n){return arguments.length?(l=n,ribbon):l};ribbon.context=function(n){return arguments.length?(h=null==n?null:n,ribbon):h};return ribbon}function ribbon$1(){return ribbon()}function ribbonArrow(){return ribbon(defaultArrowheadRadius)}export{chord,chordDirected,chordTranspose,ribbon$1 as ribbon,ribbonArrow}; diff --git a/vendor/javascript/d3-color.js b/vendor/javascript/d3-color.js index 16068986..a8f795fe 100644 --- a/vendor/javascript/d3-color.js +++ b/vendor/javascript/d3-color.js @@ -1,2 +1,4 @@ +// d3-color@3.1.0 downloaded from https://ga.jspm.io/npm:d3-color@3.1.0/src/index.js + function define(t,e,r){t.prototype=e.prototype=r;r.constructor=t}function extend(t,e){var r=Object.create(t.prototype);for(var n in e)r[n]=e[n];return r}function Color(){}var t=.7;var e=1/t;var r="\\s*([+-]?\\d+)\\s*",n="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",i="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",a=/^#([0-9a-f]{3,8})$/,l=new RegExp(`^rgb\\(${r},${r},${r}\\)$`),o=new RegExp(`^rgb\\(${i},${i},${i}\\)$`),h=new RegExp(`^rgba\\(${r},${r},${r},${n}\\)$`),s=new RegExp(`^rgba\\(${i},${i},${i},${n}\\)$`),c=new RegExp(`^hsl\\(${n},${i},${i}\\)$`),b=new RegExp(`^hsla\\(${n},${i},${i},${n}\\)$`);var u={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};define(Color,color,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:color_formatHex,formatHex:color_formatHex,formatHex8:color_formatHex8,formatHsl:color_formatHsl,formatRgb:color_formatRgb,toString:color_formatRgb});function color_formatHex(){return this.rgb().formatHex()}function color_formatHex8(){return this.rgb().formatHex8()}function color_formatHsl(){return hslConvert(this).formatHsl()}function color_formatRgb(){return this.rgb().formatRgb()}function color(t){var e,r;t=(t+"").trim().toLowerCase();return(e=a.exec(t))?(r=e[1].length,e=parseInt(e[1],16),6===r?rgbn(e):3===r?new Rgb(e>>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===r?rgba(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===r?rgba(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=l.exec(t))?new Rgb(e[1],e[2],e[3],1):(e=o.exec(t))?new Rgb(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=h.exec(t))?rgba(e[1],e[2],e[3],e[4]):(e=s.exec(t))?rgba(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=c.exec(t))?hsla(e[1],e[2]/100,e[3]/100,1):(e=b.exec(t))?hsla(e[1],e[2]/100,e[3]/100,e[4]):u.hasOwnProperty(t)?rgbn(u[t]):"transparent"===t?new Rgb(NaN,NaN,NaN,0):null}function rgbn(t){return new Rgb(t>>16&255,t>>8&255,255&t,1)}function rgba(t,e,r,n){n<=0&&(t=e=r=NaN);return new Rgb(t,e,r,n)}function rgbConvert(t){t instanceof Color||(t=color(t));if(!t)return new Rgb;t=t.rgb();return new Rgb(t.r,t.g,t.b,t.opacity)}function rgb(t,e,r,n){return 1===arguments.length?rgbConvert(t):new Rgb(t,e,r,null==n?1:n)}function Rgb(t,e,r,n){this.r=+t;this.g=+e;this.b=+r;this.opacity=+n}define(Rgb,rgb,extend(Color,{brighter(t){t=null==t?e:Math.pow(e,t);return new Rgb(this.r*t,this.g*t,this.b*t,this.opacity)},darker(e){e=null==e?t:Math.pow(t,e);return new Rgb(this.r*e,this.g*e,this.b*e,this.opacity)},rgb(){return this},clamp(){return new Rgb(clampi(this.r),clampi(this.g),clampi(this.b),clampa(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:rgb_formatHex,formatHex:rgb_formatHex,formatHex8:rgb_formatHex8,formatRgb:rgb_formatRgb,toString:rgb_formatRgb}));function rgb_formatHex(){return`#${hex(this.r)}${hex(this.g)}${hex(this.b)}`}function rgb_formatHex8(){return`#${hex(this.r)}${hex(this.g)}${hex(this.b)}${hex(255*(isNaN(this.opacity)?1:this.opacity))}`}function rgb_formatRgb(){const t=clampa(this.opacity);return`${1===t?"rgb(":"rgba("}${clampi(this.r)}, ${clampi(this.g)}, ${clampi(this.b)}${1===t?")":`, ${t})`}`}function clampa(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function clampi(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function hex(t){t=clampi(t);return(t<16?"0":"")+t.toString(16)}function hsla(t,e,r,n){n<=0?t=e=r=NaN:r<=0||r>=1?t=e=NaN:e<=0&&(t=NaN);return new Hsl(t,e,r,n)}function hslConvert(t){if(t instanceof Hsl)return new Hsl(t.h,t.s,t.l,t.opacity);t instanceof Color||(t=color(t));if(!t)return new Hsl;if(t instanceof Hsl)return t;t=t.rgb();var e=t.r/255,r=t.g/255,n=t.b/255,i=Math.min(e,r,n),a=Math.max(e,r,n),l=NaN,o=a-i,h=(a+i)/2;if(o){l=e===a?(r-n)/o+6*(r0&&h<1?0:l;return new Hsl(l,o,h,t.opacity)}function hsl(t,e,r,n){return 1===arguments.length?hslConvert(t):new Hsl(t,e,r,null==n?1:n)}function Hsl(t,e,r,n){this.h=+t;this.s=+e;this.l=+r;this.opacity=+n}define(Hsl,hsl,extend(Color,{brighter(t){t=null==t?e:Math.pow(e,t);return new Hsl(this.h,this.s,this.l*t,this.opacity)},darker(e){e=null==e?t:Math.pow(t,e);return new Hsl(this.h,this.s,this.l*e,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,r=this.l,n=r+(r<.5?r:1-r)*e,i=2*r-n;return new Rgb(hsl2rgb(t>=240?t-240:t+120,i,n),hsl2rgb(t,i,n),hsl2rgb(t<120?t+240:t-120,i,n),this.opacity)},clamp(){return new Hsl(clamph(this.h),clampt(this.s),clampt(this.l),clampa(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=clampa(this.opacity);return`${1===t?"hsl(":"hsla("}${clamph(this.h)}, ${100*clampt(this.s)}%, ${100*clampt(this.l)}%${1===t?")":`, ${t})`}`}}));function clamph(t){t=(t||0)%360;return t<0?t+360:t}function clampt(t){return Math.max(0,Math.min(1,t||0))}function hsl2rgb(t,e,r){return 255*(t<60?e+(r-e)*t/60:t<180?r:t<240?e+(r-e)*(240-t)/60:e)}const g=Math.PI/180;const p=180/Math.PI;const f=18,m=.96422,d=1,y=.82521,w=4/29,x=6/29,$=3*x*x,v=x*x*x;function labConvert(t){if(t instanceof Lab)return new Lab(t.l,t.a,t.b,t.opacity);if(t instanceof Hcl)return hcl2lab(t);t instanceof Rgb||(t=rgbConvert(t));var e,r,n=rgb2lrgb(t.r),i=rgb2lrgb(t.g),a=rgb2lrgb(t.b),l=xyz2lab((.2225045*n+.7168786*i+.0606169*a)/d);if(n===i&&i===a)e=r=l;else{e=xyz2lab((.4360747*n+.3850649*i+.1430804*a)/m);r=xyz2lab((.0139322*n+.0971045*i+.7141733*a)/y)}return new Lab(116*l-16,500*(e-l),200*(l-r),t.opacity)}function gray(t,e){return new Lab(t,0,0,null==e?1:e)}function lab(t,e,r,n){return 1===arguments.length?labConvert(t):new Lab(t,e,r,null==n?1:n)}function Lab(t,e,r,n){this.l=+t;this.a=+e;this.b=+r;this.opacity=+n}define(Lab,lab,extend(Color,{brighter(t){return new Lab(this.l+f*(null==t?1:t),this.a,this.b,this.opacity)},darker(t){return new Lab(this.l-f*(null==t?1:t),this.a,this.b,this.opacity)},rgb(){var t=(this.l+16)/116,e=isNaN(this.a)?t:t+this.a/500,r=isNaN(this.b)?t:t-this.b/200;e=m*lab2xyz(e);t=d*lab2xyz(t);r=y*lab2xyz(r);return new Rgb(lrgb2rgb(3.1338561*e-1.6168667*t-.4906146*r),lrgb2rgb(-.9787684*e+1.9161415*t+.033454*r),lrgb2rgb(.0719453*e-.2289914*t+1.4052427*r),this.opacity)}}));function xyz2lab(t){return t>v?Math.pow(t,1/3):t/$+w}function lab2xyz(t){return t>x?t*t*t:$*(t-w)}function lrgb2rgb(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function rgb2lrgb(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function hclConvert(t){if(t instanceof Hcl)return new Hcl(t.h,t.c,t.l,t.opacity);t instanceof Lab||(t=labConvert(t));if(0===t.a&&0===t.b)return new Hcl(NaN,0()=>n;function contains(n,t){var r,o=-1,i=t.length;while(++oo!==d>o&&r<(l-f)*(o-c)/(d-c)+f&&(i=-i)}return i}function segmentContains(n,t,r){var o;return collinear(n,t,r)&&within(n[o=+(n[0]===t[0])],r[o],t[o])}function collinear(n,t,r){return(t[0]-n[0])*(r[1]-n[1])===(r[0]-n[0])*(t[1]-n[1])}function within(n,t,r){return n<=t&&t<=r||r<=t&&t<=n}function noop(){}var u=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function Contours(){var i=1,e=1,a=n,f=smoothLinear;function contours(n){var i=a(n);if(Array.isArray(i))i=i.slice().sort(ascending);else{const e=t(n,finite);i=r(...o(e[0],e[1],i),i);while(i[i.length-1]>=e[1])i.pop();while(i[1]contour(n,t)))}function contour(n,t){const r=null==t?NaN:+t;if(isNaN(r))throw new Error(`invalid value: ${t}`);var o=[],i=[];isorings(n,r,(function(t){f(t,n,r);area(t)>0?o.push([t]):i.push(t)}));i.forEach((function(n){for(var t,r=0,i=o.length;r=t;u[c<<2].forEach(stitch);while(++o0&&o0&&a=0&&r>=0))throw new Error("invalid size");return i=t,e=r,contours};contours.thresholds=function(n){return arguments.length?(a="function"===typeof n?n:Array.isArray(n)?constant(s.call(n)):constant(n),contours):a};contours.smooth=function(n){return arguments.length?(f=n?smoothLinear:noop,contours):f===smoothLinear};return contours}function finite(n){return isFinite(n)?n:NaN}function above(n,t){return null!=n&&+n>=t}function valid(n){return null==n||isNaN(n=+n)?-Infinity:n}function smooth1(n,t,r,o){const i=o-t;const e=r-t;const a=isFinite(i)||isFinite(e)?i/e:Math.sign(i)/Math.sign(e);return isNaN(a)?n:n+a-.5}function defaultX(n){return n[0]}function defaultY(n){return n[1]}function defaultWeight(){return 1}function density(){var n=defaultX,t=defaultY,o=defaultWeight,a=960,u=500,f=20,c=2,h=3*f,l=a+2*h>>c,d=u+2*h>>c,g=constant(20);function grid(r){var e=new Float32Array(l*d),a=Math.pow(2,-c),s=-1;for(const i of r){var u=(n(i,++s,r)+h)*a,g=(t(i,s,r)+h)*a,v=+o(i,s,r);if(v&&u>=0&&u=0&&gn*i)))(t).map(((n,t)=>(n.value=+o[t],transform(n))))}density.contours=function(n){var t=grid(n),r=Contours().size([l,d]),o=Math.pow(2,2*c),contour=n=>{n=+n;var i=transform(r.contour(t,n*o));i.value=n;return i};Object.defineProperty(contour,"max",{get:()=>e(t)/o});return contour};function transform(n){n.coordinates.forEach(transformPolygon);return n}function transformPolygon(n){n.forEach(transformRing)}function transformRing(n){n.forEach(transformPoint)}function transformPoint(n){n[0]=n[0]*Math.pow(2,c)-h;n[1]=n[1]*Math.pow(2,c)-h}function resize(){h=3*f;l=a+2*h>>c;d=u+2*h>>c;return density}density.x=function(t){return arguments.length?(n="function"===typeof t?t:constant(+t),density):n};density.y=function(n){return arguments.length?(t="function"===typeof n?n:constant(+n),density):t};density.weight=function(n){return arguments.length?(o="function"===typeof n?n:constant(+n),density):o};density.size=function(n){if(!arguments.length)return[a,u];var t=+n[0],r=+n[1];if(!(t>=0&&r>=0))throw new Error("invalid size");return a=t,u=r,resize()};density.cellSize=function(n){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return c=Math.floor(Math.log(n)/Math.LN2),resize()};density.thresholds=function(n){return arguments.length?(g="function"===typeof n?n:Array.isArray(n)?constant(s.call(n)):constant(n),density):g};density.bandwidth=function(n){if(!arguments.length)return Math.sqrt(f*(f+1));if(!((n=+n)>=0))throw new Error("invalid bandwidth");return f=(Math.sqrt(4*n*n+1)-1)/2,resize()};return density}export{density as contourDensity,Contours as contours}; diff --git a/vendor/javascript/d3-delaunay.js b/vendor/javascript/d3-delaunay.js index a9568a9e..7d9c7243 100644 --- a/vendor/javascript/d3-delaunay.js +++ b/vendor/javascript/d3-delaunay.js @@ -1,2 +1,4 @@ +// d3-delaunay@6.0.4 downloaded from https://ga.jspm.io/npm:d3-delaunay@6.0.4/src/index.js + import t from"delaunator";const n=1e-6;class Path{constructor(){this._x0=this._y0=this._x1=this._y1=null;this._=""}moveTo(t,n){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}`}closePath(){if(null!==this._x1){this._x1=this._x0,this._y1=this._y0;this._+="Z"}}lineTo(t,n){this._+=`L${this._x1=+t},${this._y1=+n}`}arc(t,e,i){t=+t,e=+e,i=+i;const s=t+i;const l=e;if(i<0)throw new Error("negative radius");null===this._x1?this._+=`M${s},${l}`:(Math.abs(this._x1-s)>n||Math.abs(this._y1-l)>n)&&(this._+="L"+s+","+l);i&&(this._+=`A${i},${i},0,1,1,${t-i},${e}A${i},${i},0,1,1,${this._x1=s},${this._y1=l}`)}rect(t,n,e,i){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${+e}v${+i}h${-e}Z`}value(){return this._||null}}class Polygon{constructor(){this._=[]}moveTo(t,n){this._.push([t,n])}closePath(){this._.push(this._[0].slice())}lineTo(t,n){this._.push([t,n])}value(){return this._.length?this._:null}}class Voronoi{constructor(t,[n,e,i,s]=[0,0,960,500]){if(!((i=+i)>=(n=+n))||!((s=+s)>=(e=+e)))throw new Error("invalid bounds");this.delaunay=t;this._circumcenters=new Float64Array(2*t.points.length);this.vectors=new Float64Array(2*t.points.length);this.xmax=i,this.xmin=n;this.ymax=s,this.ymin=e;this._init()}update(){this.delaunay.update();this._init();return this}_init(){const{delaunay:{points:t,hull:n,triangles:e},vectors:i}=this;let s,l;const h=this.circumcenters=this._circumcenters.subarray(0,e.length/3*2);for(let i,o,r=0,c=0,a=e.length;r1)s-=2;for(let t=2;t0){if(n>=this.ymax)return null;(s=(this.ymax-n)/i)0){if(t>=this.xmax)return null;(s=(this.xmax-t)/e)this.xmax?2:0)|(nthis.ymax?8:0)}_simplify(t){if(t&&t.length>4){for(let n=0;n1e-10)return false}return true}function jitter(t,n,e){return[t+Math.sin(t+n)*e,n+Math.cos(t-n)*e]}class Delaunay{static from(t,n=pointX,e=pointY,i){return new Delaunay("length"in t?flatArray(t,n,e,i):Float64Array.from(flatIterable(t,n,e,i)))}constructor(n){this._delaunator=new t(n);this.inedges=new Int32Array(n.length/2);this._hullIndex=new Int32Array(n.length/2);this.points=this._delaunator.coords;this._init()}update(){this._delaunator.update();this._init();return this}_init(){const n=this._delaunator,e=this.points;if(n.hull&&n.hull.length>2&&collinear(n)){this.collinear=Int32Array.from({length:e.length/2},((t,n)=>n)).sort(((t,n)=>e[2*t]-e[2*n]||e[2*t+1]-e[2*n+1]));const n=this.collinear[0],i=this.collinear[this.collinear.length-1],s=[e[2*n],e[2*n+1],e[2*i],e[2*i+1]],l=1e-8*Math.hypot(s[3]-s[1],s[2]-s[0]);for(let t=0,n=e.length/2;t0){this.triangles=new Int32Array(3).fill(-1);this.halfedges=new Int32Array(3).fill(-1);this.triangles[0]=s[0];h[s[0]]=1;if(2===s.length){h[s[1]]=0;this.triangles[1]=s[1];this.triangles[2]=s[1]}}}voronoi(t){return new Voronoi(this,t)}*neighbors(t){const{inedges:n,hull:e,_hullIndex:i,halfedges:s,triangles:l,collinear:h}=this;if(h){const n=h.indexOf(t);n>0&&(yield h[n-1]);n=0&&s!==e&&s!==i)e=s;return s}_step(t,n,e){const{inedges:s,hull:l,_hullIndex:h,halfedges:o,triangles:r,points:c}=this;if(-1===s[t]||!c.length)return(t+1)%(c.length>>1);let a=t;let u=i(n-c[2*t],2)+i(e-c[2*t+1],2);const g=s[t];let f=g;do{let s=r[f];const g=i(n-c[2*s],2)+i(e-c[2*s+1],2);g{}};function dispatch(){for(var n,t=0,e=arguments.length,r={};t=0&&(e=n.slice(r+1),n=n.slice(0,r));if(n&&!t.hasOwnProperty(n))throw new Error("unknown type: "+n);return{type:n,name:e}}))}Dispatch.prototype=dispatch.prototype={constructor:Dispatch,on:function(n,t){var e,r=this._,i=parseTypenames(n+"",r),a=-1,o=i.length;if(!(arguments.length<2)){if(null!=t&&"function"!==typeof t)throw new Error("invalid callback: "+t);while(++a0)for(var e,r,i=new Array(e),a=0;a()=>e;function DragEvent(e,{sourceEvent:t,subject:n,target:r,identifier:a,active:o,x:u,y:i,dx:c,dy:l,dispatch:d}){Object.defineProperties(this,{type:{value:e,enumerable:true,configurable:true},sourceEvent:{value:t,enumerable:true,configurable:true},subject:{value:n,enumerable:true,configurable:true},target:{value:r,enumerable:true,configurable:true},identifier:{value:a,enumerable:true,configurable:true},active:{value:o,enumerable:true,configurable:true},x:{value:u,enumerable:true,configurable:true},y:{value:i,enumerable:true,configurable:true},dx:{value:c,enumerable:true,configurable:true},dy:{value:l,enumerable:true,configurable:true},_:{value:d}})}DragEvent.prototype.on=function(){var e=this._.on.apply(this._,arguments);return e===this._?this:e};function defaultFilter(e){return!e.ctrlKey&&!e.button}function defaultContainer(){return this.parentNode}function defaultSubject(e,t){return null==t?{x:e.x,y:e.y}:t}function defaultTouchable(){return navigator.maxTouchPoints||"ontouchstart"in this}function drag(){var o,u,i,c,l=defaultFilter,d=defaultContainer,s=defaultSubject,f=defaultTouchable,g={},v=e("start","drag","end"),h=0,m=0;function drag(e){e.on("mousedown.drag",mousedowned).filter(f).on("touchstart.drag",touchstarted).on("touchmove.drag",touchmoved,r).on("touchend.drag touchcancel.drag",touchended).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function mousedowned(e,n){if(!c&&l.call(this,e,n)){var r=beforestart(this,d.call(this,e,n),e,n,"mouse");if(r){t(e.view).on("mousemove.drag",mousemoved,a).on("mouseup.drag",mouseupped,a);nodrag(e.view);nopropagation(e);i=false;o=e.clientX;u=e.clientY;r("start",e)}}}function mousemoved(e){noevent(e);if(!i){var t=e.clientX-o,n=e.clientY-u;i=t*t+n*n>m}g.mouse("drag",e)}function mouseupped(e){t(e.view).on("mousemove.drag mouseup.drag",null);yesdrag(e.view,i);noevent(e);g.mouse("end",e)}function touchstarted(e,t){if(l.call(this,e,t)){var n,r,a=e.changedTouches,o=d.call(this,e,t),u=a.length;for(n=0;n9999?"+"+pad(r,6):pad(r,4)}function formatDate(r){var e=r.getUTCHours(),t=r.getUTCMinutes(),a=r.getUTCSeconds(),o=r.getUTCMilliseconds();return isNaN(r)?"Invalid Date":formatYear(r.getUTCFullYear(),4)+"-"+pad(r.getUTCMonth()+1,2)+"-"+pad(r.getUTCDate(),2)+(o?"T"+pad(e,2)+":"+pad(t,2)+":"+pad(a,2)+"."+pad(o,3)+"Z":a?"T"+pad(e,2)+":"+pad(t,2)+":"+pad(a,2)+"Z":t||e?"T"+pad(e,2)+":"+pad(t,2)+"Z":"")}function dsv(n){var u=new RegExp('["'+n+"\n\r]"),f=n.charCodeAt(0);function parse(r,e){var t,a,o=parseRows(r,(function(r,o){if(t)return t(r,o-1);a=r,t=e?customConverter(r,e):objectConverter(r)}));o.columns=a||[];return o}function parseRows(n,u){var i,s=[],c=n.length,l=0,d=0,m=c<=0,p=false;n.charCodeAt(c-1)===a&&--c;n.charCodeAt(c-1)===o&&--c;function token(){if(m)return e;if(p)return p=false,r;var u,i,s=l;if(n.charCodeAt(s)===t){while(l++=c)m=true;else if((i=n.charCodeAt(l++))===a)p=true;else if(i===o){p=true;n.charCodeAt(l)===a&&++l}return n.slice(s+1,u-1).replace(/""/g,'"')}while(l+t;function quadIn(t){return t*t}function quadOut(t){return t*(2-t)}function quadInOut(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}function cubicIn(t){return t*t*t}function cubicOut(t){return--t*t*t+1}function cubicInOut(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}var t=3;var n=function custom(t){t=+t;function polyIn(n){return Math.pow(n,t)}polyIn.exponent=custom;return polyIn}(t);var u=function custom(t){t=+t;function polyOut(n){return 1-Math.pow(1-n,t)}polyOut.exponent=custom;return polyOut}(t);var e=function custom(t){t=+t;function polyInOut(n){return((n*=2)<=1?Math.pow(n,t):2-Math.pow(2-n,t))/2}polyInOut.exponent=custom;return polyInOut}(t);var a=Math.PI,c=a/2;function sinIn(t){return 1===+t?1:1-Math.cos(t*c)}function sinOut(t){return Math.sin(t*c)}function sinInOut(t){return(1-Math.cos(a*t))/2}function tpmt(t){return 1.0009775171065494*(Math.pow(2,-10*t)-.0009765625)}function expIn(t){return tpmt(1-+t)}function expOut(t){return 1-tpmt(t)}function expInOut(t){return((t*=2)<=1?tpmt(1-t):2-tpmt(t-1))/2}function circleIn(t){return 1-Math.sqrt(1-t*t)}function circleOut(t){return Math.sqrt(1- --t*t)}function circleInOut(t){return((t*=2)<=1?1-Math.sqrt(1-t*t):Math.sqrt(1-(t-=2)*t)+1)/2}var s=4/11,r=6/11,o=8/11,i=3/4,O=9/11,I=10/11,p=15/16,f=21/22,l=63/64,m=1/s/s;function bounceIn(t){return 1-bounceOut(1-t)}function bounceOut(t){return(t=+t)text(t,e).then((t=>(new DOMParser).parseFromString(t,r)))}var s=parser("application/xml");var u=parser("text/html");var f=parser("image/svg+xml");export{blob,buffer,n as csv,dsv,u as html,image,json,f as svg,text,o as tsv,s as xml}; diff --git a/vendor/javascript/d3-force.js b/vendor/javascript/d3-force.js index ece9edd8..e1a17d2d 100644 --- a/vendor/javascript/d3-force.js +++ b/vendor/javascript/d3-force.js @@ -1,2 +1,4 @@ +// d3-force@3.0.0 downloaded from https://ga.jspm.io/npm:d3-force@3.0.0/src/index.js + import{quadtree as n}from"d3-quadtree";import{dispatch as t}from"d3-dispatch";import{timer as e}from"d3-timer";function center(n,t){var e,i=1;null==n&&(n=0);null==t&&(t=0);function force(){var r,o,f=e.length,c=0,a=0;for(r=0;ru+v||il+v||fa.index){var d=u-c.x-c.vx,p=l-c.y-c.vy,z=d*d+p*p;if(zn.r&&(n.r=n[t].r)}function initialize(){if(e){var n,r,o=e.length;i=new Array(o);for(n=0;n[c(n,t,i),n])));for(f=0,r=new Array(u);f(n=(i*n+r)%o)/o}function x$1(n){return n.x}function y$1(n){return n.y}var f=10,c=Math.PI*(3-Math.sqrt(5));function simulation(n){var i,r=1,o=.001,a=1-Math.pow(o,1/300),u=0,l=.6,s=new Map,g=e(step),h=t("tick","end"),v=lcg();null==n&&(n=[]);function step(){tick();h.call("tick",i);if(r1?(null==t?s.delete(n):s.set(n,initializeForce(t)),i):s.get(n)},find:function(t,e,i){var r,o,f,c,a,u=0,l=n.length;null==i?i=Infinity:i*=i;for(u=0;u1?(h.on(n,t),i):h.on(n)}}}function manyBody(){var t,e,i,r,o,f=constant(-30),c=1,a=Infinity,u=.81;function force(i){var o,f=t.length,c=n(t,x$1,y$1).visitAfter(accumulate);for(r=i,o=0;o=a)){if(n.data!==e||n.next){0===s&&(s=jiggle(i),v+=s*s);0===g&&(g=jiggle(i),v+=g*g);v=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)}function formatDecimalParts(t,r){if((i=(t=r?t.toExponential(r-1):t.toExponential()).indexOf("e"))<0)return null;var i,e=t.slice(0,i);return[e.length>1?e[0]+e.slice(2):e,+t.slice(i+1)]}function exponent(t){return t=formatDecimalParts(Math.abs(t)),t?t[1]:NaN}function formatGroup(t,r){return function(i,e){var n=i.length,a=[],o=0,c=t[0],f=0;while(n>0&&c>0){f+c+1>e&&(c=Math.max(1,e-f));a.push(i.substring(n-=c,n+c));if((f+=c+1)>e)break;c=t[o=(o+1)%t.length]}return a.reverse().join(r)}}function formatNumerals(t){return function(r){return r.replace(/[0-9]/g,(function(r){return t[+r]}))}}var t=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function formatSpecifier(r){if(!(i=t.exec(r)))throw new Error("invalid format: "+r);var i;return new FormatSpecifier({fill:i[1],align:i[2],sign:i[3],symbol:i[4],zero:i[5],width:i[6],comma:i[7],precision:i[8]&&i[8].slice(1),trim:i[9],type:i[10]})}formatSpecifier.prototype=FormatSpecifier.prototype;function FormatSpecifier(t){this.fill=void 0===t.fill?" ":t.fill+"";this.align=void 0===t.align?">":t.align+"";this.sign=void 0===t.sign?"-":t.sign+"";this.symbol=void 0===t.symbol?"":t.symbol+"";this.zero=!!t.zero;this.width=void 0===t.width?void 0:+t.width;this.comma=!!t.comma;this.precision=void 0===t.precision?void 0:+t.precision;this.trim=!!t.trim;this.type=void 0===t.type?"":t.type+""}FormatSpecifier.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};function formatTrim(t){t:for(var r,i=t.length,e=1,n=-1;e0&&(n=0);break}return n>0?t.slice(0,n)+t.slice(r+1):t}var r;function formatPrefixAuto(t,i){var e=formatDecimalParts(t,i);if(!e)return t+"";var n=e[0],a=e[1],o=a-(r=3*Math.max(-8,Math.min(8,Math.floor(a/3))))+1,c=n.length;return o===c?n:o>c?n+new Array(o-c+1).join("0"):o>0?n.slice(0,o)+"."+n.slice(o):"0."+new Array(1-o).join("0")+formatDecimalParts(t,Math.max(0,i+o-1))[0]}function formatRounded(t,r){var i=formatDecimalParts(t,r);if(!i)return t+"";var e=i[0],n=i[1];return n<0?"0."+new Array(-n).join("0")+e:e.length>n+1?e.slice(0,n+1)+"."+e.slice(n+1):e+new Array(n-e.length+2).join("0")}var i={"%":(t,r)=>(100*t).toFixed(r),b:t=>Math.round(t).toString(2),c:t=>t+"",d:formatDecimal,e:(t,r)=>t.toExponential(r),f:(t,r)=>t.toFixed(r),g:(t,r)=>t.toPrecision(r),o:t=>Math.round(t).toString(8),p:(t,r)=>formatRounded(100*t,r),r:formatRounded,s:formatPrefixAuto,X:t=>Math.round(t).toString(16).toUpperCase(),x:t=>Math.round(t).toString(16)};function identity(t){return t}var e=Array.prototype.map,n=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function formatLocale(t){var a=void 0===t.grouping||void 0===t.thousands?identity:formatGroup(e.call(t.grouping,Number),t.thousands+""),o=void 0===t.currency?"":t.currency[0]+"",c=void 0===t.currency?"":t.currency[1]+"",f=void 0===t.decimal?".":t.decimal+"",s=void 0===t.numerals?identity:formatNumerals(e.call(t.numerals,String)),m=void 0===t.percent?"%":t.percent+"",l=void 0===t.minus?"−":t.minus+"",u=void 0===t.nan?"NaN":t.nan+"";function newFormat(t){t=formatSpecifier(t);var e=t.fill,h=t.align,p=t.sign,d=t.symbol,g=t.zero,v=t.width,x=t.comma,y=t.precision,M=t.trim,b=t.type;"n"===b?(x=true,b="g"):i[b]||(void 0===y&&(y=12),M=true,b="g");(g||"0"===e&&"="===h)&&(g=true,e="0",h="=");var w="$"===d?o:"#"===d&&/[boxX]/.test(b)?"0"+b.toLowerCase():"",S="$"===d?c:/[%p]/.test(b)?m:"";var P=i[b],F=/[defgprs%]/.test(b);y=void 0===y?6:/[gprs]/.test(b)?Math.max(1,Math.min(21,y)):Math.max(0,Math.min(20,y));function format(t){var i,o,c,m=w,d=S;if("c"===b){d=P(t)+d;t=""}else{t=+t;var k=t<0||1/t<0;t=isNaN(t)?u:P(Math.abs(t),y);M&&(t=formatTrim(t));k&&0===+t&&"+"!==p&&(k=false);m=(k?"("===p?p:l:"-"===p||"("===p?"":p)+m;d=("s"===b?n[8+r/3]:"")+d+(k&&"("===p?")":"");if(F){i=-1,o=t.length;while(++ic||c>57){d=(46===c?f+t.slice(i+1):t.slice(i))+d;t=t.slice(0,i);break}}}x&&!g&&(t=a(t,Infinity));var A=m.length+t.length+d.length,L=A>1)+m+t+d+L.slice(A);break;default:t=L+m+t+d;break}return s(t)}format.toString=function(){return t+""};return format}function formatPrefix(t,r){var i=newFormat((t=formatSpecifier(t),t.type="f",t)),e=3*Math.max(-8,Math.min(8,Math.floor(exponent(r)/3))),a=Math.pow(10,-e),o=n[8+e/3];return function(t){return i(a*t)+o}}return{format:newFormat,formatPrefix:formatPrefix}}var a;var o;var c;defaultLocale({thousands:",",grouping:[3],currency:["$",""]});function defaultLocale(t){a=formatLocale(t);o=a.format;c=a.formatPrefix;return a}function precisionFixed(t){return Math.max(0,-exponent(Math.abs(t)))}function precisionPrefix(t,r){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(exponent(r)/3)))-exponent(Math.abs(t)))}function precisionRound(t,r){t=Math.abs(t),r=Math.abs(r)-t;return Math.max(0,exponent(r)-exponent(t))+1}export{FormatSpecifier,o as format,defaultLocale as formatDefaultLocale,formatLocale,c as formatPrefix,formatSpecifier,precisionFixed,precisionPrefix,precisionRound}; diff --git a/vendor/javascript/d3-geo.js b/vendor/javascript/d3-geo.js index 016b8564..fb3cbb77 100644 --- a/vendor/javascript/d3-geo.js +++ b/vendor/javascript/d3-geo.js @@ -1,2 +1,4 @@ -import{Adder as n,merge as t,range as r}from"d3-array";var e=1e-6;var i=1e-12;var o=Math.PI;var a=o/2;var c=o/4;var u=2*o;var l=180/o;var s=o/180;var f=Math.abs;var p=Math.atan;var g=Math.atan2;var h=Math.cos;var d=Math.ceil;var v=Math.exp;Math.floor;var m=Math.hypot;var E=Math.log;var S=Math.pow;var y=Math.sin;var R=Math.sign||function(n){return n>0?1:n<0?-1:0};var w=Math.sqrt;var P=Math.tan;function acos(n){return n>1?0:n<-1?o:Math.acos(n)}function asin(n){return n>1?a:n<-1?-a:Math.asin(n)}function haversin(n){return(n=y(n/2))*n}function noop(){}function streamGeometry(n,t){n&&M.hasOwnProperty(n.type)&&M[n.type](n,t)}var j={Feature:function(n,t){streamGeometry(n.geometry,t)},FeatureCollection:function(n,t){var r=n.features,e=-1,i=r.length;while(++e=0?1:-1,i=e*r,o=h(t),a=y(t),u=$*a,l=q*o+u*h(i),f=u*e*y(i);b.add(g(f,l));C=n,q=o,$=a}function area(t){_=new n;geoStream(t,N);return 2*_}function spherical(n){return[g(n[1],n[0]),asin(n[2])]}function cartesian(n){var t=n[0],r=n[1],e=h(r);return[e*h(t),e*y(t),y(r)]}function cartesianDot(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function cartesianCross(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function cartesianAddInPlace(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function cartesianScale(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function cartesianNormalizeInPlace(n){var t=w(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}var I,A,z,F,T,U,G,k,H,W,D;var O={point:boundsPoint$1,lineStart:boundsLineStart,lineEnd:boundsLineEnd,polygonStart:function(){O.point=boundsRingPoint;O.lineStart=boundsRingStart;O.lineEnd=boundsRingEnd;H=new n;N.polygonStart()},polygonEnd:function(){N.polygonEnd();O.point=boundsPoint$1;O.lineStart=boundsLineStart;O.lineEnd=boundsLineEnd;b<0?(I=-(z=180),A=-(F=90)):H>e?F=90:H<-e&&(A=-90);D[0]=I,D[1]=z},sphere:function(){I=-(z=180),A=-(F=90)}};function boundsPoint$1(n,t){W.push(D=[I=n,z=n]);tF&&(F=t)}function linePoint(n,t){var r=cartesian([n*s,t*s]);if(k){var e=cartesianCross(k,r),i=[e[1],-e[0],0],o=cartesianCross(i,e);cartesianNormalizeInPlace(o);o=spherical(o);var a,c=n-T,u=c>0?1:-1,p=o[0]*l*u,g=f(c)>180;if(g^(u*TF&&(F=a)}else if(p=(p+360)%360-180,g^(u*TF&&(F=t)}if(g)nangle(I,z)&&(z=n):angle(n,z)>angle(I,z)&&(I=n);else if(z>=I){nz&&(z=n)}else n>T?angle(I,n)>angle(I,z)&&(z=n):angle(n,z)>angle(I,z)&&(I=n)}else W.push(D=[I=n,z=n]);tF&&(F=t);k=r,T=n}function boundsLineStart(){O.point=linePoint}function boundsLineEnd(){D[0]=I,D[1]=z;O.point=boundsPoint$1;k=null}function boundsRingPoint(n,t){if(k){var r=n-T;H.add(f(r)>180?r+(r>0?360:-360):r)}else U=n,G=t;N.point(n,t);linePoint(n,t)}function boundsRingStart(){N.lineStart()}function boundsRingEnd(){boundsRingPoint(U,G);N.lineEnd();f(H)>e&&(I=-(z=180));D[0]=I,D[1]=z;k=null}function angle(n,t){return(t-=n)<0?t+360:t}function rangeCompare(n,t){return n[0]-t[0]}function rangeContains(n,t){return n[0]<=n[1]?n[0]<=t&&t<=n[1]:tangle(e[0],e[1])&&(e[1]=i[1]);angle(i[0],e[1])>angle(e[0],e[1])&&(e[0]=i[0])}else o.push(e=i)}for(a=-Infinity,r=o.length-1,t=0,e=o[r];t<=r;e=i,++t){i=o[t];(c=angle(e[1],i[0]))>a&&(a=c,I=i[0],z=e[1])}}W=D=null;return Infinity===I||Infinity===A?[[NaN,NaN],[NaN,NaN]]:[[I,A],[z,F]]}var X,Y,B,Z,J,K,Q,V,nn,tn,rn,en,on,an,cn,un;var ln={sphere:noop,point:centroidPoint$1,lineStart:centroidLineStart$1,lineEnd:centroidLineEnd$1,polygonStart:function(){ln.lineStart=centroidRingStart$1;ln.lineEnd=centroidRingEnd$1},polygonEnd:function(){ln.lineStart=centroidLineStart$1;ln.lineEnd=centroidLineEnd$1}};function centroidPoint$1(n,t){n*=s,t*=s;var r=h(t);centroidPointCartesian(r*h(n),r*y(n),y(t))}function centroidPointCartesian(n,t,r){++X;B+=(n-B)/X;Z+=(t-Z)/X;J+=(r-J)/X}function centroidLineStart$1(){ln.point=centroidLinePointFirst}function centroidLinePointFirst(n,t){n*=s,t*=s;var r=h(t);an=r*h(n);cn=r*y(n);un=y(t);ln.point=centroidLinePoint;centroidPointCartesian(an,cn,un)}function centroidLinePoint(n,t){n*=s,t*=s;var r=h(t),e=r*h(n),i=r*y(n),o=y(t),a=g(w((a=cn*o-un*i)*a+(a=un*e-an*o)*a+(a=an*i-cn*e)*a),an*e+cn*i+un*o);Y+=a;K+=a*(an+(an=e));Q+=a*(cn+(cn=i));V+=a*(un+(un=o));centroidPointCartesian(an,cn,un)}function centroidLineEnd$1(){ln.point=centroidPoint$1}function centroidRingStart$1(){ln.point=centroidRingPointFirst}function centroidRingEnd$1(){centroidRingPoint(en,on);ln.point=centroidPoint$1}function centroidRingPointFirst(n,t){en=n,on=t;n*=s,t*=s;ln.point=centroidRingPoint;var r=h(t);an=r*h(n);cn=r*y(n);un=y(t);centroidPointCartesian(an,cn,un)}function centroidRingPoint(n,t){n*=s,t*=s;var r=h(t),e=r*h(n),i=r*y(n),o=y(t),a=cn*o-un*i,c=un*e-an*o,u=an*i-cn*e,l=m(a,c,u),f=asin(l),p=l&&-f/l;nn.add(p*a);tn.add(p*c);rn.add(p*u);Y+=f;K+=f*(an+(an=e));Q+=f*(cn+(cn=i));V+=f*(un+(un=o));centroidPointCartesian(an,cn,un)}function centroid(t){X=Y=B=Z=J=K=Q=V=0;nn=new n;tn=new n;rn=new n;geoStream(t,ln);var r=+nn,o=+tn,a=+rn,c=m(r,o,a);if(co&&(n-=Math.round(n/u)*u);return[n,t]}rotationIdentity.invert=rotationIdentity;function rotateRadians(n,t,r){return(n%=u)?t||r?compose(rotationLambda(n),rotationPhiGamma(t,r)):rotationLambda(n):t||r?rotationPhiGamma(t,r):rotationIdentity}function forwardRotationLambda(n){return function(t,r){t+=n;f(t)>o&&(t-=Math.round(t/u)*u);return[t,r]}}function rotationLambda(n){var t=forwardRotationLambda(n);t.invert=forwardRotationLambda(-n);return t}function rotationPhiGamma(n,t){var r=h(n),e=y(n),i=h(t),o=y(t);function rotation(n,t){var a=h(t),c=h(n)*a,u=y(n)*a,l=y(t),s=l*r+c*e;return[g(u*i-s*o,c*r-l*e),asin(s*i+u*o)]}rotation.invert=function(n,t){var a=h(t),c=h(n)*a,u=y(n)*a,l=y(t),s=l*i-u*o;return[g(u*i+l*o,c*r+s*e),asin(s*r-c*e)]};return rotation}function rotation(n){n=rotateRadians(n[0]*s,n[1]*s,n.length>2?n[2]*s:0);function forward(t){t=n(t[0]*s,t[1]*s);return t[0]*=l,t[1]*=l,t}forward.invert=function(t){t=n.invert(t[0]*s,t[1]*s);return t[0]*=l,t[1]*=l,t};return forward}function circleStream(n,t,r,e,i,o){if(r){var a=h(t),c=y(t),l=e*r;if(null==i){i=t+e*u;o=t-l/2}else{i=circleRadius(a,i);o=circleRadius(a,o);(e>0?io)&&(i+=e*u)}for(var s,f=i;e>0?f>o:f1&&t.push(t.pop().concat(t.shift()))},result:function(){var r=t;t=[];n=null;return r}}}function pointEqual(n,t){return f(n[0]-t[0])=0;--a)o.point((f=s[a])[0],f[1])}else i(g.x,g.p.x,-1,o);g=g.p}g=g.o;s=g.z;h=!h}while(!g.v);o.lineEnd()}}}function link(n){if(t=n.length){var t,r,e=0,i=n[0];while(++e=0?1:-1,z=A*I,F=z>o,T=b*_;m.add(g(T*A*y(z),L*N+T*h(z)));d+=F?I+A*u:I;if(F^j>=l^q>=l){var U=cartesianCross(cartesian(P),cartesian(C));cartesianNormalizeInPlace(U);var G=cartesianCross(p,U);cartesianNormalizeInPlace(G);var k=(F^I>=0?-1:1)*asin(G[2]);(s>k||s===k&&(U[0]||U[1]))&&(v+=F^I>=0?1:-1)}}}return(d<-e||d0){p||(o.polygonStart(),p=true);o.lineStart();for(n=0;n1&&2&i&&l.push(l.pop().concat(l.shift()));c.push(l.filter(validSegment))}}return g}}function validSegment(n){return n.length>1}function compareIntersection(n,t){return((n=n.x)[0]<0?n[1]-a-e:a-n[1])-((t=t.x)[0]<0?t[1]-a-e:a-t[1])}var sn=clip((function(){return true}),clipAntimeridianLine,clipAntimeridianInterpolate,[-o,-a]);function clipAntimeridianLine(n){var t,r=NaN,i=NaN,c=NaN;return{lineStart:function(){n.lineStart();t=1},point:function(u,l){var s=u>0?o:-o,p=f(u-r);if(f(p-o)0?a:-a);n.point(c,i);n.lineEnd();n.lineStart();n.point(s,i);n.point(u,i);t=0}else if(c!==s&&p>=o){f(r-c)e?p((y(t)*(a=h(i))*y(r)-y(i)*(o=h(t))*y(n))/(o*a*c)):(t+i)/2}function clipAntimeridianInterpolate(n,t,r,i){var c;if(null==n){c=r*a;i.point(-o,c);i.point(0,c);i.point(o,c);i.point(o,0);i.point(o,-c);i.point(0,-c);i.point(-o,-c);i.point(-o,0);i.point(-o,c)}else if(f(n[0]-t[0])>e){var u=n[0]0,a=f(t)>e;function interpolate(t,e,i,o){circleStream(o,n,r,i,t,e)}function visible(n,r){return h(n)*h(r)>t}function clipLine(n){var t,r,e,c,u;return{lineStart:function(){c=e=false;u=1},point:function(l,s){var f,p=[l,s],g=visible(l,s),h=i?g?0:code(l,s):g?code(l+(l<0?o:-o),s):0;!t&&(c=e=g)&&n.lineStart();if(g!==e){f=intersect(t,p);(!f||pointEqual(t,f)||pointEqual(p,f))&&(p[2]=1)}if(g!==e){u=0;if(g){n.lineStart();f=intersect(p,t);n.point(f[0],f[1])}else{f=intersect(t,p);n.point(f[0],f[1],2);n.lineEnd()}t=f}else if(a&&t&&i^g){var d;if(!(h&r)&&(d=intersect(p,t,true))){u=0;if(i){n.lineStart();n.point(d[0][0],d[0][1]);n.point(d[1][0],d[1][1]);n.lineEnd()}else{n.point(d[1][0],d[1][1]);n.lineEnd();n.lineStart();n.point(d[0][0],d[0][1],3)}}}!g||t&&pointEqual(t,p)||n.point(p[0],p[1]);t=p,e=g,r=h},lineEnd:function(){e&&n.lineEnd();t=null},clean:function(){return u|(c&&e)<<1}}}function intersect(n,r,i){var a=cartesian(n),c=cartesian(r);var u=[1,0,0],l=cartesianCross(a,c),s=cartesianDot(l,l),p=l[0],g=s-p*p;if(!g)return!i&&n;var h=t*s/g,d=-t*p/g,v=cartesianCross(u,l),m=cartesianScale(u,h),E=cartesianScale(l,d);cartesianAddInPlace(m,E);var S=v,y=cartesianDot(m,S),R=cartesianDot(S,S),P=y*y-R*(cartesianDot(m,m)-1);if(!(P<0)){var j=w(P),M=cartesianScale(S,(-y-j)/R);cartesianAddInPlace(M,m);M=spherical(M);if(!i)return M;var b,L=n[0],x=r[0],C=n[1],q=r[1];x0^M[1]<(f(M[0]-L)o^(L<=M[0]&&M[0]<=x)){var I=cartesianScale(S,(-y+j)/R);cartesianAddInPlace(I,m);return[M,spherical(I)]}}}function code(t,r){var e=i?n:o-n,a=0;t<-e?a|=1:t>e&&(a|=2);r<-e?a|=4:r>e&&(a|=8);return a}return clip(visible,clipLine,interpolate,i?[0,-n]:[-o,n-o])}function clipLine(n,t,r,e,i,o){var a,c=n[0],u=n[1],l=t[0],s=t[1],f=0,p=1,g=l-c,h=s-u;a=r-c;if(g||!(a>0)){a/=g;if(g<0){if(a0){if(a>p)return;a>f&&(f=a)}a=i-c;if(g||!(a<0)){a/=g;if(g<0){if(a>p)return;a>f&&(f=a)}else if(g>0){if(a0)){a/=h;if(h<0){if(a0){if(a>p)return;a>f&&(f=a)}a=o-u;if(h||!(a<0)){a/=h;if(h<0){if(a>p)return;a>f&&(f=a)}else if(h>0){if(a0&&(n[0]=c+f*g,n[1]=u+f*h);p<1&&(t[0]=c+p*g,t[1]=u+p*h);return true}}}}}var fn=1e9,pn=-fn;function clipRectangle(n,r,i,o){function visible(t,e){return n<=t&&t<=i&&r<=e&&e<=o}function interpolate(t,e,a,c){var u=0,l=0;if(null==t||(u=corner(t,a))!==(l=corner(e,a))||comparePoint(t,e)<0^a>0)do{c.point(0===u||3===u?n:i,u>1?o:r)}while((u=(u+a+4)%4)!==l);else c.point(e[0],e[1])}function corner(t,o){return f(t[0]-n)0?0:3:f(t[0]-i)0?2:1:f(t[1]-r)0?1:0:o>0?3:2}function compareIntersection(n,t){return comparePoint(n.x,t.x)}function comparePoint(n,t){var r=corner(n,1),e=corner(t,1);return r!==e?r-e:0===r?t[1]-n[1]:1===r?n[0]-t[0]:2===r?n[1]-t[1]:t[0]-n[0]}return function(e){var a,c,u,l,s,f,p,g,h,d,v,m=e,E=clipBuffer();var S={point:point,lineStart:lineStart,lineEnd:lineEnd,polygonStart:polygonStart,polygonEnd:polygonEnd};function point(n,t){visible(n,t)&&m.point(n,t)}function polygonInside(){var t=0;for(var r=0,e=c.length;ro&&(p-i)*(o-a)>(g-a)*(n-i)&&++t:g<=o&&(p-i)*(o-a)<(g-a)*(n-i)&&--t}return t}function polygonStart(){m=E,a=[],c=[],v=true}function polygonEnd(){var n=polygonInside(),r=v&&n,i=(a=t(a)).length;if(r||i){e.polygonStart();if(r){e.lineStart();interpolate(null,null,1,e);e.lineEnd()}i&&clipRejoin(a,compareIntersection,n,interpolate,e);e.polygonEnd()}m=e,a=c=u=null}function lineStart(){S.point=linePoint;c&&c.push(u=[]);d=true;h=false;p=g=NaN}function lineEnd(){if(a){linePoint(l,s);f&&h&&E.rejoin();a.push(E.result())}S.point=point;h&&m.lineEnd()}function linePoint(t,e){var a=visible(t,e);c&&u.push([t,e]);if(d){l=t,s=e,f=a;d=false;if(a){m.lineStart();m.point(t,e)}}else if(a&&h)m.point(t,e);else{var E=[p=Math.max(pn,Math.min(fn,p)),g=Math.max(pn,Math.min(fn,g))],S=[t=Math.max(pn,Math.min(fn,t)),e=Math.max(pn,Math.min(fn,e))];if(clipLine(E,S,n,r,i,o)){if(!h){m.lineStart();m.point(E[0],E[1])}m.point(S[0],S[1]);a||m.lineEnd();v=false}else if(a){m.lineStart();m.point(t,e);v=false}}p=t,g=e,h=a}return S}}function extent(){var n,t,r,e=0,i=0,o=960,a=500;return r={stream:function(r){return n&&t===r?n:n=clipRectangle(e,i,o,a)(t=r)},extent:function(c){return arguments.length?(e=+c[0][0],i=+c[0][1],o=+c[1][0],a=+c[1][1],n=t=null,r):[[e,i],[o,a]]}}}var gn,hn,dn,vn;var mn={sphere:noop,point:noop,lineStart:lengthLineStart,lineEnd:noop,polygonStart:noop,polygonEnd:noop};function lengthLineStart(){mn.point=lengthPointFirst$1;mn.lineEnd=lengthLineEnd}function lengthLineEnd(){mn.point=mn.lineEnd=noop}function lengthPointFirst$1(n,t){n*=s,t*=s;hn=n,dn=y(t),vn=h(t);mn.point=lengthPoint$1}function lengthPoint$1(n,t){n*=s,t*=s;var r=y(t),e=h(t),i=f(n-hn),o=h(i),a=y(i),c=e*a,u=vn*r-dn*e*o,l=dn*r+vn*e*o;gn.add(g(w(c*c+u*u),l));hn=n,dn=r,vn=e}function length(t){gn=new n;geoStream(t,mn);return+gn}var En=[null,null],Sn={type:"LineString",coordinates:En};function distance(n,t){En[0]=n;En[1]=t;return length(Sn)}var yn={Feature:function(n,t){return containsGeometry(n.geometry,t)},FeatureCollection:function(n,t){var r=n.features,e=-1,i=r.length;while(++e0){o=distance(n[a],n[a-1]);if(o>0&&r<=o&&e<=o&&(r+e-o)*(1-Math.pow((r-e)/o,2))e})).map(s)).concat(r(d(c/m)*m,a,m).filter((function(n){return f(n%S)>e})).map(p))}graticule.lines=function(){return lines().map((function(n){return{type:"LineString",coordinates:n}}))};graticule.outline=function(){return{type:"Polygon",coordinates:[g(o).concat(h(u).slice(1),g(i).reverse().slice(1),h(l).reverse().slice(1))]}};graticule.extent=function(n){return arguments.length?graticule.extentMajor(n).extentMinor(n):graticule.extentMinor()};graticule.extentMajor=function(n){if(!arguments.length)return[[o,l],[i,u]];o=+n[0][0],i=+n[1][0];l=+n[0][1],u=+n[1][1];o>i&&(n=o,o=i,i=n);l>u&&(n=l,l=u,u=n);return graticule.precision(y)};graticule.extentMinor=function(r){if(!arguments.length)return[[t,c],[n,a]];t=+r[0][0],n=+r[1][0];c=+r[0][1],a=+r[1][1];t>n&&(r=t,t=n,n=r);c>a&&(r=c,c=a,a=r);return graticule.precision(y)};graticule.step=function(n){return arguments.length?graticule.stepMajor(n).stepMinor(n):graticule.stepMinor()};graticule.stepMajor=function(n){if(!arguments.length)return[E,S];E=+n[0],S=+n[1];return graticule};graticule.stepMinor=function(n){if(!arguments.length)return[v,m];v=+n[0],m=+n[1];return graticule};graticule.precision=function(r){if(!arguments.length)return y;y=+r;s=graticuleX(c,a,90);p=graticuleY(t,n,y);g=graticuleX(l,u,90);h=graticuleY(o,i,y);return graticule};return graticule.extentMajor([[-180,-90+e],[180,90-e]]).extentMinor([[-180,-80-e],[180,80+e]])}function graticule10(){return graticule()()}function interpolate(n,t){var r=n[0]*s,e=n[1]*s,i=t[0]*s,o=t[1]*s,a=h(e),c=y(e),u=h(o),f=y(o),p=a*h(r),d=a*y(r),v=u*h(i),m=u*y(i),E=2*asin(w(haversin(o-e)+a*u*haversin(i-r))),S=y(E);var R=E?function(n){var t=y(n*=E)/S,r=y(E-n)/S,e=r*p+t*v,i=r*d+t*m,o=r*c+t*f;return[g(i,e)*l,g(o,w(e*e+i*i))*l]}:function(){return[r*l,e*l]};R.distance=E;return R}var identity$1=n=>n;var wn,Pn,jn,Mn,bn=new n,Ln=new n;var xn={point:noop,lineStart:noop,lineEnd:noop,polygonStart:function(){xn.lineStart=areaRingStart;xn.lineEnd=areaRingEnd},polygonEnd:function(){xn.lineStart=xn.lineEnd=xn.point=noop;bn.add(f(Ln));Ln=new n},result:function(){var t=bn/2;bn=new n;return t}};function areaRingStart(){xn.point=areaPointFirst}function areaPointFirst(n,t){xn.point=areaPoint;wn=jn=n,Pn=Mn=t}function areaPoint(n,t){Ln.add(Mn*n-jn*t);jn=n,Mn=t}function areaRingEnd(){areaPoint(wn,Pn)}var Cn=Infinity,qn=Cn,$n=-Cn,_n=$n;var Nn={point:boundsPoint,lineStart:noop,lineEnd:noop,polygonStart:noop,polygonEnd:noop,result:function(){var n=[[Cn,qn],[$n,_n]];$n=_n=-(qn=Cn=Infinity);return n}};function boundsPoint(n,t){n$n&&($n=n);t_n&&(_n=t)}var In,An,zn,Fn,Tn=0,Un=0,Gn=0,kn=0,Hn=0,Wn=0,Dn=0,On=0,Xn=0;var Yn={point:centroidPoint,lineStart:centroidLineStart,lineEnd:centroidLineEnd,polygonStart:function(){Yn.lineStart=centroidRingStart;Yn.lineEnd=centroidRingEnd},polygonEnd:function(){Yn.point=centroidPoint;Yn.lineStart=centroidLineStart;Yn.lineEnd=centroidLineEnd},result:function(){var n=Xn?[Dn/Xn,On/Xn]:Wn?[kn/Wn,Hn/Wn]:Gn?[Tn/Gn,Un/Gn]:[NaN,NaN];Tn=Un=Gn=kn=Hn=Wn=Dn=On=Xn=0;return n}};function centroidPoint(n,t){Tn+=n;Un+=t;++Gn}function centroidLineStart(){Yn.point=centroidPointFirstLine}function centroidPointFirstLine(n,t){Yn.point=centroidPointLine;centroidPoint(zn=n,Fn=t)}function centroidPointLine(n,t){var r=n-zn,e=t-Fn,i=w(r*r+e*e);kn+=i*(zn+n)/2;Hn+=i*(Fn+t)/2;Wn+=i;centroidPoint(zn=n,Fn=t)}function centroidLineEnd(){Yn.point=centroidPoint}function centroidRingStart(){Yn.point=centroidPointFirstRing}function centroidRingEnd(){centroidPointRing(In,An)}function centroidPointFirstRing(n,t){Yn.point=centroidPointRing;centroidPoint(In=zn=n,An=Fn=t)}function centroidPointRing(n,t){var r=n-zn,e=t-Fn,i=w(r*r+e*e);kn+=i*(zn+n)/2;Hn+=i*(Fn+t)/2;Wn+=i;i=Fn*n-zn*t;Dn+=i*(zn+n);On+=i*(Fn+t);Xn+=3*i;centroidPoint(zn=n,Fn=t)}function PathContext(n){this._context=n}PathContext.prototype={_radius:4.5,pointRadius:function(n){return this._radius=n,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath();this._point=NaN},point:function(n,t){switch(this._point){case 0:this._context.moveTo(n,t);this._point=1;break;case 1:this._context.lineTo(n,t);break;default:this._context.moveTo(n+this._radius,t);this._context.arc(n,t,this._radius,0,u);break}},result:noop};var Bn,Zn,Jn,Kn,Qn,Vn=new n;var nt={point:noop,lineStart:function(){nt.point=lengthPointFirst},lineEnd:function(){Bn&&lengthPoint(Zn,Jn);nt.point=noop},polygonStart:function(){Bn=true},polygonEnd:function(){Bn=null},result:function(){var t=+Vn;Vn=new n;return t}};function lengthPointFirst(n,t){nt.point=lengthPoint;Zn=Kn=n,Jn=Qn=t}function lengthPoint(n,t){Kn-=n,Qn-=t;Vn.add(w(Kn*Kn+Qn*Qn));Kn=n,Qn=t}let tt,rt,et,it;class PathString{constructor(n){this._append=null==n?append:appendRound(n);this._radius=4.5;this._=""}pointRadius(n){this._radius=+n;return this}polygonStart(){this._line=0}polygonEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){0===this._line&&(this._+="Z");this._point=NaN}point(n,t){switch(this._point){case 0:this._append`M${n},${t}`;this._point=1;break;case 1:this._append`L${n},${t}`;break;default:this._append`M${n},${t}`;if(this._radius!==et||this._append!==rt){const n=this._radius;const t=this._;this._="";this._append`m0,${n}a${n},${n} 0 1,1 0,${-2*n}a${n},${n} 0 1,1 0,${2*n}z`;et=n;rt=this._append;it=this._;this._=t}this._+=it;break}}result(){const n=this._;this._="";return n.length?n:null}}function append(n){let t=1;this._+=n[0];for(const r=n.length;t=0))throw new RangeError(`invalid digits: ${n}`);if(t>15)return append;if(t!==tt){const n=10**t;tt=t;rt=function append(t){let r=1;this._+=t[0];for(const e=t.length;r=0))throw new RangeError(`invalid digits: ${n}`);i=t}null===t&&(e=new PathString(i));return path};return path.projection(n).digits(i).context(t)}function transform(n){return{stream:transformer(n)}}function transformer(n){return function(t){var r=new TransformStream;for(var e in n)r[e]=n[e];r.stream=t;return r}}function TransformStream(){}TransformStream.prototype={constructor:TransformStream,point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};function fit(n,t,r){var e=n.clipExtent&&n.clipExtent();n.scale(150).translate([0,0]);null!=e&&n.clipExtent(null);geoStream(r,n.stream(Nn));t(Nn.result());null!=e&&n.clipExtent(e);return n}function fitExtent(n,t,r){return fit(n,(function(r){var e=t[1][0]-t[0][0],i=t[1][1]-t[0][1],o=Math.min(e/(r[1][0]-r[0][0]),i/(r[1][1]-r[0][1])),a=+t[0][0]+(e-o*(r[1][0]+r[0][0]))/2,c=+t[0][1]+(i-o*(r[1][1]+r[0][1]))/2;n.scale(150*o).translate([a,c])}),r)}function fitSize(n,t,r){return fitExtent(n,[[0,0],t],r)}function fitWidth(n,t,r){return fit(n,(function(r){var e=+t,i=e/(r[1][0]-r[0][0]),o=(e-i*(r[1][0]+r[0][0]))/2,a=-i*r[0][1];n.scale(150*i).translate([o,a])}),r)}function fitHeight(n,t,r){return fit(n,(function(r){var e=+t,i=e/(r[1][1]-r[0][1]),o=-i*r[0][0],a=(e-i*(r[1][1]+r[0][1]))/2;n.scale(150*i).translate([o,a])}),r)}var ot=16,at=h(30*s);function resample(n,t){return+t?resample$1(n,t):resampleNone(n)}function resampleNone(n){return transformer({point:function(t,r){t=n(t,r);this.stream.point(t[0],t[1])}})}function resample$1(n,t){function resampleLineTo(r,i,o,a,c,u,l,s,p,h,d,v,m,E){var S=l-r,y=s-i,R=S*S+y*y;if(R>4*t&&m--){var P=a+h,j=c+d,M=u+v,b=w(P*P+j*j+M*M),L=asin(M/=b),x=f(f(M)-1)t||f((S*_+y*N)/R-.5)>.3||a*h+c*d+u*v2?n[2]%360*s:0,recenter()):[E*l,S*l,y*l]};projection.angle=function(n){return arguments.length?(R=n%360*s,recenter()):R*l};projection.reflectX=function(n){return arguments.length?(P=n?-1:1,recenter()):P<0};projection.reflectY=function(n){return arguments.length?(j=n?-1:1,recenter()):j<0};projection.precision=function(n){return arguments.length?(a=resample(c,C=n*n),reset()):w(C)};projection.fitExtent=function(n,t){return fitExtent(projection,n,t)};projection.fitSize=function(n,t){return fitSize(projection,n,t)};projection.fitWidth=function(n,t){return fitWidth(projection,n,t)};projection.fitHeight=function(n,t){return fitHeight(projection,n,t)};function recenter(){var n=scaleTranslateRotate(g,0,0,P,j,R).apply(null,t(v,m)),e=scaleTranslateRotate(g,h-n[0],d-n[1],P,j,R);r=rotateRadians(E,S,y);c=compose(t,e);u=compose(r,c);a=resample(c,C);return reset()}function reset(){f=p=null;return projection}return function(){t=n.apply(this,arguments);projection.invert=t.invert&&invert;return recenter()}}function conicProjection(n){var t=0,r=o/3,e=projectionMutator(n),i=e(t,r);i.parallels=function(n){return arguments.length?e(t=n[0]*s,r=n[1]*s):[t*l,r*l]};return i}function cylindricalEqualAreaRaw(n){var t=h(n);function forward(n,r){return[n*t,y(r)/t]}forward.invert=function(n,r){return[n/t,asin(r*t)]};return forward}function conicEqualAreaRaw(n,t){var r=y(n),i=(r+y(t))/2;if(f(i)=.12&&i<.234&&e>=-.425&&e<-.214?u:i>=.166&&i<.234&&e>=-.214&&e<-.115?l:c).invert(n)};albersUsa.stream=function(r){return n&&t===r?n:n=multiplex([c.stream(t=r),u.stream(r),l.stream(r)])};albersUsa.precision=function(n){if(!arguments.length)return c.precision();c.precision(n),u.precision(n),l.precision(n);return reset()};albersUsa.scale=function(n){if(!arguments.length)return c.scale();c.scale(n),u.scale(.35*n),l.scale(n);return albersUsa.translate(c.translate())};albersUsa.translate=function(n){if(!arguments.length)return c.translate();var t=c.scale(),a=+n[0],f=+n[1];r=c.translate(n).clipExtent([[a-.455*t,f-.238*t],[a+.455*t,f+.238*t]]).stream(s);i=u.translate([a-.307*t,f+.201*t]).clipExtent([[a-.425*t+e,f+.12*t+e],[a-.214*t-e,f+.234*t-e]]).stream(s);o=l.translate([a-.205*t,f+.212*t]).clipExtent([[a-.214*t+e,f+.166*t+e],[a-.115*t-e,f+.234*t-e]]).stream(s);return reset()};albersUsa.fitExtent=function(n,t){return fitExtent(albersUsa,n,t)};albersUsa.fitSize=function(n,t){return fitSize(albersUsa,n,t)};albersUsa.fitWidth=function(n,t){return fitWidth(albersUsa,n,t)};albersUsa.fitHeight=function(n,t){return fitHeight(albersUsa,n,t)};function reset(){n=t=null;return albersUsa}return albersUsa.scale(1070)}function azimuthalRaw(n){return function(t,r){var e=h(t),i=h(r),o=n(e*i);return Infinity===o?[2,0]:[o*i*y(t),o*y(r)]}}function azimuthalInvert(n){return function(t,r){var e=w(t*t+r*r),i=n(e),o=y(i),a=h(i);return[g(t*o,e*a),asin(e&&r*o/e)]}}var ut=azimuthalRaw((function(n){return w(2/(1+n))}));ut.invert=azimuthalInvert((function(n){return 2*asin(n/2)}));function azimuthalEqualArea(){return projection(ut).scale(124.75).clipAngle(179.999)}var lt=azimuthalRaw((function(n){return(n=acos(n))&&n/y(n)}));lt.invert=azimuthalInvert((function(n){return n}));function azimuthalEquidistant(){return projection(lt).scale(79.4188).clipAngle(179.999)}function mercatorRaw(n,t){return[n,E(P((a+t)/2))]}mercatorRaw.invert=function(n,t){return[n,2*p(v(t))-a]};function mercator(){return mercatorProjection(mercatorRaw).scale(961/u)}function mercatorProjection(n){var t,r,e,i=projection(n),a=i.center,c=i.scale,u=i.translate,l=i.clipExtent,s=null;i.scale=function(n){return arguments.length?(c(n),reclip()):c()};i.translate=function(n){return arguments.length?(u(n),reclip()):u()};i.center=function(n){return arguments.length?(a(n),reclip()):a()};i.clipExtent=function(n){return arguments.length?(null==n?s=t=r=e=null:(s=+n[0][0],t=+n[0][1],r=+n[1][0],e=+n[1][1]),reclip()):null==s?null:[[s,t],[r,e]]};function reclip(){var a=o*c(),u=i(rotation(i.rotate()).invert([0,0]));return l(null==s?[[u[0]-a,u[1]-a],[u[0]+a,u[1]+a]]:n===mercatorRaw?[[Math.max(u[0]-a,s),t],[Math.min(u[0]+a,r),e]]:[[s,Math.max(u[1]-a,t)],[r,Math.min(u[1]+a,e)]])}return reclip()}function tany(n){return P((a+n)/2)}function conicConformalRaw(n,t){var r=h(n),i=n===t?y(n):E(r/h(t))/E(tany(t)/tany(n)),c=r*S(tany(n),i)/i;if(!i)return mercatorRaw;function project(n,t){c>0?t<-a+e&&(t=-a+e):t>a-e&&(t=a-e);var r=c/S(tany(t),i);return[r*y(i*n),c-r*h(i*n)]}project.invert=function(n,t){var r=c-t,e=R(i)*w(n*n+r*r),u=g(n,f(r))*R(r);r*i<0&&(u-=o*R(n)*R(r));return[u/i,2*p(S(c/e,1/i))-a]};return project}function conicConformal(){return conicProjection(conicConformalRaw).scale(109.5).parallels([30,30])}function equirectangularRaw(n,t){return[n,t]}equirectangularRaw.invert=equirectangularRaw;function equirectangular(){return projection(equirectangularRaw).scale(152.63)}function conicEquidistantRaw(n,t){var r=h(n),i=n===t?y(n):(r-h(t))/(t-n),a=r/i+n;if(f(i)e&&--o>0);return[n/(.8707+(a=i*i)*(a*(a*a*a*(.003971-.001529*a)-.013791)-.131979)),i]};function naturalEarth1(){return projection(naturalEarth1Raw).scale(175.295)}function orthographicRaw(n,t){return[h(t)*y(n),y(t)]}orthographicRaw.invert=azimuthalInvert(asin);function orthographic(){return projection(orthographicRaw).scale(249.5).clipAngle(90+e)}function stereographicRaw(n,t){var r=h(t),e=1+h(n)*r;return[r*y(n)/e,y(t)/e]}stereographicRaw.invert=azimuthalInvert((function(n){return 2*p(n)}));function stereographic(){return projection(stereographicRaw).scale(250).clipAngle(142)}function transverseMercatorRaw(n,t){return[E(P((a+t)/2)),-n]}transverseMercatorRaw.invert=function(n,t){return[-t,2*p(v(n))-a]};function transverseMercator(){var n=mercatorProjection(transverseMercatorRaw),t=n.center,r=n.rotate;n.center=function(n){return arguments.length?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])};n.rotate=function(n){return arguments.length?r([n[0],n[1],n.length>2?n[2]+90:90]):(n=r(),[n[0],n[1],n[2]-90])};return r([0,0,90]).scale(159.155)}export{albers as geoAlbers,albersUsa as geoAlbersUsa,area as geoArea,azimuthalEqualArea as geoAzimuthalEqualArea,ut as geoAzimuthalEqualAreaRaw,azimuthalEquidistant as geoAzimuthalEquidistant,lt as geoAzimuthalEquidistantRaw,bounds as geoBounds,centroid as geoCentroid,circle as geoCircle,sn as geoClipAntimeridian,clipCircle as geoClipCircle,extent as geoClipExtent,clipRectangle as geoClipRectangle,conicConformal as geoConicConformal,conicConformalRaw as geoConicConformalRaw,conicEqualArea as geoConicEqualArea,conicEqualAreaRaw as geoConicEqualAreaRaw,conicEquidistant as geoConicEquidistant,conicEquidistantRaw as geoConicEquidistantRaw,contains as geoContains,distance as geoDistance,equalEarth as geoEqualEarth,equalEarthRaw as geoEqualEarthRaw,equirectangular as geoEquirectangular,equirectangularRaw as geoEquirectangularRaw,gnomonic as geoGnomonic,gnomonicRaw as geoGnomonicRaw,graticule as geoGraticule,graticule10 as geoGraticule10,identity as geoIdentity,interpolate as geoInterpolate,length as geoLength,mercator as geoMercator,mercatorRaw as geoMercatorRaw,naturalEarth1 as geoNaturalEarth1,naturalEarth1Raw as geoNaturalEarth1Raw,orthographic as geoOrthographic,orthographicRaw as geoOrthographicRaw,index as geoPath,projection as geoProjection,projectionMutator as geoProjectionMutator,rotation as geoRotation,stereographic as geoStereographic,stereographicRaw as geoStereographicRaw,geoStream,transform as geoTransform,transverseMercator as geoTransverseMercator,transverseMercatorRaw as geoTransverseMercatorRaw}; +// d3-geo@3.1.1 downloaded from https://ga.jspm.io/npm:d3-geo@3.1.1/src/index.js + +import{Adder as n,merge as t,range as r}from"d3-array";var e=1e-6;var i=1e-12;var o=Math.PI;var a=o/2;var c=o/4;var u=o*2;var l=180/o;var s=o/180;var f=Math.abs;var p=Math.atan;var g=Math.atan2;var h=Math.cos;var d=Math.ceil;var v=Math.exp;Math.floor;var m=Math.hypot;var E=Math.log;var S=Math.pow;var y=Math.sin;var R=Math.sign||function(n){return n>0?1:n<0?-1:0};var w=Math.sqrt;var P=Math.tan;function acos(n){return n>1?0:n<-1?o:Math.acos(n)}function asin(n){return n>1?a:n<-1?-a:Math.asin(n)}function haversin(n){return(n=y(n/2))*n}function noop(){}function streamGeometry(n,t){n&&M.hasOwnProperty(n.type)&&M[n.type](n,t)}var j={Feature:function(n,t){streamGeometry(n.geometry,t)},FeatureCollection:function(n,t){var r=n.features,e=-1,i=r.length;while(++e=0?1:-1,i=e*r,o=h(t),a=y(t),u=$*a,l=q*o+u*h(i),f=u*e*y(i);b.add(g(f,l));C=n,q=o,$=a}function area(t){_=new n;geoStream(t,N);return _*2}function spherical(n){return[g(n[1],n[0]),asin(n[2])]}function cartesian(n){var t=n[0],r=n[1],e=h(r);return[e*h(t),e*y(t),y(r)]}function cartesianDot(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function cartesianCross(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function cartesianAddInPlace(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function cartesianScale(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function cartesianNormalizeInPlace(n){var t=w(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}var I,A,z,F,T,U,G,k,H,W,D;var O={point:boundsPoint$1,lineStart:boundsLineStart,lineEnd:boundsLineEnd,polygonStart:function(){O.point=boundsRingPoint;O.lineStart=boundsRingStart;O.lineEnd=boundsRingEnd;H=new n;N.polygonStart()},polygonEnd:function(){N.polygonEnd();O.point=boundsPoint$1;O.lineStart=boundsLineStart;O.lineEnd=boundsLineEnd;b<0?(I=-(z=180),A=-(F=90)):H>e?F=90:H<-e&&(A=-90);D[0]=I,D[1]=z},sphere:function(){I=-(z=180),A=-(F=90)}};function boundsPoint$1(n,t){W.push(D=[I=n,z=n]);tF&&(F=t)}function linePoint(n,t){var r=cartesian([n*s,t*s]);if(k){var e=cartesianCross(k,r),i=[e[1],-e[0],0],o=cartesianCross(i,e);cartesianNormalizeInPlace(o);o=spherical(o);var a,c=n-T,u=c>0?1:-1,p=o[0]*l*u,g=f(c)>180;if(g^(u*TF&&(F=a)}else if(p=(p+360)%360-180,g^(u*TF&&(F=t)}if(g)nangle(I,z)&&(z=n):angle(n,z)>angle(I,z)&&(I=n);else if(z>=I){nz&&(z=n)}else n>T?angle(I,n)>angle(I,z)&&(z=n):angle(n,z)>angle(I,z)&&(I=n)}else W.push(D=[I=n,z=n]);tF&&(F=t);k=r,T=n}function boundsLineStart(){O.point=linePoint}function boundsLineEnd(){D[0]=I,D[1]=z;O.point=boundsPoint$1;k=null}function boundsRingPoint(n,t){if(k){var r=n-T;H.add(f(r)>180?r+(r>0?360:-360):r)}else U=n,G=t;N.point(n,t);linePoint(n,t)}function boundsRingStart(){N.lineStart()}function boundsRingEnd(){boundsRingPoint(U,G);N.lineEnd();f(H)>e&&(I=-(z=180));D[0]=I,D[1]=z;k=null}function angle(n,t){return(t-=n)<0?t+360:t}function rangeCompare(n,t){return n[0]-t[0]}function rangeContains(n,t){return n[0]<=n[1]?n[0]<=t&&t<=n[1]:tangle(e[0],e[1])&&(e[1]=i[1]);angle(i[0],e[1])>angle(e[0],e[1])&&(e[0]=i[0])}else o.push(e=i)}for(a=-Infinity,r=o.length-1,t=0,e=o[r];t<=r;e=i,++t){i=o[t];(c=angle(e[1],i[0]))>a&&(a=c,I=i[0],z=e[1])}}W=D=null;return I===Infinity||A===Infinity?[[NaN,NaN],[NaN,NaN]]:[[I,A],[z,F]]}var X,Y,B,Z,J,K,Q,V,nn,tn,rn,en,on,an,cn,un;var ln={sphere:noop,point:centroidPoint$1,lineStart:centroidLineStart$1,lineEnd:centroidLineEnd$1,polygonStart:function(){ln.lineStart=centroidRingStart$1;ln.lineEnd=centroidRingEnd$1},polygonEnd:function(){ln.lineStart=centroidLineStart$1;ln.lineEnd=centroidLineEnd$1}};function centroidPoint$1(n,t){n*=s,t*=s;var r=h(t);centroidPointCartesian(r*h(n),r*y(n),y(t))}function centroidPointCartesian(n,t,r){++X;B+=(n-B)/X;Z+=(t-Z)/X;J+=(r-J)/X}function centroidLineStart$1(){ln.point=centroidLinePointFirst}function centroidLinePointFirst(n,t){n*=s,t*=s;var r=h(t);an=r*h(n);cn=r*y(n);un=y(t);ln.point=centroidLinePoint;centroidPointCartesian(an,cn,un)}function centroidLinePoint(n,t){n*=s,t*=s;var r=h(t),e=r*h(n),i=r*y(n),o=y(t),a=g(w((a=cn*o-un*i)*a+(a=un*e-an*o)*a+(a=an*i-cn*e)*a),an*e+cn*i+un*o);Y+=a;K+=a*(an+(an=e));Q+=a*(cn+(cn=i));V+=a*(un+(un=o));centroidPointCartesian(an,cn,un)}function centroidLineEnd$1(){ln.point=centroidPoint$1}function centroidRingStart$1(){ln.point=centroidRingPointFirst}function centroidRingEnd$1(){centroidRingPoint(en,on);ln.point=centroidPoint$1}function centroidRingPointFirst(n,t){en=n,on=t;n*=s,t*=s;ln.point=centroidRingPoint;var r=h(t);an=r*h(n);cn=r*y(n);un=y(t);centroidPointCartesian(an,cn,un)}function centroidRingPoint(n,t){n*=s,t*=s;var r=h(t),e=r*h(n),i=r*y(n),o=y(t),a=cn*o-un*i,c=un*e-an*o,u=an*i-cn*e,l=m(a,c,u),f=asin(l),p=l&&-f/l;nn.add(p*a);tn.add(p*c);rn.add(p*u);Y+=f;K+=f*(an+(an=e));Q+=f*(cn+(cn=i));V+=f*(un+(un=o));centroidPointCartesian(an,cn,un)}function centroid(t){X=Y=B=Z=J=K=Q=V=0;nn=new n;tn=new n;rn=new n;geoStream(t,ln);var r=+nn,o=+tn,a=+rn,c=m(r,o,a);if(co&&(n-=Math.round(n/u)*u);return[n,t]}rotationIdentity.invert=rotationIdentity;function rotateRadians(n,t,r){return(n%=u)?t||r?compose(rotationLambda(n),rotationPhiGamma(t,r)):rotationLambda(n):t||r?rotationPhiGamma(t,r):rotationIdentity}function forwardRotationLambda(n){return function(t,r){t+=n;f(t)>o&&(t-=Math.round(t/u)*u);return[t,r]}}function rotationLambda(n){var t=forwardRotationLambda(n);t.invert=forwardRotationLambda(-n);return t}function rotationPhiGamma(n,t){var r=h(n),e=y(n),i=h(t),o=y(t);function rotation(n,t){var a=h(t),c=h(n)*a,u=y(n)*a,l=y(t),s=l*r+c*e;return[g(u*i-s*o,c*r-l*e),asin(s*i+u*o)]}rotation.invert=function(n,t){var a=h(t),c=h(n)*a,u=y(n)*a,l=y(t),s=l*i-u*o;return[g(u*i+l*o,c*r+s*e),asin(s*r-c*e)]};return rotation}function rotation(n){n=rotateRadians(n[0]*s,n[1]*s,n.length>2?n[2]*s:0);function forward(t){t=n(t[0]*s,t[1]*s);return t[0]*=l,t[1]*=l,t}forward.invert=function(t){t=n.invert(t[0]*s,t[1]*s);return t[0]*=l,t[1]*=l,t};return forward}function circleStream(n,t,r,e,i,o){if(r){var a=h(t),c=y(t),l=e*r;if(i==null){i=t+e*u;o=t-l/2}else{i=circleRadius(a,i);o=circleRadius(a,o);(e>0?io)&&(i+=e*u)}for(var s,f=i;e>0?f>o:f1&&t.push(t.pop().concat(t.shift()))},result:function(){var r=t;t=[];n=null;return r}}}function pointEqual(n,t){return f(n[0]-t[0])=0;--a)o.point((f=s[a])[0],f[1])}else i(g.x,g.p.x,-1,o);g=g.p}g=g.o;s=g.z;h=!h}while(!g.v);o.lineEnd()}}}function link(n){if(t=n.length){var t,r,e=0,i=n[0];while(++e=0?1:-1,z=A*I,F=z>o,T=b*_;m.add(g(T*A*y(z),L*N+T*h(z)));d+=F?I+A*u:I;if(F^j>=l^q>=l){var U=cartesianCross(cartesian(P),cartesian(C));cartesianNormalizeInPlace(U);var G=cartesianCross(p,U);cartesianNormalizeInPlace(G);var k=(F^I>=0?-1:1)*asin(G[2]);(s>k||s===k&&(U[0]||U[1]))&&(v+=F^I>=0?1:-1)}}}return(d<-e||d0){p||(o.polygonStart(),p=true);o.lineStart();for(n=0;n1&&i&2&&l.push(l.pop().concat(l.shift()));c.push(l.filter(validSegment))}}return g}}function validSegment(n){return n.length>1}function compareIntersection(n,t){return((n=n.x)[0]<0?n[1]-a-e:a-n[1])-((t=t.x)[0]<0?t[1]-a-e:a-t[1])}var sn=clip((function(){return true}),clipAntimeridianLine,clipAntimeridianInterpolate,[-o,-a]);function clipAntimeridianLine(n){var t,r=NaN,i=NaN,c=NaN;return{lineStart:function(){n.lineStart();t=1},point:function(u,l){var s=u>0?o:-o,p=f(u-r);if(f(p-o)0?a:-a);n.point(c,i);n.lineEnd();n.lineStart();n.point(s,i);n.point(u,i);t=0}else if(c!==s&&p>=o){f(r-c)e?p((y(t)*(a=h(i))*y(r)-y(i)*(o=h(t))*y(n))/(o*a*c)):(t+i)/2}function clipAntimeridianInterpolate(n,t,r,i){var c;if(n==null){c=r*a;i.point(-o,c);i.point(0,c);i.point(o,c);i.point(o,0);i.point(o,-c);i.point(0,-c);i.point(-o,-c);i.point(-o,0);i.point(-o,c)}else if(f(n[0]-t[0])>e){var u=n[0]0,a=f(t)>e;function interpolate(t,e,i,o){circleStream(o,n,r,i,t,e)}function visible(n,r){return h(n)*h(r)>t}function clipLine(n){var t,r,e,c,u;return{lineStart:function(){c=e=false;u=1},point:function(l,s){var f,p=[l,s],g=visible(l,s),h=i?g?0:code(l,s):g?code(l+(l<0?o:-o),s):0;!t&&(c=e=g)&&n.lineStart();if(g!==e){f=intersect(t,p);(!f||pointEqual(t,f)||pointEqual(p,f))&&(p[2]=1)}if(g!==e){u=0;if(g){n.lineStart();f=intersect(p,t);n.point(f[0],f[1])}else{f=intersect(t,p);n.point(f[0],f[1],2);n.lineEnd()}t=f}else if(a&&t&&i^g){var d;if(!(h&r)&&(d=intersect(p,t,true))){u=0;if(i){n.lineStart();n.point(d[0][0],d[0][1]);n.point(d[1][0],d[1][1]);n.lineEnd()}else{n.point(d[1][0],d[1][1]);n.lineEnd();n.lineStart();n.point(d[0][0],d[0][1],3)}}}!g||t&&pointEqual(t,p)||n.point(p[0],p[1]);t=p,e=g,r=h},lineEnd:function(){e&&n.lineEnd();t=null},clean:function(){return u|(c&&e)<<1}}}function intersect(n,r,i){var a=cartesian(n),c=cartesian(r);var u=[1,0,0],l=cartesianCross(a,c),s=cartesianDot(l,l),p=l[0],g=s-p*p;if(!g)return!i&&n;var h=t*s/g,d=-t*p/g,v=cartesianCross(u,l),m=cartesianScale(u,h),E=cartesianScale(l,d);cartesianAddInPlace(m,E);var S=v,y=cartesianDot(m,S),R=cartesianDot(S,S),P=y*y-R*(cartesianDot(m,m)-1);if(!(P<0)){var j=w(P),M=cartesianScale(S,(-y-j)/R);cartesianAddInPlace(M,m);M=spherical(M);if(!i)return M;var b,L=n[0],x=r[0],C=n[1],q=r[1];x0^M[1]<(f(M[0]-L)o^(L<=M[0]&&M[0]<=x)){var I=cartesianScale(S,(-y+j)/R);cartesianAddInPlace(I,m);return[M,spherical(I)]}}}function code(t,r){var e=i?n:o-n,a=0;t<-e?a|=1:t>e&&(a|=2);r<-e?a|=4:r>e&&(a|=8);return a}return clip(visible,clipLine,interpolate,i?[0,-n]:[-o,n-o])}function clipLine(n,t,r,e,i,o){var a,c=n[0],u=n[1],l=t[0],s=t[1],f=0,p=1,g=l-c,h=s-u;a=r-c;if(g||!(a>0)){a/=g;if(g<0){if(a0){if(a>p)return;a>f&&(f=a)}a=i-c;if(g||!(a<0)){a/=g;if(g<0){if(a>p)return;a>f&&(f=a)}else if(g>0){if(a0)){a/=h;if(h<0){if(a0){if(a>p)return;a>f&&(f=a)}a=o-u;if(h||!(a<0)){a/=h;if(h<0){if(a>p)return;a>f&&(f=a)}else if(h>0){if(a0&&(n[0]=c+f*g,n[1]=u+f*h);p<1&&(t[0]=c+p*g,t[1]=u+p*h);return true}}}}}var fn=1e9,pn=-fn;function clipRectangle(n,r,i,o){function visible(t,e){return n<=t&&t<=i&&r<=e&&e<=o}function interpolate(t,e,a,c){var u=0,l=0;if(t==null||(u=corner(t,a))!==(l=corner(e,a))||comparePoint(t,e)<0^a>0)do{c.point(u===0||u===3?n:i,u>1?o:r)}while((u=(u+a+4)%4)!==l);else c.point(e[0],e[1])}function corner(t,o){return f(t[0]-n)0?0:3:f(t[0]-i)0?2:1:f(t[1]-r)0?1:0:o>0?3:2}function compareIntersection(n,t){return comparePoint(n.x,t.x)}function comparePoint(n,t){var r=corner(n,1),e=corner(t,1);return r!==e?r-e:r===0?t[1]-n[1]:r===1?n[0]-t[0]:r===2?n[1]-t[1]:t[0]-n[0]}return function(e){var a,c,u,l,s,f,p,g,h,d,v,m=e,E=clipBuffer();var S={point:point,lineStart:lineStart,lineEnd:lineEnd,polygonStart:polygonStart,polygonEnd:polygonEnd};function point(n,t){visible(n,t)&&m.point(n,t)}function polygonInside(){var t=0;for(var r=0,e=c.length;ro&&(p-i)*(o-a)>(g-a)*(n-i)&&++t:g<=o&&(p-i)*(o-a)<(g-a)*(n-i)&&--t}return t}function polygonStart(){m=E,a=[],c=[],v=true}function polygonEnd(){var n=polygonInside(),r=v&&n,i=(a=t(a)).length;if(r||i){e.polygonStart();if(r){e.lineStart();interpolate(null,null,1,e);e.lineEnd()}i&&clipRejoin(a,compareIntersection,n,interpolate,e);e.polygonEnd()}m=e,a=c=u=null}function lineStart(){S.point=linePoint;c&&c.push(u=[]);d=true;h=false;p=g=NaN}function lineEnd(){if(a){linePoint(l,s);f&&h&&E.rejoin();a.push(E.result())}S.point=point;h&&m.lineEnd()}function linePoint(t,e){var a=visible(t,e);c&&u.push([t,e]);if(d){l=t,s=e,f=a;d=false;if(a){m.lineStart();m.point(t,e)}}else if(a&&h)m.point(t,e);else{var E=[p=Math.max(pn,Math.min(fn,p)),g=Math.max(pn,Math.min(fn,g))],S=[t=Math.max(pn,Math.min(fn,t)),e=Math.max(pn,Math.min(fn,e))];if(clipLine(E,S,n,r,i,o)){if(!h){m.lineStart();m.point(E[0],E[1])}m.point(S[0],S[1]);a||m.lineEnd();v=false}else if(a){m.lineStart();m.point(t,e);v=false}}p=t,g=e,h=a}return S}}function extent(){var n,t,r,e=0,i=0,o=960,a=500;return r={stream:function(r){return n&&t===r?n:n=clipRectangle(e,i,o,a)(t=r)},extent:function(c){return arguments.length?(e=+c[0][0],i=+c[0][1],o=+c[1][0],a=+c[1][1],n=t=null,r):[[e,i],[o,a]]}}}var gn,hn,dn,vn;var mn={sphere:noop,point:noop,lineStart:lengthLineStart,lineEnd:noop,polygonStart:noop,polygonEnd:noop};function lengthLineStart(){mn.point=lengthPointFirst$1;mn.lineEnd=lengthLineEnd}function lengthLineEnd(){mn.point=mn.lineEnd=noop}function lengthPointFirst$1(n,t){n*=s,t*=s;hn=n,dn=y(t),vn=h(t);mn.point=lengthPoint$1}function lengthPoint$1(n,t){n*=s,t*=s;var r=y(t),e=h(t),i=f(n-hn),o=h(i),a=y(i),c=e*a,u=vn*r-dn*e*o,l=dn*r+vn*e*o;gn.add(g(w(c*c+u*u),l));hn=n,dn=r,vn=e}function length(t){gn=new n;geoStream(t,mn);return+gn}var En=[null,null],Sn={type:"LineString",coordinates:En};function distance(n,t){En[0]=n;En[1]=t;return length(Sn)}var yn={Feature:function(n,t){return containsGeometry(n.geometry,t)},FeatureCollection:function(n,t){var r=n.features,e=-1,i=r.length;while(++e0){o=distance(n[a],n[a-1]);if(o>0&&r<=o&&e<=o&&(r+e-o)*(1-Math.pow((r-e)/o,2))e})).map(s)).concat(r(d(c/m)*m,a,m).filter((function(n){return f(n%S)>e})).map(p))}graticule.lines=function(){return lines().map((function(n){return{type:"LineString",coordinates:n}}))};graticule.outline=function(){return{type:"Polygon",coordinates:[g(o).concat(h(u).slice(1),g(i).reverse().slice(1),h(l).reverse().slice(1))]}};graticule.extent=function(n){return arguments.length?graticule.extentMajor(n).extentMinor(n):graticule.extentMinor()};graticule.extentMajor=function(n){if(!arguments.length)return[[o,l],[i,u]];o=+n[0][0],i=+n[1][0];l=+n[0][1],u=+n[1][1];o>i&&(n=o,o=i,i=n);l>u&&(n=l,l=u,u=n);return graticule.precision(y)};graticule.extentMinor=function(r){if(!arguments.length)return[[t,c],[n,a]];t=+r[0][0],n=+r[1][0];c=+r[0][1],a=+r[1][1];t>n&&(r=t,t=n,n=r);c>a&&(r=c,c=a,a=r);return graticule.precision(y)};graticule.step=function(n){return arguments.length?graticule.stepMajor(n).stepMinor(n):graticule.stepMinor()};graticule.stepMajor=function(n){if(!arguments.length)return[E,S];E=+n[0],S=+n[1];return graticule};graticule.stepMinor=function(n){if(!arguments.length)return[v,m];v=+n[0],m=+n[1];return graticule};graticule.precision=function(r){if(!arguments.length)return y;y=+r;s=graticuleX(c,a,90);p=graticuleY(t,n,y);g=graticuleX(l,u,90);h=graticuleY(o,i,y);return graticule};return graticule.extentMajor([[-180,-90+e],[180,90-e]]).extentMinor([[-180,-80-e],[180,80+e]])}function graticule10(){return graticule()()}function interpolate(n,t){var r=n[0]*s,e=n[1]*s,i=t[0]*s,o=t[1]*s,a=h(e),c=y(e),u=h(o),f=y(o),p=a*h(r),d=a*y(r),v=u*h(i),m=u*y(i),E=2*asin(w(haversin(o-e)+a*u*haversin(i-r))),S=y(E);var R=E?function(n){var t=y(n*=E)/S,r=y(E-n)/S,e=r*p+t*v,i=r*d+t*m,o=r*c+t*f;return[g(i,e)*l,g(o,w(e*e+i*i))*l]}:function(){return[r*l,e*l]};R.distance=E;return R}var identity$1=n=>n;var wn,Pn,jn,Mn,bn=new n,Ln=new n;var xn={point:noop,lineStart:noop,lineEnd:noop,polygonStart:function(){xn.lineStart=areaRingStart;xn.lineEnd=areaRingEnd},polygonEnd:function(){xn.lineStart=xn.lineEnd=xn.point=noop;bn.add(f(Ln));Ln=new n},result:function(){var t=bn/2;bn=new n;return t}};function areaRingStart(){xn.point=areaPointFirst}function areaPointFirst(n,t){xn.point=areaPoint;wn=jn=n,Pn=Mn=t}function areaPoint(n,t){Ln.add(Mn*n-jn*t);jn=n,Mn=t}function areaRingEnd(){areaPoint(wn,Pn)}var Cn=Infinity,qn=Cn,$n=-Cn,_n=$n;var Nn={point:boundsPoint,lineStart:noop,lineEnd:noop,polygonStart:noop,polygonEnd:noop,result:function(){var n=[[Cn,qn],[$n,_n]];$n=_n=-(qn=Cn=Infinity);return n}};function boundsPoint(n,t){n$n&&($n=n);t_n&&(_n=t)}var In,An,zn,Fn,Tn=0,Un=0,Gn=0,kn=0,Hn=0,Wn=0,Dn=0,On=0,Xn=0;var Yn={point:centroidPoint,lineStart:centroidLineStart,lineEnd:centroidLineEnd,polygonStart:function(){Yn.lineStart=centroidRingStart;Yn.lineEnd=centroidRingEnd},polygonEnd:function(){Yn.point=centroidPoint;Yn.lineStart=centroidLineStart;Yn.lineEnd=centroidLineEnd},result:function(){var n=Xn?[Dn/Xn,On/Xn]:Wn?[kn/Wn,Hn/Wn]:Gn?[Tn/Gn,Un/Gn]:[NaN,NaN];Tn=Un=Gn=kn=Hn=Wn=Dn=On=Xn=0;return n}};function centroidPoint(n,t){Tn+=n;Un+=t;++Gn}function centroidLineStart(){Yn.point=centroidPointFirstLine}function centroidPointFirstLine(n,t){Yn.point=centroidPointLine;centroidPoint(zn=n,Fn=t)}function centroidPointLine(n,t){var r=n-zn,e=t-Fn,i=w(r*r+e*e);kn+=i*(zn+n)/2;Hn+=i*(Fn+t)/2;Wn+=i;centroidPoint(zn=n,Fn=t)}function centroidLineEnd(){Yn.point=centroidPoint}function centroidRingStart(){Yn.point=centroidPointFirstRing}function centroidRingEnd(){centroidPointRing(In,An)}function centroidPointFirstRing(n,t){Yn.point=centroidPointRing;centroidPoint(In=zn=n,An=Fn=t)}function centroidPointRing(n,t){var r=n-zn,e=t-Fn,i=w(r*r+e*e);kn+=i*(zn+n)/2;Hn+=i*(Fn+t)/2;Wn+=i;i=Fn*n-zn*t;Dn+=i*(zn+n);On+=i*(Fn+t);Xn+=i*3;centroidPoint(zn=n,Fn=t)}function PathContext(n){this._context=n}PathContext.prototype={_radius:4.5,pointRadius:function(n){return this._radius=n,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){this._line===0&&this._context.closePath();this._point=NaN},point:function(n,t){switch(this._point){case 0:this._context.moveTo(n,t);this._point=1;break;case 1:this._context.lineTo(n,t);break;default:this._context.moveTo(n+this._radius,t);this._context.arc(n,t,this._radius,0,u);break}},result:noop};var Bn,Zn,Jn,Kn,Qn,Vn=new n;var nt={point:noop,lineStart:function(){nt.point=lengthPointFirst},lineEnd:function(){Bn&&lengthPoint(Zn,Jn);nt.point=noop},polygonStart:function(){Bn=true},polygonEnd:function(){Bn=null},result:function(){var t=+Vn;Vn=new n;return t}};function lengthPointFirst(n,t){nt.point=lengthPoint;Zn=Kn=n,Jn=Qn=t}function lengthPoint(n,t){Kn-=n,Qn-=t;Vn.add(w(Kn*Kn+Qn*Qn));Kn=n,Qn=t}let tt,rt,et,it;class PathString{constructor(n){this._append=n==null?append:appendRound(n);this._radius=4.5;this._=""}pointRadius(n){this._radius=+n;return this}polygonStart(){this._line=0}polygonEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){this._line===0&&(this._+="Z");this._point=NaN}point(n,t){switch(this._point){case 0:this._append`M${n},${t}`;this._point=1;break;case 1:this._append`L${n},${t}`;break;default:this._append`M${n},${t}`;if(this._radius!==et||this._append!==rt){const n=this._radius;const t=this._;this._="";this._append`m0,${n}a${n},${n} 0 1,1 0,${-2*n}a${n},${n} 0 1,1 0,${2*n}z`;et=n;rt=this._append;it=this._;this._=t}this._+=it;break}}result(){const n=this._;this._="";return n.length?n:null}}function append(n){let t=1;this._+=n[0];for(const r=n.length;t=0))throw new RangeError(`invalid digits: ${n}`);if(t>15)return append;if(t!==tt){const n=10**t;tt=t;rt=function append(t){let r=1;this._+=t[0];for(const e=t.length;r=0))throw new RangeError(`invalid digits: ${n}`);i=t}t===null&&(e=new PathString(i));return path};return path.projection(n).digits(i).context(t)}function transform(n){return{stream:transformer(n)}}function transformer(n){return function(t){var r=new TransformStream;for(var e in n)r[e]=n[e];r.stream=t;return r}}function TransformStream(){}TransformStream.prototype={constructor:TransformStream,point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};function fit(n,t,r){var e=n.clipExtent&&n.clipExtent();n.scale(150).translate([0,0]);e!=null&&n.clipExtent(null);geoStream(r,n.stream(Nn));t(Nn.result());e!=null&&n.clipExtent(e);return n}function fitExtent(n,t,r){return fit(n,(function(r){var e=t[1][0]-t[0][0],i=t[1][1]-t[0][1],o=Math.min(e/(r[1][0]-r[0][0]),i/(r[1][1]-r[0][1])),a=+t[0][0]+(e-o*(r[1][0]+r[0][0]))/2,c=+t[0][1]+(i-o*(r[1][1]+r[0][1]))/2;n.scale(150*o).translate([a,c])}),r)}function fitSize(n,t,r){return fitExtent(n,[[0,0],t],r)}function fitWidth(n,t,r){return fit(n,(function(r){var e=+t,i=e/(r[1][0]-r[0][0]),o=(e-i*(r[1][0]+r[0][0]))/2,a=-i*r[0][1];n.scale(150*i).translate([o,a])}),r)}function fitHeight(n,t,r){return fit(n,(function(r){var e=+t,i=e/(r[1][1]-r[0][1]),o=-i*r[0][0],a=(e-i*(r[1][1]+r[0][1]))/2;n.scale(150*i).translate([o,a])}),r)}var ot=16,at=h(30*s);function resample(n,t){return+t?resample$1(n,t):resampleNone(n)}function resampleNone(n){return transformer({point:function(t,r){t=n(t,r);this.stream.point(t[0],t[1])}})}function resample$1(n,t){function resampleLineTo(r,i,o,a,c,u,l,s,p,h,d,v,m,E){var S=l-r,y=s-i,R=S*S+y*y;if(R>4*t&&m--){var P=a+h,j=c+d,M=u+v,b=w(P*P+j*j+M*M),L=asin(M/=b),x=f(f(M)-1)t||f((S*_+y*N)/R-.5)>.3||a*h+c*d+u*v2?n[2]%360*s:0,recenter()):[E*l,S*l,y*l]};projection.angle=function(n){return arguments.length?(R=n%360*s,recenter()):R*l};projection.reflectX=function(n){return arguments.length?(P=n?-1:1,recenter()):P<0};projection.reflectY=function(n){return arguments.length?(j=n?-1:1,recenter()):j<0};projection.precision=function(n){return arguments.length?(a=resample(c,C=n*n),reset()):w(C)};projection.fitExtent=function(n,t){return fitExtent(projection,n,t)};projection.fitSize=function(n,t){return fitSize(projection,n,t)};projection.fitWidth=function(n,t){return fitWidth(projection,n,t)};projection.fitHeight=function(n,t){return fitHeight(projection,n,t)};function recenter(){var n=scaleTranslateRotate(g,0,0,P,j,R).apply(null,t(v,m)),e=scaleTranslateRotate(g,h-n[0],d-n[1],P,j,R);r=rotateRadians(E,S,y);c=compose(t,e);u=compose(r,c);a=resample(c,C);return reset()}function reset(){f=p=null;return projection}return function(){t=n.apply(this,arguments);projection.invert=t.invert&&invert;return recenter()}}function conicProjection(n){var t=0,r=o/3,e=projectionMutator(n),i=e(t,r);i.parallels=function(n){return arguments.length?e(t=n[0]*s,r=n[1]*s):[t*l,r*l]};return i}function cylindricalEqualAreaRaw(n){var t=h(n);function forward(n,r){return[n*t,y(r)/t]}forward.invert=function(n,r){return[n/t,asin(r*t)]};return forward}function conicEqualAreaRaw(n,t){var r=y(n),i=(r+y(t))/2;if(f(i)=.12&&i<.234&&e>=-.425&&e<-.214?u:i>=.166&&i<.234&&e>=-.214&&e<-.115?l:c).invert(n)};albersUsa.stream=function(r){return n&&t===r?n:n=multiplex([c.stream(t=r),u.stream(r),l.stream(r)])};albersUsa.precision=function(n){if(!arguments.length)return c.precision();c.precision(n),u.precision(n),l.precision(n);return reset()};albersUsa.scale=function(n){if(!arguments.length)return c.scale();c.scale(n),u.scale(n*.35),l.scale(n);return albersUsa.translate(c.translate())};albersUsa.translate=function(n){if(!arguments.length)return c.translate();var t=c.scale(),a=+n[0],f=+n[1];r=c.translate(n).clipExtent([[a-.455*t,f-.238*t],[a+.455*t,f+.238*t]]).stream(s);i=u.translate([a-.307*t,f+.201*t]).clipExtent([[a-.425*t+e,f+.12*t+e],[a-.214*t-e,f+.234*t-e]]).stream(s);o=l.translate([a-.205*t,f+.212*t]).clipExtent([[a-.214*t+e,f+.166*t+e],[a-.115*t-e,f+.234*t-e]]).stream(s);return reset()};albersUsa.fitExtent=function(n,t){return fitExtent(albersUsa,n,t)};albersUsa.fitSize=function(n,t){return fitSize(albersUsa,n,t)};albersUsa.fitWidth=function(n,t){return fitWidth(albersUsa,n,t)};albersUsa.fitHeight=function(n,t){return fitHeight(albersUsa,n,t)};function reset(){n=t=null;return albersUsa}return albersUsa.scale(1070)}function azimuthalRaw(n){return function(t,r){var e=h(t),i=h(r),o=n(e*i);return o===Infinity?[2,0]:[o*i*y(t),o*y(r)]}}function azimuthalInvert(n){return function(t,r){var e=w(t*t+r*r),i=n(e),o=y(i),a=h(i);return[g(t*o,e*a),asin(e&&r*o/e)]}}var ut=azimuthalRaw((function(n){return w(2/(1+n))}));ut.invert=azimuthalInvert((function(n){return 2*asin(n/2)}));function azimuthalEqualArea(){return projection(ut).scale(124.75).clipAngle(179.999)}var lt=azimuthalRaw((function(n){return(n=acos(n))&&n/y(n)}));lt.invert=azimuthalInvert((function(n){return n}));function azimuthalEquidistant(){return projection(lt).scale(79.4188).clipAngle(179.999)}function mercatorRaw(n,t){return[n,E(P((a+t)/2))]}mercatorRaw.invert=function(n,t){return[n,2*p(v(t))-a]};function mercator(){return mercatorProjection(mercatorRaw).scale(961/u)}function mercatorProjection(n){var t,r,e,i=projection(n),a=i.center,c=i.scale,u=i.translate,l=i.clipExtent,s=null;i.scale=function(n){return arguments.length?(c(n),reclip()):c()};i.translate=function(n){return arguments.length?(u(n),reclip()):u()};i.center=function(n){return arguments.length?(a(n),reclip()):a()};i.clipExtent=function(n){return arguments.length?(n==null?s=t=r=e=null:(s=+n[0][0],t=+n[0][1],r=+n[1][0],e=+n[1][1]),reclip()):s==null?null:[[s,t],[r,e]]};function reclip(){var a=o*c(),u=i(rotation(i.rotate()).invert([0,0]));return l(s==null?[[u[0]-a,u[1]-a],[u[0]+a,u[1]+a]]:n===mercatorRaw?[[Math.max(u[0]-a,s),t],[Math.min(u[0]+a,r),e]]:[[s,Math.max(u[1]-a,t)],[r,Math.min(u[1]+a,e)]])}return reclip()}function tany(n){return P((a+n)/2)}function conicConformalRaw(n,t){var r=h(n),i=n===t?y(n):E(r/h(t))/E(tany(t)/tany(n)),c=r*S(tany(n),i)/i;if(!i)return mercatorRaw;function project(n,t){c>0?t<-a+e&&(t=-a+e):t>a-e&&(t=a-e);var r=c/S(tany(t),i);return[r*y(i*n),c-r*h(i*n)]}project.invert=function(n,t){var r=c-t,e=R(i)*w(n*n+r*r),u=g(n,f(r))*R(r);r*i<0&&(u-=o*R(n)*R(r));return[u/i,2*p(S(c/e,1/i))-a]};return project}function conicConformal(){return conicProjection(conicConformalRaw).scale(109.5).parallels([30,30])}function equirectangularRaw(n,t){return[n,t]}equirectangularRaw.invert=equirectangularRaw;function equirectangular(){return projection(equirectangularRaw).scale(152.63)}function conicEquidistantRaw(n,t){var r=h(n),i=n===t?y(n):(r-h(t))/(t-n),a=r/i+n;if(f(i)e&&--o>0);return[n/(.8707+(a=i*i)*(a*(a*a*a*(.003971-.001529*a)-.013791)-.131979)),i]};function naturalEarth1(){return projection(naturalEarth1Raw).scale(175.295)}function orthographicRaw(n,t){return[h(t)*y(n),y(t)]}orthographicRaw.invert=azimuthalInvert(asin);function orthographic(){return projection(orthographicRaw).scale(249.5).clipAngle(90+e)}function stereographicRaw(n,t){var r=h(t),e=1+h(n)*r;return[r*y(n)/e,y(t)/e]}stereographicRaw.invert=azimuthalInvert((function(n){return 2*p(n)}));function stereographic(){return projection(stereographicRaw).scale(250).clipAngle(142)}function transverseMercatorRaw(n,t){return[E(P((a+t)/2)),-n]}transverseMercatorRaw.invert=function(n,t){return[-t,2*p(v(n))-a]};function transverseMercator(){var n=mercatorProjection(transverseMercatorRaw),t=n.center,r=n.rotate;n.center=function(n){return arguments.length?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])};n.rotate=function(n){return arguments.length?r([n[0],n[1],n.length>2?n[2]+90:90]):(n=r(),[n[0],n[1],n[2]-90])};return r([0,0,90]).scale(159.155)}export{albers as geoAlbers,albersUsa as geoAlbersUsa,area as geoArea,azimuthalEqualArea as geoAzimuthalEqualArea,ut as geoAzimuthalEqualAreaRaw,azimuthalEquidistant as geoAzimuthalEquidistant,lt as geoAzimuthalEquidistantRaw,bounds as geoBounds,centroid as geoCentroid,circle as geoCircle,sn as geoClipAntimeridian,clipCircle as geoClipCircle,extent as geoClipExtent,clipRectangle as geoClipRectangle,conicConformal as geoConicConformal,conicConformalRaw as geoConicConformalRaw,conicEqualArea as geoConicEqualArea,conicEqualAreaRaw as geoConicEqualAreaRaw,conicEquidistant as geoConicEquidistant,conicEquidistantRaw as geoConicEquidistantRaw,contains as geoContains,distance as geoDistance,equalEarth as geoEqualEarth,equalEarthRaw as geoEqualEarthRaw,equirectangular as geoEquirectangular,equirectangularRaw as geoEquirectangularRaw,gnomonic as geoGnomonic,gnomonicRaw as geoGnomonicRaw,graticule as geoGraticule,graticule10 as geoGraticule10,identity as geoIdentity,interpolate as geoInterpolate,length as geoLength,mercator as geoMercator,mercatorRaw as geoMercatorRaw,naturalEarth1 as geoNaturalEarth1,naturalEarth1Raw as geoNaturalEarth1Raw,orthographic as geoOrthographic,orthographicRaw as geoOrthographicRaw,index as geoPath,projection as geoProjection,projectionMutator as geoProjectionMutator,rotation as geoRotation,stereographic as geoStereographic,stereographicRaw as geoStereographicRaw,geoStream,transform as geoTransform,transverseMercator as geoTransverseMercator,transverseMercatorRaw as geoTransverseMercatorRaw}; diff --git a/vendor/javascript/d3-hierarchy.js b/vendor/javascript/d3-hierarchy.js index 1d69d374..ab7f2f9d 100644 --- a/vendor/javascript/d3-hierarchy.js +++ b/vendor/javascript/d3-hierarchy.js @@ -1,2 +1,4 @@ +// d3-hierarchy@3.1.2 downloaded from https://ga.jspm.io/npm:d3-hierarchy@3.1.2/src/index.js + function defaultSeparation$1(e,n){return e.parent===n.parent?1:2}function meanX(e){return e.reduce(meanXReduce,0)/e.length}function meanXReduce(e,n){return e+n.x}function maxY(e){return 1+e.reduce(maxYReduce,0)}function maxYReduce(e,n){return Math.max(e,n.y)}function leafLeft(e){var n;while(n=e.children)e=n[0];return e}function leafRight(e){var n;while(n=e.children)e=n[n.length-1];return e}function cluster(){var e=defaultSeparation$1,n=1,t=1,r=false;function cluster(i){var a,o=0;i.eachAfter((function(n){var t=n.children;if(t){n.x=meanX(t);n.y=maxY(t)}else{n.x=a?o+=e(n,a):0;n.y=0;a=n}}));var u=leafLeft(i),c=leafRight(i),l=u.x-e(u,c)/2,s=c.x+e(c,u)/2;return i.eachAfter(r?function(e){e.x=(e.x-i.x)*n;e.y=(i.y-e.y)*t}:function(e){e.x=(e.x-l)/(s-l)*n;e.y=(1-(i.y?e.y/i.y:1))*t})}cluster.separation=function(n){return arguments.length?(e=n,cluster):e};cluster.size=function(e){return arguments.length?(r=false,n=+e[0],t=+e[1],cluster):r?null:[n,t]};cluster.nodeSize=function(e){return arguments.length?(r=true,n=+e[0],t=+e[1],cluster):r?[n,t]:null};return cluster}function count(e){var n=0,t=e.children,r=t&&t.length;if(r)while(--r>=0)n+=t[r].value;else n=1;e.value=n}function node_count(){return this.eachAfter(count)}function node_each(e,n){let t=-1;for(const r of this)e.call(n,r,++t,this);return this}function node_eachBefore(e,n){var t,r,i=this,a=[i],o=-1;while(i=a.pop()){e.call(n,i,++o,this);if(t=i.children)for(r=t.length-1;r>=0;--r)a.push(t[r])}return this}function node_eachAfter(e,n){var t,r,i,a=this,o=[a],u=[],c=-1;while(a=o.pop()){u.push(a);if(t=a.children)for(r=0,i=t.length;r=0)t+=r[i].value;n.value=t}))}function node_sort(e){return this.eachBefore((function(n){n.children&&n.children.sort(e)}))}function node_path(e){var n=this,t=leastCommonAncestor(n,e),r=[n];while(n!==t){n=n.parent;r.push(n)}var i=r.length;while(e!==t){r.splice(i,0,e);e=e.parent}return r}function leastCommonAncestor(e,n){if(e===n)return e;var t=e.ancestors(),r=n.ancestors(),i=null;e=t.pop();n=r.pop();while(e===n){i=e;e=t.pop();n=r.pop()}return i}function node_ancestors(){var e=this,n=[e];while(e=e.parent)n.push(e);return n}function node_descendants(){return Array.from(this)}function node_leaves(){var e=[];this.eachBefore((function(n){n.children||e.push(n)}));return e}function node_links(){var e=this,n=[];e.each((function(t){t!==e&&n.push({source:t.parent,target:t})}));return n}function*node_iterator(){var e,n,t,r,i=this,a=[i];do{e=a.reverse(),a=[];while(i=e.pop()){yield i;if(n=i.children)for(t=0,r=n.length;t=0;--a){c.push(r=i[a]=new Node$1(i[a]));r.parent=t;r.depth=t.depth+1}}return u.eachBefore(computeHeight)}function node_copy(){return hierarchy(this).eachBefore(copyData)}function objectChildren(e){return e.children}function mapChildren(e){return Array.isArray(e)?e[1]:null}function copyData(e){void 0!==e.data.value&&(e.value=e.data.value);e.data=e.data.data}function computeHeight(e){var n=0;do{e.height=n}while((e=e.parent)&&e.height<++n)}function Node$1(e){this.data=e;this.depth=this.height=0;this.parent=null}Node$1.prototype=hierarchy.prototype={constructor:Node$1,count:node_count,each:node_each,eachAfter:node_eachAfter,eachBefore:node_eachBefore,find:node_find,sum:node_sum,sort:node_sort,path:node_path,ancestors:node_ancestors,descendants:node_descendants,leaves:node_leaves,links:node_links,copy:node_copy,[Symbol.iterator]:node_iterator};function optional(e){return null==e?null:required(e)}function required(e){if("function"!==typeof e)throw new Error;return e}function constantZero(){return 0}function constant(e){return function(){return e}}const e=1664525;const n=1013904223;const t=4294967296;function lcg(){let r=1;return()=>(r=(e*r+n)%t)/t}function array(e){return"object"===typeof e&&"length"in e?e:Array.from(e)}function shuffle(e,n){let t,r,i=e.length;while(i){r=n()*i--|0;t=e[i];e[i]=e[r];e[r]=t}return e}function enclose(e){return packEncloseRandom(e,lcg())}function packEncloseRandom(e,n){var t,r,i=0,a=(e=shuffle(Array.from(e),n)).length,o=[];while(i0&&t*t>r*r+i*i}function enclosesWeakAll(e,n){for(var t=0;t1e-6?(z+Math.sqrt(z*z-4*R*M))/(2*R):M/z);return{x:r+B+k*S,y:i+N+A*S,r:S}}function place(e,n,t){var r,i,a,o,u=e.x-n.x,c=e.y-n.y,l=u*u+c*c;if(l){i=n.r+t.r,i*=i;o=e.r+t.r,o*=o;if(i>o){r=(l+o-i)/(2*l);a=Math.sqrt(Math.max(0,o/l-r*r));t.x=e.x-r*u-a*c;t.y=e.y-r*c+a*u}else{r=(l+i-o)/(2*l);a=Math.sqrt(Math.max(0,i/l-r*r));t.x=n.x+r*u-a*c;t.y=n.y+r*c+a*u}}else{t.x=n.x+t.r;t.y=n.y}}function intersects(e,n){var t=e.r+n.r-1e-6,r=n.x-e.x,i=n.y-e.y;return t>0&&t*t>r*r+i*i}function score(e){var n=e._,t=e.next._,r=n.r+t.r,i=(n.x*t.r+t.x*n.r)/r,a=(n.y*t.r+t.y*n.r)/r;return i*i+a*a}function Node(e){this._=e;this.next=null;this.previous=null}function packSiblingsRandom(e,n){if(!(a=(e=array(e)).length))return 0;var t,r,i,a,o,u,c,l,s,f,h;t=e[0],t.x=0,t.y=0;if(!(a>1))return t.r;r=e[1],t.x=-r.r,r.x=t.r,r.y=0;if(!(a>2))return t.r+r.r;place(r,t,i=e[2]);t=new Node(t),r=new Node(r),i=new Node(i);t.next=i.previous=r;r.next=t.previous=i;i.next=r.previous=t;e:for(c=3;cnormalize(e(n,t,o))));const t=n.map(parentof);const r=new Set(n).add("");for(const e of t)if(!r.has(e)){r.add(e);n.push(e);t.push(parentof(e));y.push(a)}x=(e,t)=>n[t];m=(e,n)=>t[n]}for(l=0,u=y.length;l=0;--e){h=y[e];if(h.data!==a)break;h.data=null}}s.parent=r;s.eachBefore((function(e){e.depth=e.parent.depth+1;--u})).eachBefore(computeHeight);s.parent=null;if(u>0)throw new Error("cycle");return s}stratify.id=function(e){return arguments.length?(n=optional(e),stratify):n};stratify.parentId=function(e){return arguments.length?(t=optional(e),stratify):t};stratify.path=function(n){return arguments.length?(e=optional(n),stratify):e};return stratify}function normalize(e){e=`${e}`;let n=e.length;slash(e,n-1)&&!slash(e,n-2)&&(e=e.slice(0,-1));return"/"===e[0]?e:`/${e}`}function parentof(e){let n=e.length;if(n<2)return"";while(--n>1)if(slash(e,n))break;return e.slice(0,n)}function slash(e,n){if("/"===e[n]){let t=0;while(n>0&&"\\"===e[--n])++t;if(0===(1&t))return true}return false}function defaultSeparation(e,n){return e.parent===n.parent?1:2}function nextLeft(e){var n=e.children;return n?n[0]:e.t}function nextRight(e){var n=e.children;return n?n[n.length-1]:e.t}function moveSubtree(e,n,t){var r=t/(n.i-e.i);n.c-=r;n.s+=t;e.c+=r;n.z+=t;n.m+=t}function executeShifts(e){var n,t=0,r=0,i=e.children,a=i.length;while(--a>=0){n=i[a];n.z+=t;n.m+=t;t+=n.s+(r+=n.c)}}function nextAncestor(e,n,t){return e.a.parent===n.parent?e.a:t}function TreeNode(e,n){this._=e;this.parent=null;this.children=null;this.A=null;this.a=this;this.z=0;this.m=0;this.c=0;this.s=0;this.t=null;this.i=n}TreeNode.prototype=Object.create(Node$1.prototype);function treeRoot(e){var n,t,r,i,a,o=new TreeNode(e,0),u=[o];while(n=u.pop())if(r=n._.children){n.children=new Array(a=r.length);for(i=a-1;i>=0;--i){u.push(t=n.children[i]=new TreeNode(r[i],i));t.parent=n}}(o.parent=new TreeNode(null,0)).children=[o];return o}function tree(){var e=defaultSeparation,n=1,t=1,r=null;function tree(i){var a=treeRoot(i);a.eachAfter(firstWalk),a.parent.m=-a.z;a.eachBefore(secondWalk);if(r)i.eachBefore(sizeNode);else{var o=i,u=i,c=i;i.eachBefore((function(e){e.xu.x&&(u=e);e.depth>c.depth&&(c=e)}));var l=o===u?1:e(o,u)/2,s=l-o.x,f=n/(u.x+l+s),h=t/(c.depth||1);i.eachBefore((function(e){e.x=(e.x+s)*f;e.y=e.depth*h}))}return i}function firstWalk(n){var t=n.children,r=n.parent.children,i=n.i?r[n.i-1]:null;if(t){executeShifts(n);var a=(t[0].z+t[t.length-1].z)/2;if(i){n.z=i.z+e(n._,i._);n.m=n.z-a}else n.z=a}else i&&(n.z=i.z+e(n._,i._));n.parent.A=apportion(n,i,n.parent.A||r[0])}function secondWalk(e){e._.x=e.z+e.parent.m;e.m+=e.parent.m}function apportion(n,t,r){if(t){var i,a=n,o=n,u=t,c=a.parent.children[0],l=a.m,s=o.m,f=u.m,h=c.m;while(u=nextRight(u),a=nextLeft(a),u&&a){c=nextLeft(c);o=nextRight(o);o.a=n;i=u.z+f-a.z-l+e(u._,a._);if(i>0){moveSubtree(nextAncestor(u,n,r),n,i);l+=i;s+=i}f+=u.m;l+=a.m;h+=c.m;s+=o.m}if(u&&!nextRight(o)){o.t=u;o.m+=f-s}if(a&&!nextLeft(c)){c.t=a;c.m+=l-h;r=n}}return r}function sizeNode(e){e.x*=n;e.y=e.depth*t}tree.separation=function(n){return arguments.length?(e=n,tree):e};tree.size=function(e){return arguments.length?(r=false,n=+e[0],t=+e[1],tree):r?null:[n,t]};tree.nodeSize=function(e){return arguments.length?(r=true,n=+e[0],t=+e[1],tree):r?[n,t]:null};return tree}function treemapSlice(e,n,t,r,i){var a,o=e.children,u=-1,c=o.length,l=e.value&&(i-t)/e.value;while(++uh&&(h=u);x=s*s*y;d=Math.max(h/x,x/f);if(d>p){s-=u;break}p=d}m.push(o={value:s,dice:c1?e:1)};return squarify}(o);function index(){var e=u,n=false,t=1,r=1,i=[0],a=constantZero,o=constantZero,c=constantZero,l=constantZero,s=constantZero;function treemap(e){e.x0=e.y0=0;e.x1=t;e.y1=r;e.eachBefore(positionNode);i=[0];n&&e.eachBefore(roundNode);return e}function positionNode(n){var t=i[n.depth],r=n.x0+t,u=n.y0+t,f=n.x1-t,h=n.y1-t;f=n-1){var c=u[e];c.x0=r,c.y0=i;c.x1=a,c.y1=o}else{var s=l[e],f=t/2+s,h=e+1,d=n-1;while(h>>1;l[p]o-i){var m=t?(r*x+a*y)/t:a;partition(e,h,y,r,i,m,o);partition(h,n,x,m,i,a,o)}else{var v=t?(i*x+o*y)/t:o;partition(e,h,y,r,i,a,v);partition(h,n,x,r,v,a,o)}}}}function sliceDice(e,n,t,r,i){(1&e.depth?treemapSlice:treemapDice)(e,n,t,r,i)}var c=function custom(e){function resquarify(n,t,r,i,a){if((o=n._squarify)&&o.ratio===e){var o,u,c,l,s,f=-1,h=o.length,d=n.value;while(++f1?e:1)};return resquarify}(o);export{Node$1 as Node,cluster,hierarchy,index$1 as pack,enclose as packEnclose,siblings as packSiblings,partition,stratify,tree,index as treemap,binary as treemapBinary,treemapDice,c as treemapResquarify,treemapSlice,sliceDice as treemapSliceDice,u as treemapSquarify}; diff --git a/vendor/javascript/d3-interpolate.js b/vendor/javascript/d3-interpolate.js index 9822fa1f..626ad6fd 100644 --- a/vendor/javascript/d3-interpolate.js +++ b/vendor/javascript/d3-interpolate.js @@ -1,2 +1,4 @@ +// d3-interpolate@3.0.1 downloaded from https://ga.jspm.io/npm:d3-interpolate@3.0.1/src/index.js + import{rgb as n,color as r,hsl as t,lab as e,hcl as a,cubehelix as o}from"d3-color";function basis(n,r,t,e,a){var o=n*n,u=o*n;return((1-3*n+3*o-u)*r+(4-6*o+3*u)*t+(1+3*n+3*o-3*u)*e+u*a)/6}function basis$1(n){var r=n.length-1;return function(t){var e=t<=0?t=0:t>=1?(t=1,r-1):Math.floor(t*r),a=n[e],o=n[e+1],u=e>0?n[e-1]:2*a-o,i=e()=>n;function linear(n,r){return function(t){return n+t*r}}function exponential(n,r,t){return n=Math.pow(n,t),r=Math.pow(r,t)-n,t=1/t,function(e){return Math.pow(n+e*r,t)}}function hue$1(n,r){var t=r-n;return t?linear(n,t>180||t<-180?t-360*Math.round(t/360):t):constant(isNaN(n)?r:n)}function gamma(n){return 1===(n=+n)?nogamma:function(r,t){return t-r?exponential(r,t,n):constant(isNaN(r)?t:r)}}function nogamma(n,r){var t=r-n;return t?linear(n,t):constant(isNaN(n)?r:n)}var u=function rgbGamma(r){var t=gamma(r);function rgb(r,e){var a=t((r=n(r)).r,(e=n(e)).r),o=t(r.g,e.g),u=t(r.b,e.b),i=nogamma(r.opacity,e.opacity);return function(n){r.r=a(n);r.g=o(n);r.b=u(n);r.opacity=i(n);return r+""}}rgb.gamma=rgbGamma;return rgb}(1);function rgbSpline(r){return function(t){var e,a,o=t.length,u=new Array(o),i=new Array(o),s=new Array(o);for(e=0;eo){a=r.slice(o,a);i[u]?i[u]+=a:i[++u]=a}if((t=t[0])===(e=e[0]))i[u]?i[u]+=e:i[++u]=e;else{i[++u]=null;s.push({i:u,x:number(t,e)})}o=c.lastIndex}if(o180?r+=360:r-n>180&&(n+=360);a.push({i:t.push(pop(t)+"rotate(",null,e)-2,x:number(n,r)})}else r&&t.push(pop(t)+"rotate("+r+e)}function skewX(n,r,t,a){n!==r?a.push({i:t.push(pop(t)+"skewX(",null,e)-2,x:number(n,r)}):r&&t.push(pop(t)+"skewX("+r+e)}function scale(n,r,t,e,a,o){if(n!==t||r!==e){var u=a.push(pop(a)+"scale(",null,",",null,")");o.push({i:u-4,x:number(n,t)},{i:u-2,x:number(r,e)})}else 1===t&&1===e||a.push(pop(a)+"scale("+t+","+e+")")}return function(r,t){var e=[],a=[];r=n(r),t=n(t);translate(r.translateX,r.translateY,t.translateX,t.translateY,e,a);rotate(r.rotate,t.rotate,e,a);skewX(r.skewX,t.skewX,e,a);scale(r.scaleX,r.scaleY,t.scaleX,t.scaleY,e,a);r=t=null;return function(n){var r,t=-1,o=a.length;while(++t=0))throw new Error(`invalid digits: ${t}`);if(h>15)return append;const i=10**h;return function(t){this._+=t[0];for(let h=1,s=t.length;hi)if(Math.abs(d*p-r*o)>i&&e){let u=n-_,x=a-$,y=p*p+r*r,M=u*u+x*x,c=Math.sqrt(y),f=Math.sqrt(l),w=e*Math.tan((t-Math.acos((y+l-M)/(2*c*f)))/2),v=w/f,P=w/c;Math.abs(v-1)>i&&this._append`L${h+v*o},${s+v*d}`;this._append`A${e},${e},0,0,${+(d*u>o*x)},${this._x1=h+P*p},${this._y1=s+P*r}`}else this._append`L${this._x1=h},${this._y1=s}`;else;}arc(n,a,e,_,$,p){n=+n,a=+a,e=+e,p=!!p;if(e<0)throw new Error(`negative radius: ${e}`);let r=e*Math.cos(_),o=e*Math.sin(_),d=n+r,l=a+o,u=1^p,x=p?_-$:$-_;null===this._x1?this._append`M${d},${l}`:(Math.abs(this._x1-d)>i||Math.abs(this._y1-l)>i)&&this._append`L${d},${l}`;if(e){x<0&&(x=x%h+h);x>s?this._append`A${e},${e},0,1,${u},${n-r},${a-o}A${e},${e},0,1,${u},${this._x1=d},${this._y1=l}`:x>i&&this._append`A${e},${e},0,${+(x>=t)},${u},${this._x1=n+e*Math.cos($)},${this._y1=a+e*Math.sin($)}`}}rect(t,h,i,s){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+h}h${i=+i}v${+s}h${-i}Z`}toString(){return this._}}function path(){return new Path}path.prototype=Path.prototype;function pathRound(t=3){return new Path(+t)}export{Path,path,pathRound}; +// d3-path@1.0.9 downloaded from https://ga.jspm.io/npm:d3-path@1.0.9/dist/d3-path.js + +var t="undefined"!==typeof globalThis?globalThis:"undefined"!==typeof self?self:global;var i={};(function(t,h){h(i)})(i,(function(i){var h=Math.PI,s=2*h,_=1e-6,n=s-_;function Path(){(this||t)._x0=(this||t)._y0=(this||t)._x1=(this||t)._y1=null;(this||t)._=""}function path(){return new Path}Path.prototype=path.prototype={constructor:Path,moveTo:function(i,h){(this||t)._+="M"+((this||t)._x0=(this||t)._x1=+i)+","+((this||t)._y0=(this||t)._y1=+h)},closePath:function(){if(null!==(this||t)._x1){(this||t)._x1=(this||t)._x0,(this||t)._y1=(this||t)._y0;(this||t)._+="Z"}},lineTo:function(i,h){(this||t)._+="L"+((this||t)._x1=+i)+","+((this||t)._y1=+h)},quadraticCurveTo:function(i,h,s,_){(this||t)._+="Q"+ +i+","+ +h+","+((this||t)._x1=+s)+","+((this||t)._y1=+_)},bezierCurveTo:function(i,h,s,_,n,a){(this||t)._+="C"+ +i+","+ +h+","+ +s+","+ +_+","+((this||t)._x1=+n)+","+((this||t)._y1=+a)},arcTo:function(i,s,n,a,e){i=+i,s=+s,n=+n,a=+a,e=+e;var o=(this||t)._x1,r=(this||t)._y1,u=n-i,f=a-s,c=o-i,l=r-s,x=c*c+l*l;if(e<0)throw new Error("negative radius: "+e);if(null===(this||t)._x1)(this||t)._+="M"+((this||t)._x1=i)+","+((this||t)._y1=s);else if(x>_)if(Math.abs(l*u-f*c)>_&&e){var y=n-o,M=a-r,p=u*u+f*f,v=y*y+M*M,d=Math.sqrt(p),b=Math.sqrt(x),P=e*Math.tan((h-Math.acos((p+x-v)/(2*d*b)))/2),T=P/b,g=P/d;Math.abs(T-1)>_&&((this||t)._+="L"+(i+T*c)+","+(s+T*l));(this||t)._+="A"+e+","+e+",0,0,"+ +(l*y>c*M)+","+((this||t)._x1=i+g*u)+","+((this||t)._y1=s+g*f)}else(this||t)._+="L"+((this||t)._x1=i)+","+((this||t)._y1=s);else;},arc:function(i,a,e,o,r,u){i=+i,a=+a,e=+e,u=!!u;var f=e*Math.cos(o),c=e*Math.sin(o),l=i+f,x=a+c,y=1^u,M=u?o-r:r-o;if(e<0)throw new Error("negative radius: "+e);null===(this||t)._x1?(this||t)._+="M"+l+","+x:(Math.abs((this||t)._x1-l)>_||Math.abs((this||t)._y1-x)>_)&&((this||t)._+="L"+l+","+x);if(e){M<0&&(M=M%s+s);M>n?(this||t)._+="A"+e+","+e+",0,1,"+y+","+(i-f)+","+(a-c)+"A"+e+","+e+",0,1,"+y+","+((this||t)._x1=l)+","+((this||t)._y1=x):M>_&&((this||t)._+="A"+e+","+e+",0,"+ +(M>=h)+","+y+","+((this||t)._x1=i+e*Math.cos(r))+","+((this||t)._y1=a+e*Math.sin(r)))}},rect:function(i,h,s,_){(this||t)._+="M"+((this||t)._x0=(this||t)._x1=+i)+","+((this||t)._y0=(this||t)._y1=+h)+"h"+ +s+"v"+ +_+"h"+-s+"Z"},toString:function(){return(this||t)._}};i.path=path;Object.defineProperty(i,"__esModule",{value:true})}));const h=i.path,s=i.__esModule;export default i;export{s as __esModule,h as path}; diff --git a/vendor/javascript/d3-polygon.js b/vendor/javascript/d3-polygon.js index 4ec6cc5c..f9371c51 100644 --- a/vendor/javascript/d3-polygon.js +++ b/vendor/javascript/d3-polygon.js @@ -1,2 +1,4 @@ +// d3-polygon@3.0.1 downloaded from https://ga.jspm.io/npm:d3-polygon@3.0.1/src/index.js + function area(n){var r,e=-1,t=n.length,o=n[t-1],l=0;while(++e1&&cross(n[e[o-2]],n[e[o-1]],n[t])<=0)--o;e[o++]=t}return e.slice(0,o)}function hull(n){if((e=n.length)<3)return null;var r,e,t=new Array(e),o=new Array(e);for(r=0;r=0;--r)i.push(n[t[l[r]][2]]);for(r=+a;ra!==i>a&&u<(h-e)*(a-t)/(i-t)+e&&(c=!c);h=e,i=t}return c}function length(n){var r,e,t=-1,o=n.length,l=n[o-1],u=l[0],a=l[1],h=0;while(++t=(h=(x+v)/2))?x=h:v=h;(l=i>=(s=(c+w)/2))?c=s:w=s;if(n=f,!(f=f[_=l<<1|u]))return n[_]=y,t}a=+t._x.call(null,f.data);o=+t._y.call(null,f.data);if(e===a&&i===o)return y.next=f,n?n[_]=y:t._root=y,t;do{n=n?n[_]=new Array(4):t._root=new Array(4);(u=e>=(h=(x+v)/2))?x=h:v=h;(l=i>=(s=(c+w)/2))?c=s:w=s}while((_=l<<1|u)===(d=(o>=s)<<1|a>=h));return n[d]=f,n[_]=y,t}function addAll(t){var e,i,r,n,h=t.length,s=new Array(h),a=new Array(h),o=Infinity,u=Infinity,l=-Infinity,_=-Infinity;for(i=0;il&&(l=r);n_&&(_=n)}if(o>l||u>_)return this;this.cover(o,u).cover(l,_);for(i=0;it||t>=n||r>e||e>=h){a=(ed||(h=o.y0)>f||(s=o.x1)=v)<<1|t>=c){o=y[y.length-1];y[y.length-1]=y[y.length-1-u];y[y.length-1-u]=o}}else{var w=t-+this._x.call(null,x.data),p=e-+this._y.call(null,x.data),N=w*w+p*p;if(N=(a=(y+c)/2))?y=a:c=a;(l=s>=(o=(x+v)/2))?x=o:v=o;if(!(e=f,f=f[_=l<<1|u]))return this;if(!f.length)break;(e[_+1&3]||e[_+2&3]||e[_+3&3])&&(i=e,d=_)}while(f.data!==t)if(!(r=f,f=f.next))return this;(n=f.next)&&delete f.next;if(r)return n?r.next=n:delete r.next,this;if(!e)return this._root=n,this;n?e[_]=n:delete e[_];(f=e[0]||e[1]||e[2]||e[3])&&f===(e[3]||e[2]||e[1]||e[0])&&!f.length&&(i?i[d]=f:this._root=f);return this}function removeAll(t){for(var e=0,i=t.length;e1);return n+o*t*Math.sqrt(-2*Math.log(u)/u)}}randomNormal.source=sourceRandomNormal;return randomNormal}(r);var u=function sourceRandomLogNormal(r){var n=a.source(r);function randomLogNormal(){var r=n.apply(this,arguments);return function(){return Math.exp(r())}}randomLogNormal.source=sourceRandomLogNormal;return randomLogNormal}(r);var t=function sourceRandomIrwinHall(r){function randomIrwinHall(n){return(n=+n)<=0?()=>0:function(){for(var o=0,a=n;a>1;--a)o+=r();return o+a*r()}}randomIrwinHall.source=sourceRandomIrwinHall;return randomIrwinHall}(r);var e=function sourceRandomBates(r){var n=t.source(r);function randomBates(o){if(0===(o=+o))return r;var a=n(o);return function(){return a()/o}}randomBates.source=sourceRandomBates;return randomBates}(r);var i=function sourceRandomExponential(r){function randomExponential(n){return function(){return-Math.log1p(-r())/n}}randomExponential.source=sourceRandomExponential;return randomExponential}(r);var m=function sourceRandomPareto(r){function randomPareto(n){if((n=+n)<0)throw new RangeError("invalid alpha");n=1/-n;return function(){return Math.pow(1-r(),n)}}randomPareto.source=sourceRandomPareto;return randomPareto}(r);var c=function sourceRandomBernoulli(r){function randomBernoulli(n){if((n=+n)<0||n>1)throw new RangeError("invalid p");return function(){return Math.floor(r()+n)}}randomBernoulli.source=sourceRandomBernoulli;return randomBernoulli}(r);var l=function sourceRandomGeometric(r){function randomGeometric(n){if((n=+n)<0||n>1)throw new RangeError("invalid p");if(0===n)return()=>Infinity;if(1===n)return()=>1;n=Math.log1p(-n);return function(){return 1+Math.floor(Math.log1p(-r())/n)}}randomGeometric.source=sourceRandomGeometric;return randomGeometric}(r);var d=function sourceRandomGamma(r){var n=a.source(r)();function randomGamma(o,a){if((o=+o)<0)throw new RangeError("invalid k");if(0===o)return()=>0;a=null==a?1:+a;if(1===o)return()=>-Math.log1p(-r())*a;var u=(o<1?o+1:o)-1/3,t=1/(3*Math.sqrt(u)),e=o<1?()=>Math.pow(r(),1/o):()=>1;return function(){do{do{var o=n(),i=1+t*o}while(i<=0);i*=i*i;var m=1-r()}while(m>=1-.0331*o*o*o*o&&Math.log(m)>=.5*o*o+u*(1-i+Math.log(i)));return u*i*e()*a}}randomGamma.source=sourceRandomGamma;return randomGamma}(r);var s=function sourceRandomBeta(r){var n=d.source(r);function randomBeta(r,o){var a=n(r),u=n(o);return function(){var r=a();return 0===r?0:r/(r+u())}}randomBeta.source=sourceRandomBeta;return randomBeta}(r);var f=function sourceRandomBinomial(r){var n=l.source(r),o=s.source(r);function randomBinomial(r,a){r=+r;return(a=+a)>=1?()=>r:a<=0?()=>0:function(){var u=0,t=r,e=a;while(t*e>16&&t*(1-e)>16){var i=Math.floor((t+1)*e),m=o(i,t-i+1)();if(m<=e){u+=i;t-=i;e=(e-m)/(1-m)}else{t=i-1;e/=m}}var c=e<.5,l=c?e:1-e,d=n(l);for(var s=d(),f=0;s<=t;++f)s+=d();return u+(c?f:t-f)}}randomBinomial.source=sourceRandomBinomial;return randomBinomial}(r);var h=function sourceRandomWeibull(r){function randomWeibull(n,o,a){var u;if(0===(n=+n))u=r=>-Math.log(r);else{n=1/n;u=r=>Math.pow(r,n)}o=null==o?0:+o;a=null==a?1:+a;return function(){return o+a*u(-Math.log1p(-r()))}}randomWeibull.source=sourceRandomWeibull;return randomWeibull}(r);var v=function sourceRandomCauchy(r){function randomCauchy(n,o){n=null==n?0:+n;o=null==o?1:+o;return function(){return n+o*Math.tan(Math.PI*r())}}randomCauchy.source=sourceRandomCauchy;return randomCauchy}(r);var R=function sourceRandomLogistic(r){function randomLogistic(n,o){n=null==n?0:+n;o=null==o?1:+o;return function(){var a=r();return n+o*Math.log(a/(1-a))}}randomLogistic.source=sourceRandomLogistic;return randomLogistic}(r);var g=function sourceRandomPoisson(r){var n=d.source(r),o=f.source(r);function randomPoisson(a){return function(){var u=0,t=a;while(t>16){var e=Math.floor(.875*t),i=n(e)();if(i>t)return u+o(e-1,t/i)();u+=e;t-=i}for(var m=-Math.log1p(-r()),c=0;m<=t;++c)m-=Math.log1p(-r());return u+c}}randomPoisson.source=sourceRandomPoisson;return randomPoisson}(r);const M=1664525;const B=1013904223;const p=1/4294967296;function lcg(r=Math.random()){let n=0|(0<=r&&r<1?r/p:Math.abs(r));return()=>(n=M*n+B|0,p*(n>>>0))}export{e as randomBates,c as randomBernoulli,s as randomBeta,f as randomBinomial,v as randomCauchy,i as randomExponential,d as randomGamma,l as randomGeometric,o as randomInt,t as randomIrwinHall,lcg as randomLcg,u as randomLogNormal,R as randomLogistic,a as randomNormal,m as randomPareto,g as randomPoisson,n as randomUniform,h as randomWeibull}; diff --git a/vendor/javascript/d3-sankey.js b/vendor/javascript/d3-sankey.js new file mode 100644 index 00000000..704a8471 --- /dev/null +++ b/vendor/javascript/d3-sankey.js @@ -0,0 +1,4 @@ +// d3-sankey@0.12.3 downloaded from https://ga.jspm.io/npm:d3-sankey@0.12.3/dist/d3-sankey.js + +import e from"d3-array";import t from"d3-shape";var n={};(function(o,r){r(n,e,t)})(n,(function(e,t,n){function targetDepth(e){return e.target.depth}function left(e){return e.depth}function right(e,t){return t-1-e.height}function justify(e,t){return e.sourceLinks.length?e.depth:t-1}function center(e){return e.targetLinks.length?e.depth:e.sourceLinks.length?t.min(e.sourceLinks,targetDepth)-1:0}function constant(e){return function(){return e}}function ascendingSourceBreadth(e,t){return ascendingBreadth(e.source,t.source)||e.index-t.index}function ascendingTargetBreadth(e,t){return ascendingBreadth(e.target,t.target)||e.index-t.index}function ascendingBreadth(e,t){return e.y0-t.y0}function value(e){return e.value}function defaultId(e){return e.index}function defaultNodes(e){return e.nodes}function defaultLinks(e){return e.links}function find(e,t){const n=e.get(t);if(!n)throw new Error("missing: "+t);return n}function computeLinkBreadths({nodes:e}){for(const t of e){let e=t.y0;let n=e;for(const n of t.sourceLinks){n.y0=e+n.width/2;e+=n.width}for(const e of t.targetLinks){e.y1=n+e.width/2;n+=e.width}}}function Sankey(){let e=0,n=0,o=1,r=1;let s=24;let i=8,a;let u=defaultId;let c=justify;let f;let l;let d=defaultNodes;let k=defaultLinks;let h=6;function sankey(){const e={nodes:d.apply(null,arguments),links:k.apply(null,arguments)};computeNodeLinks(e);computeNodeValues(e);computeNodeDepths(e);computeNodeHeights(e);computeNodeBreadths(e);computeLinkBreadths(e);return e}sankey.update=function(e){computeLinkBreadths(e);return e};sankey.nodeId=function(e){return arguments.length?(u="function"===typeof e?e:constant(e),sankey):u};sankey.nodeAlign=function(e){return arguments.length?(c="function"===typeof e?e:constant(e),sankey):c};sankey.nodeSort=function(e){return arguments.length?(f=e,sankey):f};sankey.nodeWidth=function(e){return arguments.length?(s=+e,sankey):s};sankey.nodePadding=function(e){return arguments.length?(i=a=+e,sankey):i};sankey.nodes=function(e){return arguments.length?(d="function"===typeof e?e:constant(e),sankey):d};sankey.links=function(e){return arguments.length?(k="function"===typeof e?e:constant(e),sankey):k};sankey.linkSort=function(e){return arguments.length?(l=e,sankey):l};sankey.size=function(t){return arguments.length?(e=n=0,o=+t[0],r=+t[1],sankey):[o-e,r-n]};sankey.extent=function(t){return arguments.length?(e=+t[0][0],o=+t[1][0],n=+t[0][1],r=+t[1][1],sankey):[[e,n],[o,r]]};sankey.iterations=function(e){return arguments.length?(h=+e,sankey):h};function computeNodeLinks({nodes:e,links:t}){for(const[t,n]of e.entries()){n.index=t;n.sourceLinks=[];n.targetLinks=[]}const n=new Map(e.map((t,n)=>[u(t,n,e),t]));for(const[e,o]of t.entries()){o.index=e;let{source:t,target:r}=o;"object"!==typeof t&&(t=o.source=find(n,t));"object"!==typeof r&&(r=o.target=find(n,r));t.sourceLinks.push(o);r.targetLinks.push(o)}if(null!=l)for(const{sourceLinks:t,targetLinks:n}of e){t.sort(l);n.sort(l)}}function computeNodeValues({nodes:e}){for(const n of e)n.value=void 0===n.fixedValue?Math.max(t.sum(n.sourceLinks,value),t.sum(n.targetLinks,value)):n.fixedValue}function computeNodeDepths({nodes:e}){const t=e.length;let n=new Set(e);let o=new Set;let r=0;while(n.size){for(const e of n){e.depth=r;for(const{target:t}of e.sourceLinks)o.add(t)}if(++r>t)throw new Error("circular link");n=o;o=new Set}}function computeNodeHeights({nodes:e}){const t=e.length;let n=new Set(e);let o=new Set;let r=0;while(n.size){for(const e of n){e.height=r;for(const{source:t}of e.targetLinks)o.add(t)}if(++r>t)throw new Error("circular link");n=o;o=new Set}}function computeNodeLayers({nodes:n}){const r=t.max(n,e=>e.depth)+1;const i=(o-e-s)/(r-1);const a=new Array(r);for(const t of n){const n=Math.max(0,Math.min(r-1,Math.floor(c.call(null,t,r))));t.layer=n;t.x0=e+n*i;t.x1=t.x0+s;a[n]?a[n].push(t):a[n]=[t]}if(f)for(const e of a)e.sort(f);return a}function initializeNodeBreadths(e){const o=t.min(e,e=>(r-n-(e.length-1)*a)/t.sum(e,value));for(const t of e){let e=n;for(const n of t){n.y0=e;n.y1=e+n.value*o;e=n.y1+a;for(const e of n.sourceLinks)e.width=e.value*o}e=(r-e+a)/(t.length+1);for(let n=0;ne.length)-1));initializeNodeBreadths(o);for(let e=0;e0))continue;let r=(n/o-e.y0)*t;e.y0+=r;e.y1+=r;reorderNodeLinks(e)}void 0===f&&r.sort(ascendingBreadth);resolveCollisions(r,n)}}function relaxRightToLeft(e,t,n){for(let o=e.length,r=o-2;r>=0;--r){const o=e[r];for(const e of o){let n=0;let o=0;for(const{target:t,value:r}of e.sourceLinks){let s=r*(t.layer-e.layer);n+=sourceTop(e,t)*s;o+=s}if(!(o>0))continue;let r=(n/o-e.y0)*t;e.y0+=r;e.y1+=r;reorderNodeLinks(e)}void 0===f&&o.sort(ascendingBreadth);resolveCollisions(o,n)}}function resolveCollisions(e,t){const o=e.length>>1;const s=e[o];resolveCollisionsBottomToTop(e,s.y0-a,o-1,t);resolveCollisionsTopToBottom(e,s.y1+a,o+1,t);resolveCollisionsBottomToTop(e,r,e.length-1,t);resolveCollisionsTopToBottom(e,n,0,t)}function resolveCollisionsTopToBottom(e,t,n,o){for(;n1e-6&&(r.y0+=s,r.y1+=s);t=r.y1+a}}function resolveCollisionsBottomToTop(e,t,n,o){for(;n>=0;--n){const r=e[n];const s=(r.y1-t)*o;s>1e-6&&(r.y0-=s,r.y1-=s);t=r.y0-a}}function reorderNodeLinks({sourceLinks:e,targetLinks:t}){if(void 0===l){for(const{source:{sourceLinks:e}}of t)e.sort(ascendingTargetBreadth);for(const{target:{targetLinks:t}}of e)t.sort(ascendingSourceBreadth)}}function reorderLinks(e){if(void 0===l)for(const{sourceLinks:t,targetLinks:n}of e){t.sort(ascendingTargetBreadth);n.sort(ascendingSourceBreadth)}}function targetTop(e,t){let n=e.y0-(e.sourceLinks.length-1)*a/2;for(const{target:o,width:r}of e.sourceLinks){if(o===t)break;n+=r+a}for(const{source:o,width:r}of t.targetLinks){if(o===e)break;n-=r}return n}function sourceTop(e,t){let n=t.y0-(t.targetLinks.length-1)*a/2;for(const{source:o,width:r}of t.targetLinks){if(o===e)break;n+=r+a}for(const{target:o,width:r}of e.sourceLinks){if(o===t)break;n-=r}return n}return sankey}function horizontalSource(e){return[e.source.x1,e.y0]}function horizontalTarget(e){return[e.target.x0,e.y1]}function sankeyLinkHorizontal(){return n.linkHorizontal().source(horizontalSource).target(horizontalTarget)}e.sankey=Sankey;e.sankeyCenter=center;e.sankeyJustify=justify;e.sankeyLeft=left;e.sankeyLinkHorizontal=sankeyLinkHorizontal;e.sankeyRight=right;Object.defineProperty(e,"__esModule",{value:true})}));const o=n.sankey,r=n.sankeyCenter,s=n.sankeyJustify,i=n.sankeyLeft,a=n.sankeyLinkHorizontal,u=n.sankeyRight,c=n.__esModule;export default n;export{c as __esModule,o as sankey,r as sankeyCenter,s as sankeyJustify,i as sankeyLeft,a as sankeyLinkHorizontal,u as sankeyRight}; + diff --git a/vendor/javascript/d3-scale-chromatic.js b/vendor/javascript/d3-scale-chromatic.js index 6f6d4230..d3df76ab 100644 --- a/vendor/javascript/d3-scale-chromatic.js +++ b/vendor/javascript/d3-scale-chromatic.js @@ -1,2 +1,4 @@ -import{interpolateRgbBasis as f,interpolateCubehelixLong as e}from"d3-interpolate";import{cubehelix as a,rgb as d}from"d3-color";function colors(f){var e=f.length/6|0,a=new Array(e),d=0;while(df(e[e.length-1]);var v=new Array(3).concat("d8b365f5f5f55ab4ac","a6611adfc27d80cdc1018571","a6611adfc27df5f5f580cdc1018571","8c510ad8b365f6e8c3c7eae55ab4ac01665e","8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e","8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e","8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e","5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30","5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30").map(colors);var p=ramp$1(v);var h=new Array(3).concat("af8dc3f7f7f77fbf7b","7b3294c2a5cfa6dba0008837","7b3294c2a5cff7f7f7a6dba0008837","762a83af8dc3e7d4e8d9f0d37fbf7b1b7837","762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837","762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837","762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837","40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b","40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b").map(colors);var u=ramp$1(h);var w=new Array(3).concat("e9a3c9f7f7f7a1d76a","d01c8bf1b6dab8e1864dac26","d01c8bf1b6daf7f7f7b8e1864dac26","c51b7de9a3c9fde0efe6f5d0a1d76a4d9221","c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221","c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221","c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221","8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419","8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419").map(colors);var M=ramp$1(w);var y=new Array(3).concat("998ec3f7f7f7f1a340","5e3c99b2abd2fdb863e66101","5e3c99b2abd2f7f7f7fdb863e66101","542788998ec3d8daebfee0b6f1a340b35806","542788998ec3d8daebf7f7f7fee0b6f1a340b35806","5427888073acb2abd2d8daebfee0b6fdb863e08214b35806","5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806","2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08","2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08").map(colors);var A=ramp$1(y);var P=new Array(3).concat("ef8a62f7f7f767a9cf","ca0020f4a58292c5de0571b0","ca0020f4a582f7f7f792c5de0571b0","b2182bef8a62fddbc7d1e5f067a9cf2166ac","b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac","b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac","b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac","67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061","67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061").map(colors);var B=ramp$1(P);var G=new Array(3).concat("ef8a62ffffff999999","ca0020f4a582bababa404040","ca0020f4a582ffffffbababa404040","b2182bef8a62fddbc7e0e0e09999994d4d4d","b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d","b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d","b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d","67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a","67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a").map(colors);var R=ramp$1(G);var Y=new Array(3).concat("fc8d59ffffbf91bfdb","d7191cfdae61abd9e92c7bb6","d7191cfdae61ffffbfabd9e92c7bb6","d73027fc8d59fee090e0f3f891bfdb4575b4","d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4","d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4","d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4","a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695","a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695").map(colors);var x=ramp$1(Y);var g=new Array(3).concat("fc8d59ffffbf91cf60","d7191cfdae61a6d96a1a9641","d7191cfdae61ffffbfa6d96a1a9641","d73027fc8d59fee08bd9ef8b91cf601a9850","d73027fc8d59fee08bffffbfd9ef8b91cf601a9850","d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850","d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850","a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837","a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837").map(colors);var O=ramp$1(g);var S=new Array(3).concat("fc8d59ffffbf99d594","d7191cfdae61abdda42b83ba","d7191cfdae61ffffbfabdda42b83ba","d53e4ffc8d59fee08be6f59899d5943288bd","d53e4ffc8d59fee08bffffbfe6f59899d5943288bd","d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd","d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd","9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2","9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2").map(colors);var C=ramp$1(S);var I=new Array(3).concat("e5f5f999d8c92ca25f","edf8fbb2e2e266c2a4238b45","edf8fbb2e2e266c2a42ca25f006d2c","edf8fbccece699d8c966c2a42ca25f006d2c","edf8fbccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b").map(colors);var D=ramp$1(I);var T=new Array(3).concat("e0ecf49ebcda8856a7","edf8fbb3cde38c96c688419d","edf8fbb3cde38c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b").map(colors);var k=ramp$1(T);var V=new Array(3).concat("e0f3dba8ddb543a2ca","f0f9e8bae4bc7bccc42b8cbe","f0f9e8bae4bc7bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081").map(colors);var W=ramp$1(V);var j=new Array(3).concat("fee8c8fdbb84e34a33","fef0d9fdcc8afc8d59d7301f","fef0d9fdcc8afc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000").map(colors);var q=ramp$1(j);var z=new Array(3).concat("ece2f0a6bddb1c9099","f6eff7bdc9e167a9cf02818a","f6eff7bdc9e167a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636").map(colors);var E=ramp$1(z);var F=new Array(3).concat("ece7f2a6bddb2b8cbe","f1eef6bdc9e174a9cf0570b0","f1eef6bdc9e174a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858").map(colors);var H=ramp$1(F);var J=new Array(3).concat("e7e1efc994c7dd1c77","f1eef6d7b5d8df65b0ce1256","f1eef6d7b5d8df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f").map(colors);var K=ramp$1(J);var L=new Array(3).concat("fde0ddfa9fb5c51b8a","feebe2fbb4b9f768a1ae017e","feebe2fbb4b9f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a").map(colors);var N=ramp$1(L);var Q=new Array(3).concat("edf8b17fcdbb2c7fb8","ffffcca1dab441b6c4225ea8","ffffcca1dab441b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58").map(colors);var U=ramp$1(Q);var X=new Array(3).concat("f7fcb9addd8e31a354","ffffccc2e69978c679238443","ffffccc2e69978c67931a354006837","ffffccd9f0a3addd8e78c67931a354006837","ffffccd9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529").map(colors);var Z=ramp$1(X);var $=new Array(3).concat("fff7bcfec44fd95f0e","ffffd4fed98efe9929cc4c02","ffffd4fed98efe9929d95f0e993404","ffffd4fee391fec44ffe9929d95f0e993404","ffffd4fee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506").map(colors);var _=ramp$1($);var ff=new Array(3).concat("ffeda0feb24cf03b20","ffffb2fecc5cfd8d3ce31a1c","ffffb2fecc5cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026").map(colors);var ef=ramp$1(ff);var af=new Array(3).concat("deebf79ecae13182bd","eff3ffbdd7e76baed62171b5","eff3ffbdd7e76baed63182bd08519c","eff3ffc6dbef9ecae16baed63182bd08519c","eff3ffc6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b").map(colors);var df=ramp$1(af);var cf=new Array(3).concat("e5f5e0a1d99b31a354","edf8e9bae4b374c476238b45","edf8e9bae4b374c47631a354006d2c","edf8e9c7e9c0a1d99b74c47631a354006d2c","edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b").map(colors);var bf=ramp$1(cf);var rf=new Array(3).concat("f0f0f0bdbdbd636363","f7f7f7cccccc969696525252","f7f7f7cccccc969696636363252525","f7f7f7d9d9d9bdbdbd969696636363252525","f7f7f7d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000").map(colors);var of=ramp$1(rf);var sf=new Array(3).concat("efedf5bcbddc756bb1","f2f0f7cbc9e29e9ac86a51a3","f2f0f7cbc9e29e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d").map(colors);var tf=ramp$1(sf);var nf=new Array(3).concat("fee0d2fc9272de2d26","fee5d9fcae91fb6a4acb181d","fee5d9fcae91fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d").map(colors);var lf=ramp$1(nf);var mf=new Array(3).concat("fee6cefdae6be6550d","feeddefdbe85fd8d3cd94701","feeddefdbe85fd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704").map(colors);var vf=ramp$1(mf);function cividis(f){f=Math.max(0,Math.min(1,f));return"rgb("+Math.max(0,Math.min(255,Math.round(-4.54-f*(35.34-f*(2381.73-f*(6402.7-f*(7024.72-2710.57*f)))))))+", "+Math.max(0,Math.min(255,Math.round(32.49+f*(170.73+f*(52.82-f*(131.46-f*(176.58-67.37*f)))))))+", "+Math.max(0,Math.min(255,Math.round(81.24+f*(442.36-f*(2482.43-f*(6167.24-f*(6614.94-2475.67*f)))))))+")"}var pf=e(a(300,.5,0),a(-240,.5,1));var hf=e(a(-100,.75,.35),a(80,1.5,.8));var uf=e(a(260,.75,.35),a(80,1.5,.8));var wf=a();function rainbow(f){(f<0||f>1)&&(f-=Math.floor(f));var e=Math.abs(f-.5);wf.h=360*f-100;wf.s=1.5-1.5*e;wf.l=.8-.9*e;return wf+""}var Mf=d(),yf=Math.PI/3,Af=2*Math.PI/3;function sinebow(f){var e;f=(.5-f)*Math.PI;Mf.r=255*(e=Math.sin(f))*e;Mf.g=255*(e=Math.sin(f+yf))*e;Mf.b=255*(e=Math.sin(f+Af))*e;return Mf+""}function turbo(f){f=Math.max(0,Math.min(1,f));return"rgb("+Math.max(0,Math.min(255,Math.round(34.61+f*(1172.33-f*(10793.56-f*(33300.12-f*(38394.49-14825.05*f)))))))+", "+Math.max(0,Math.min(255,Math.round(23.31+f*(557.33+f*(1225.33-f*(3574.96-f*(1073.77+707.56*f)))))))+", "+Math.max(0,Math.min(255,Math.round(27.2+f*(3211.1-f*(15327.97-f*(27814-f*(22569.18-6838.66*f)))))))+")"}function ramp(f){var e=f.length;return function(a){return f[Math.max(0,Math.min(e-1,Math.floor(a*e)))]}}var Pf=ramp(colors("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725"));var Bf=ramp(colors("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf"));var Gf=ramp(colors("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4"));var Rf=ramp(colors("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));export{df as interpolateBlues,p as interpolateBrBG,D as interpolateBuGn,k as interpolateBuPu,cividis as interpolateCividis,uf as interpolateCool,pf as interpolateCubehelixDefault,W as interpolateGnBu,bf as interpolateGreens,of as interpolateGreys,Gf as interpolateInferno,Bf as interpolateMagma,q as interpolateOrRd,vf as interpolateOranges,u as interpolatePRGn,M as interpolatePiYG,Rf as interpolatePlasma,H as interpolatePuBu,E as interpolatePuBuGn,A as interpolatePuOr,K as interpolatePuRd,tf as interpolatePurples,rainbow as interpolateRainbow,B as interpolateRdBu,R as interpolateRdGy,N as interpolateRdPu,x as interpolateRdYlBu,O as interpolateRdYlGn,lf as interpolateReds,sinebow as interpolateSinebow,C as interpolateSpectral,turbo as interpolateTurbo,Pf as interpolateViridis,hf as interpolateWarm,Z as interpolateYlGn,U as interpolateYlGnBu,_ as interpolateYlOrBr,ef as interpolateYlOrRd,b as schemeAccent,af as schemeBlues,v as schemeBrBG,I as schemeBuGn,T as schemeBuPu,c as schemeCategory10,r as schemeDark2,V as schemeGnBu,cf as schemeGreens,rf as schemeGreys,j as schemeOrRd,mf as schemeOranges,h as schemePRGn,o as schemePaired,s as schemePastel1,t as schemePastel2,w as schemePiYG,F as schemePuBu,z as schemePuBuGn,y as schemePuOr,J as schemePuRd,sf as schemePurples,P as schemeRdBu,G as schemeRdGy,L as schemeRdPu,Y as schemeRdYlBu,g as schemeRdYlGn,nf as schemeReds,n as schemeSet1,l as schemeSet2,m as schemeSet3,S as schemeSpectral,i as schemeTableau10,X as schemeYlGn,Q as schemeYlGnBu,$ as schemeYlOrBr,ff as schemeYlOrRd}; +// d3-scale-chromatic@3.1.0 downloaded from https://ga.jspm.io/npm:d3-scale-chromatic@3.1.0/src/index.js + +import{interpolateRgbBasis as f,interpolateCubehelixLong as e}from"d3-interpolate";import{cubehelix as a,rgb as d}from"d3-color";function colors(f){var e=f.length/6|0,a=new Array(e),d=0;while(df(e[e.length-1]);var h=new Array(3).concat("d8b365f5f5f55ab4ac","a6611adfc27d80cdc1018571","a6611adfc27df5f5f580cdc1018571","8c510ad8b365f6e8c3c7eae55ab4ac01665e","8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e","8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e","8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e","5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30","5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30").map(colors);var p=ramp$1(h);var u=new Array(3).concat("af8dc3f7f7f77fbf7b","7b3294c2a5cfa6dba0008837","7b3294c2a5cff7f7f7a6dba0008837","762a83af8dc3e7d4e8d9f0d37fbf7b1b7837","762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837","762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837","762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837","40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b","40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b").map(colors);var w=ramp$1(u);var M=new Array(3).concat("e9a3c9f7f7f7a1d76a","d01c8bf1b6dab8e1864dac26","d01c8bf1b6daf7f7f7b8e1864dac26","c51b7de9a3c9fde0efe6f5d0a1d76a4d9221","c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221","c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221","c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221","8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419","8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419").map(colors);var y=ramp$1(M);var A=new Array(3).concat("998ec3f7f7f7f1a340","5e3c99b2abd2fdb863e66101","5e3c99b2abd2f7f7f7fdb863e66101","542788998ec3d8daebfee0b6f1a340b35806","542788998ec3d8daebf7f7f7fee0b6f1a340b35806","5427888073acb2abd2d8daebfee0b6fdb863e08214b35806","5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806","2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08","2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08").map(colors);var P=ramp$1(A);var B=new Array(3).concat("ef8a62f7f7f767a9cf","ca0020f4a58292c5de0571b0","ca0020f4a582f7f7f792c5de0571b0","b2182bef8a62fddbc7d1e5f067a9cf2166ac","b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac","b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac","b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac","67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061","67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061").map(colors);var G=ramp$1(B);var R=new Array(3).concat("ef8a62ffffff999999","ca0020f4a582bababa404040","ca0020f4a582ffffffbababa404040","b2182bef8a62fddbc7e0e0e09999994d4d4d","b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d","b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d","b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d","67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a","67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a").map(colors);var Y=ramp$1(R);var x=new Array(3).concat("fc8d59ffffbf91bfdb","d7191cfdae61abd9e92c7bb6","d7191cfdae61ffffbfabd9e92c7bb6","d73027fc8d59fee090e0f3f891bfdb4575b4","d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4","d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4","d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4","a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695","a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695").map(colors);var O=ramp$1(x);var g=new Array(3).concat("fc8d59ffffbf91cf60","d7191cfdae61a6d96a1a9641","d7191cfdae61ffffbfa6d96a1a9641","d73027fc8d59fee08bd9ef8b91cf601a9850","d73027fc8d59fee08bffffbfd9ef8b91cf601a9850","d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850","d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850","a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837","a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837").map(colors);var S=ramp$1(g);var C=new Array(3).concat("fc8d59ffffbf99d594","d7191cfdae61abdda42b83ba","d7191cfdae61ffffbfabdda42b83ba","d53e4ffc8d59fee08be6f59899d5943288bd","d53e4ffc8d59fee08bffffbfe6f59899d5943288bd","d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd","d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd","9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2","9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2").map(colors);var I=ramp$1(C);var D=new Array(3).concat("e5f5f999d8c92ca25f","edf8fbb2e2e266c2a4238b45","edf8fbb2e2e266c2a42ca25f006d2c","edf8fbccece699d8c966c2a42ca25f006d2c","edf8fbccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b").map(colors);var T=ramp$1(D);var k=new Array(3).concat("e0ecf49ebcda8856a7","edf8fbb3cde38c96c688419d","edf8fbb3cde38c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b").map(colors);var V=ramp$1(k);var W=new Array(3).concat("e0f3dba8ddb543a2ca","f0f9e8bae4bc7bccc42b8cbe","f0f9e8bae4bc7bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081").map(colors);var j=ramp$1(W);var q=new Array(3).concat("fee8c8fdbb84e34a33","fef0d9fdcc8afc8d59d7301f","fef0d9fdcc8afc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000").map(colors);var z=ramp$1(q);var E=new Array(3).concat("ece2f0a6bddb1c9099","f6eff7bdc9e167a9cf02818a","f6eff7bdc9e167a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636").map(colors);var F=ramp$1(E);var H=new Array(3).concat("ece7f2a6bddb2b8cbe","f1eef6bdc9e174a9cf0570b0","f1eef6bdc9e174a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858").map(colors);var J=ramp$1(H);var K=new Array(3).concat("e7e1efc994c7dd1c77","f1eef6d7b5d8df65b0ce1256","f1eef6d7b5d8df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f").map(colors);var L=ramp$1(K);var N=new Array(3).concat("fde0ddfa9fb5c51b8a","feebe2fbb4b9f768a1ae017e","feebe2fbb4b9f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a").map(colors);var Q=ramp$1(N);var U=new Array(3).concat("edf8b17fcdbb2c7fb8","ffffcca1dab441b6c4225ea8","ffffcca1dab441b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58").map(colors);var X=ramp$1(U);var Z=new Array(3).concat("f7fcb9addd8e31a354","ffffccc2e69978c679238443","ffffccc2e69978c67931a354006837","ffffccd9f0a3addd8e78c67931a354006837","ffffccd9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529").map(colors);var $=ramp$1(Z);var _=new Array(3).concat("fff7bcfec44fd95f0e","ffffd4fed98efe9929cc4c02","ffffd4fed98efe9929d95f0e993404","ffffd4fee391fec44ffe9929d95f0e993404","ffffd4fee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506").map(colors);var ff=ramp$1(_);var ef=new Array(3).concat("ffeda0feb24cf03b20","ffffb2fecc5cfd8d3ce31a1c","ffffb2fecc5cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026").map(colors);var af=ramp$1(ef);var df=new Array(3).concat("deebf79ecae13182bd","eff3ffbdd7e76baed62171b5","eff3ffbdd7e76baed63182bd08519c","eff3ffc6dbef9ecae16baed63182bd08519c","eff3ffc6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b").map(colors);var cf=ramp$1(df);var bf=new Array(3).concat("e5f5e0a1d99b31a354","edf8e9bae4b374c476238b45","edf8e9bae4b374c47631a354006d2c","edf8e9c7e9c0a1d99b74c47631a354006d2c","edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b").map(colors);var rf=ramp$1(bf);var of=new Array(3).concat("f0f0f0bdbdbd636363","f7f7f7cccccc969696525252","f7f7f7cccccc969696636363252525","f7f7f7d9d9d9bdbdbd969696636363252525","f7f7f7d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000").map(colors);var sf=ramp$1(of);var tf=new Array(3).concat("efedf5bcbddc756bb1","f2f0f7cbc9e29e9ac86a51a3","f2f0f7cbc9e29e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d").map(colors);var nf=ramp$1(tf);var lf=new Array(3).concat("fee0d2fc9272de2d26","fee5d9fcae91fb6a4acb181d","fee5d9fcae91fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d").map(colors);var mf=ramp$1(lf);var vf=new Array(3).concat("fee6cefdae6be6550d","feeddefdbe85fd8d3cd94701","feeddefdbe85fd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704").map(colors);var hf=ramp$1(vf);function cividis(f){f=Math.max(0,Math.min(1,f));return"rgb("+Math.max(0,Math.min(255,Math.round(-4.54-f*(35.34-f*(2381.73-f*(6402.7-f*(7024.72-f*2710.57)))))))+", "+Math.max(0,Math.min(255,Math.round(32.49+f*(170.73+f*(52.82-f*(131.46-f*(176.58-f*67.37)))))))+", "+Math.max(0,Math.min(255,Math.round(81.24+f*(442.36-f*(2482.43-f*(6167.24-f*(6614.94-f*2475.67)))))))+")"}var pf=e(a(300,.5,0),a(-240,.5,1));var uf=e(a(-100,.75,.35),a(80,1.5,.8));var wf=e(a(260,.75,.35),a(80,1.5,.8));var Mf=a();function rainbow(f){(f<0||f>1)&&(f-=Math.floor(f));var e=Math.abs(f-.5);Mf.h=360*f-100;Mf.s=1.5-1.5*e;Mf.l=.8-.9*e;return Mf+""}var yf=d(),Af=Math.PI/3,Pf=Math.PI*2/3;function sinebow(f){var e;f=(.5-f)*Math.PI;yf.r=255*(e=Math.sin(f))*e;yf.g=255*(e=Math.sin(f+Af))*e;yf.b=255*(e=Math.sin(f+Pf))*e;return yf+""}function turbo(f){f=Math.max(0,Math.min(1,f));return"rgb("+Math.max(0,Math.min(255,Math.round(34.61+f*(1172.33-f*(10793.56-f*(33300.12-f*(38394.49-f*14825.05)))))))+", "+Math.max(0,Math.min(255,Math.round(23.31+f*(557.33+f*(1225.33-f*(3574.96-f*(1073.77+f*707.56)))))))+", "+Math.max(0,Math.min(255,Math.round(27.2+f*(3211.1-f*(15327.97-f*(27814-f*(22569.18-f*6838.66)))))))+")"}function ramp(f){var e=f.length;return function(a){return f[Math.max(0,Math.min(e-1,Math.floor(a*e)))]}}var Bf=ramp(colors("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725"));var Gf=ramp(colors("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf"));var Rf=ramp(colors("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4"));var Yf=ramp(colors("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));export{cf as interpolateBlues,p as interpolateBrBG,T as interpolateBuGn,V as interpolateBuPu,cividis as interpolateCividis,wf as interpolateCool,pf as interpolateCubehelixDefault,j as interpolateGnBu,rf as interpolateGreens,sf as interpolateGreys,Rf as interpolateInferno,Gf as interpolateMagma,z as interpolateOrRd,hf as interpolateOranges,w as interpolatePRGn,y as interpolatePiYG,Yf as interpolatePlasma,J as interpolatePuBu,F as interpolatePuBuGn,P as interpolatePuOr,L as interpolatePuRd,nf as interpolatePurples,rainbow as interpolateRainbow,G as interpolateRdBu,Y as interpolateRdGy,Q as interpolateRdPu,O as interpolateRdYlBu,S as interpolateRdYlGn,mf as interpolateReds,sinebow as interpolateSinebow,I as interpolateSpectral,turbo as interpolateTurbo,Bf as interpolateViridis,uf as interpolateWarm,$ as interpolateYlGn,X as interpolateYlGnBu,ff as interpolateYlOrBr,af as interpolateYlOrRd,b as schemeAccent,df as schemeBlues,h as schemeBrBG,D as schemeBuGn,k as schemeBuPu,c as schemeCategory10,r as schemeDark2,W as schemeGnBu,bf as schemeGreens,of as schemeGreys,o as schemeObservable10,q as schemeOrRd,vf as schemeOranges,u as schemePRGn,s as schemePaired,t as schemePastel1,n as schemePastel2,M as schemePiYG,H as schemePuBu,E as schemePuBuGn,A as schemePuOr,K as schemePuRd,tf as schemePurples,B as schemeRdBu,R as schemeRdGy,N as schemeRdPu,x as schemeRdYlBu,g as schemeRdYlGn,lf as schemeReds,l as schemeSet1,m as schemeSet2,i as schemeSet3,C as schemeSpectral,v as schemeTableau10,Z as schemeYlGn,U as schemeYlGnBu,_ as schemeYlOrBr,ef as schemeYlOrRd}; diff --git a/vendor/javascript/d3-scale.js b/vendor/javascript/d3-scale.js index 2cdc5594..99910a88 100644 --- a/vendor/javascript/d3-scale.js +++ b/vendor/javascript/d3-scale.js @@ -1,2 +1,4 @@ +// d3-scale@4.0.2 downloaded from https://ga.jspm.io/npm:d3-scale@4.0.2/src/index.js + import{InternMap as n,range as e,bisect as t,tickStep as r,ticks as a,tickIncrement as i,quantileSorted as o,ascending as l,quantile as u}from"d3-array";import{interpolate as c,interpolateNumber as s,interpolateRound as f,piecewise as g}from"d3-interpolate";import{formatSpecifier as p,precisionFixed as h,precisionRound as m,precisionPrefix as d,formatPrefix as y,format as v}from"d3-format";import{timeTicks as w,timeTickInterval as M,timeYear as q,timeMonth as k,timeWeek as b,timeDay as x,timeHour as $,timeMinute as N,timeSecond as S,utcTicks as I,utcTickInterval as R,utcYear as A,utcMonth as L,utcWeek as P,utcDay as D,utcHour as E,utcMinute as F,utcSecond as z}from"d3-time";import{timeFormat as O,utcFormat as Q}from"d3-time-format";function initRange(n,e){switch(arguments.length){case 0:break;case 1:this.range(n);break;default:this.range(e).domain(n);break}return this}function initInterpolator(n,e){switch(arguments.length){case 0:break;case 1:"function"===typeof n?this.interpolator(n):this.range(n);break;default:this.domain(n);"function"===typeof e?this.interpolator(e):this.range(e);break}return this}const T=Symbol("implicit");function ordinal(){var e=new n,t=[],r=[],a=T;function scale(n){let i=e.get(n);if(void 0===i){if(a!==T)return a;e.set(n,i=t.push(n)-1)}return r[i%r.length]}scale.domain=function(r){if(!arguments.length)return t.slice();t=[],e=new n;for(const n of r)e.has(n)||e.set(n,t.push(n)-1);return scale};scale.range=function(n){return arguments.length?(r=Array.from(n),scale):r.slice()};scale.unknown=function(n){return arguments.length?(a=n,scale):a};scale.copy=function(){return ordinal(t,r).unknown(a)};initRange.apply(scale,arguments);return scale}function band(){var n,t,r=ordinal().unknown(void 0),a=r.domain,i=r.range,o=0,l=1,u=false,c=0,s=0,f=.5;delete r.unknown;function rescale(){var r=a().length,g=le&&(t=n,n=e,e=t);return function(t){return Math.max(n,Math.min(e,t))}}function bimap(n,e,t){var r=n[0],a=n[1],i=e[0],o=e[1];a2?polymap:bimap;a=i=null;return scale}function scale(e){return null==e||isNaN(e=+e)?t:(a||(a=r(o.map(n),l,u)))(n(g(e)))}scale.invert=function(t){return g(e((i||(i=r(l,o.map(n),s)))(t)))};scale.domain=function(n){return arguments.length?(o=Array.from(n,number$1),rescale()):o.slice()};scale.range=function(n){return arguments.length?(l=Array.from(n),rescale()):l.slice()};scale.rangeRound=function(n){return l=Array.from(n),u=f,rescale()};scale.clamp=function(n){return arguments.length?(g=!!n||identity$1,rescale()):g!==identity$1};scale.interpolate=function(n){return arguments.length?(u=n,rescale()):u};scale.unknown=function(n){return arguments.length?(t=n,scale):t};return function(t,r){n=t,e=r;return rescale()}}function continuous(){return transformer$2()(identity$1,identity$1)}function tickFormat(n,e,t,a){var i,o=r(n,e,t);a=p(null==a?",f":a);switch(a.type){case"s":var l=Math.max(Math.abs(n),Math.abs(e));null!=a.precision||isNaN(i=d(o,l))||(a.precision=i);return y(a,l);case"":case"e":case"g":case"p":case"r":null!=a.precision||isNaN(i=m(o,Math.max(Math.abs(n),Math.abs(e))))||(a.precision=i-("e"===a.type));break;case"f":case"%":null!=a.precision||isNaN(i=h(o))||(a.precision=i-2*("%"===a.type));break}return v(a)}function linearish(n){var e=n.domain;n.ticks=function(n){var t=e();return a(t[0],t[t.length-1],null==n?10:n)};n.tickFormat=function(n,t){var r=e();return tickFormat(r[0],r[r.length-1],null==n?10:n,t)};n.nice=function(t){null==t&&(t=10);var r=e();var a=0;var o=r.length-1;var l=r[a];var u=r[o];var c;var s;var f=10;if(u0){s=i(l,u,t);if(s===c){r[a]=l;r[o]=u;return e(r)}if(s>0){l=Math.floor(l/s)*s;u=Math.ceil(u/s)*s}else{if(!(s<0))break;l=Math.ceil(l*s)/s;u=Math.floor(u*s)/s}c=s}return n};return n}function linear(){var n=continuous();n.copy=function(){return copy$1(n,linear())};initRange.apply(n,arguments);return linearish(n)}function identity(n){var e;function scale(n){return null==n||isNaN(n=+n)?e:n}scale.invert=scale;scale.domain=scale.range=function(e){return arguments.length?(n=Array.from(e,number$1),scale):n.slice()};scale.unknown=function(n){return arguments.length?(e=n,scale):e};scale.copy=function(){return identity(n).unknown(e)};n=arguments.length?Array.from(n,number$1):[0,1];return linearish(scale)}function nice(n,e){n=n.slice();var t,r=0,a=n.length-1,i=n[r],o=n[a];if(oMath.pow(n,e)}function logp(n){return n===Math.E?Math.log:10===n&&Math.log10||2===n&&Math.log2||(n=Math.log(n),e=>Math.log(e)/n)}function reflect(n){return(e,t)=>-n(-e,t)}function loggish(n){const e=n(transformLog,transformExp);const t=e.domain;let r=10;let i;let o;function rescale(){i=logp(r),o=powp(r);if(t()[0]<0){i=reflect(i),o=reflect(o);n(transformLogn,transformExpn)}else n(transformLog,transformExp);return e}e.base=function(n){return arguments.length?(r=+n,rescale()):r};e.domain=function(n){return arguments.length?(t(n),rescale()):t()};e.ticks=n=>{const e=t();let l=e[0];let u=e[e.length-1];const c=u0)for(;s<=f;++s)for(g=1;gu)break;m.push(p)}}else for(;s<=f;++s)for(g=r-1;g>=1;--g){p=s>0?g/o(-s):g*o(s);if(!(pu)break;m.push(p)}}2*m.length{null==n&&(n=10);null==t&&(t=10===r?"s":",");if("function"!==typeof t){r%1||null!=(t=p(t)).precision||(t.trim=true);t=v(t)}if(Infinity===n)return t;const a=Math.max(1,r*n/e.ticks().length);return n=>{let e=n/o(Math.round(i(n)));e*rt(nice(t(),{floor:n=>o(Math.floor(i(n))),ceil:n=>o(Math.ceil(i(n)))}));return e}function log(){const n=loggish(transformer$2()).domain([1,10]);n.copy=()=>copy$1(n,log()).base(n.base());initRange.apply(n,arguments);return n}function transformSymlog(n){return function(e){return Math.sign(e)*Math.log1p(Math.abs(e/n))}}function transformSymexp(n){return function(e){return Math.sign(e)*Math.expm1(Math.abs(e))*n}}function symlogish(n){var e=1,t=n(transformSymlog(e),transformSymexp(e));t.constant=function(t){return arguments.length?n(transformSymlog(e=+t),transformSymexp(e)):e};return linearish(t)}function symlog(){var n=symlogish(transformer$2());n.copy=function(){return copy$1(n,symlog()).constant(n.constant())};return initRange.apply(n,arguments)}function transformPow(n){return function(e){return e<0?-Math.pow(-e,n):Math.pow(e,n)}}function transformSqrt(n){return n<0?-Math.sqrt(-n):Math.sqrt(n)}function transformSquare(n){return n<0?-n*n:n*n}function powish(n){var e=n(identity$1,identity$1),t=1;function rescale(){return 1===t?n(identity$1,identity$1):.5===t?n(transformSqrt,transformSquare):n(transformPow(t),transformPow(1/t))}e.exponent=function(n){return arguments.length?(t=+n,rescale()):t};return linearish(e)}function pow(){var n=powish(transformer$2());n.copy=function(){return copy$1(n,pow()).exponent(n.exponent())};initRange.apply(n,arguments);return n}function sqrt(){return pow.apply(null,arguments).exponent(.5)}function square(n){return Math.sign(n)*n*n}function unsquare(n){return Math.sign(n)*Math.sqrt(Math.abs(n))}function radial(){var n,e=continuous(),t=[0,1],r=false;function scale(t){var a=unsquare(e(t));return isNaN(a)?n:r?Math.round(a):a}scale.invert=function(n){return e.invert(square(n))};scale.domain=function(n){return arguments.length?(e.domain(n),scale):e.domain()};scale.range=function(n){return arguments.length?(e.range((t=Array.from(n,number$1)).map(square)),scale):t.slice()};scale.rangeRound=function(n){return scale.range(n).round(true)};scale.round=function(n){return arguments.length?(r=!!n,scale):r};scale.clamp=function(n){return arguments.length?(e.clamp(n),scale):e.clamp()};scale.unknown=function(e){return arguments.length?(n=e,scale):n};scale.copy=function(){return radial(e.domain(),t).round(r).clamp(e.clamp()).unknown(n)};initRange.apply(scale,arguments);return linearish(scale)}function quantile(){var n,e=[],r=[],a=[];function rescale(){var n=0,t=Math.max(1,r.length);a=new Array(t-1);while(++n0?a[t-1]:e[0],t=a?[i[a-1],r]:[i[t-1],i[t]]};scale.unknown=function(e){return arguments.length?(n=e,scale):scale};scale.thresholds=function(){return i.slice()};scale.copy=function(){return quantize().domain([e,r]).range(o).unknown(n)};return initRange.apply(linearish(scale),arguments)}function threshold(){var n,e=[.5],r=[0,1],a=1;function scale(i){return null!=i&&i<=i?r[t(e,i,0,a)]:n}scale.domain=function(n){return arguments.length?(e=Array.from(n),a=Math.min(e.length,r.length-1),scale):e.slice()};scale.range=function(n){return arguments.length?(r=Array.from(n),a=Math.min(e.length,r.length-1),scale):r.slice()};scale.invertExtent=function(n){var t=r.indexOf(n);return[e[t-1],e[t]]};scale.unknown=function(e){return arguments.length?(n=e,scale):n};scale.copy=function(){return threshold().domain(e).range(r).unknown(n)};return initRange.apply(scale,arguments)}function date(n){return new Date(n)}function number(n){return n instanceof Date?+n:+new Date(+n)}function calendar(n,e,t,r,a,i,o,l,u,c){var s=continuous(),f=s.invert,g=s.domain;var p=c(".%L"),h=c(":%S"),m=c("%I:%M"),d=c("%I %p"),y=c("%a %d"),v=c("%b %d"),w=c("%B"),M=c("%Y");function tickFormat(n){return(u(n)e(r/(n.length-1))))};scale.quantiles=function(e){return Array.from({length:e+1},((t,r)=>u(n,r/e)))};scale.copy=function(){return sequentialQuantile(e).domain(n)};return initInterpolator.apply(scale,arguments)}function transformer(){var n,e,t,r,a,i,o,l=0,u=.5,s=1,p=1,h=identity$1,m=false;function scale(n){return isNaN(n=+n)?o:(n=.5+((n=+i(n))-e)*(p*n=0&&"xmlns"!==(n=t.slice(0,r))&&(t=t.slice(r+1));return e.hasOwnProperty(n)?{space:e[n],local:t}:t}function creatorInherit(e){return function(){var n=this.ownerDocument,r=this.namespaceURI;return r===t&&n.documentElement.namespaceURI===t?n.createElement(e):n.createElementNS(r,e)}}function creatorFixed(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function creator(t){var e=namespace(t);return(e.local?creatorFixed:creatorInherit)(e)}function none(){}function selector(t){return null==t?none:function(){return this.querySelector(t)}}function selection_select(t){"function"!==typeof t&&(t=selector(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;i=A&&(A=w+1);while(!(g=y[A])&&++A<_);v._next=g||null}}s=new Selection(s,r);s._enter=c;s._exit=l;return s}function arraylike(t){return"object"===typeof t&&"length"in t?t:Array.from(t)}function selection_exit(){return new Selection(this._exit||this._groups.map(sparse),this._parents)}function selection_join(t,e,n){var r=this.enter(),i=this,o=this.exit();if("function"===typeof t){r=t(r);r&&(r=r.selection())}else r=r.append(t+"");if(null!=e){i=e(i);i&&(i=i.selection())}null==n?o.remove():n(o);return r&&i?r.merge(i).order():i}function selection_merge(t){var e=t.selection?t.selection():t;for(var n=this._groups,r=e._groups,i=n.length,o=r.length,s=Math.min(i,o),c=new Array(i),l=0;l=0;)if(r=i[o]){s&&4^r.compareDocumentPosition(s)&&s.parentNode.insertBefore(r,s);s=r}return this}function selection_sort(t){t||(t=ascending);function compareNode(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}for(var e=this._groups,n=e.length,r=new Array(n),i=0;ie?1:t>=e?0:NaN}function selection_call(){var t=arguments[0];arguments[0]=this;t.apply(null,arguments);return this}function selection_nodes(){return Array.from(this)}function selection_node(){for(var t=this._groups,e=0,n=t.length;e1?this.each((null==e?styleRemove:"function"===typeof e?styleFunction:styleConstant)(t,e,null==n?"":n)):styleValue(this.node(),t)}function styleValue(t,e){return t.style.getPropertyValue(e)||defaultView(t).getComputedStyle(t,null).getPropertyValue(e)}function propertyRemove(t){return function(){delete this[t]}}function propertyConstant(t,e){return function(){this[t]=e}}function propertyFunction(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function selection_property(t,e){return arguments.length>1?this.each((null==e?propertyRemove:"function"===typeof e?propertyFunction:propertyConstant)(t,e)):this.node()[t]}function classArray(t){return t.trim().split(/^|\s+/)}function classList(t){return t.classList||new ClassList(t)}function ClassList(t){this._node=t;this._names=classArray(t.getAttribute("class")||"")}ClassList.prototype={add:function(t){var e=this._names.indexOf(t);if(e<0){this._names.push(t);this._node.setAttribute("class",this._names.join(" "))}},remove:function(t){var e=this._names.indexOf(t);if(e>=0){this._names.splice(e,1);this._node.setAttribute("class",this._names.join(" "))}},contains:function(t){return this._names.indexOf(t)>=0}};function classedAdd(t,e){var n=classList(t),r=-1,i=e.length;while(++r=0&&(e=t.slice(n+1),t=t.slice(0,n));return{type:t,name:e}}))}function onRemove(t){return function(){var e=this.__on;if(e){for(var n,r=0,i=-1,o=e.length;rpointer(t,e)))}function selectAll(t){return"string"===typeof t?new Selection([document.querySelectorAll(t)],[document.documentElement]):new Selection([array(t)],i)}export{create,creator,local,matcher,namespace,e as namespaces,pointer,pointers,select,selectAll,selection,selector,selectorAll,styleValue as style,defaultView as window}; diff --git a/vendor/javascript/d3-shape.js b/vendor/javascript/d3-shape.js index a4307180..b4049e5a 100644 --- a/vendor/javascript/d3-shape.js +++ b/vendor/javascript/d3-shape.js @@ -1,2 +1,4 @@ -import{Path as t}from"d3-path";function constant(t){return function constant(){return t}}const n=Math.abs;const i=Math.atan2;const e=Math.cos;const s=Math.max;const o=Math.min;const a=Math.sin;const r=Math.sqrt;const h=1e-12;const l=Math.PI;const c=l/2;const _=2*l;function acos(t){return t>1?0:t<-1?l:Math.acos(t)}function asin(t){return t>=1?c:t<=-1?-c:Math.asin(t)}function withPath(n){let i=3;n.digits=function(t){if(!arguments.length)return i;if(null==t)i=null;else{const n=Math.floor(t);if(!(n>=0))throw new RangeError(`invalid digits: ${t}`);i=n}return n};return()=>new t(i)}function arcInnerRadius(t){return t.innerRadius}function arcOuterRadius(t){return t.outerRadius}function arcStartAngle(t){return t.startAngle}function arcEndAngle(t){return t.endAngle}function arcPadAngle(t){return t&&t.padAngle}function intersect(t,n,i,e,s,o,a,r){var l=i-t,c=e-n,_=a-s,u=r-o,f=u*l-_*c;if(!(f*f$*$+B*B&&(N=P,E=A);return{cx:N,cy:E,x01:-u,y01:-f,x11:N*(o/R-1),y11:E*(o/R-1)}}function arc(){var t=arcInnerRadius,s=arcOuterRadius,u=constant(0),f=null,p=arcStartAngle,d=arcEndAngle,v=arcPadAngle,m=null,T=withPath(arc);function arc(){var b,g,k=+t.apply(this,arguments),w=+s.apply(this,arguments),R=p.apply(this,arguments)-c,C=d.apply(this,arguments)-c,S=n(C-R),N=C>R;m||(m=b=T());wh)if(S>_-h){m.moveTo(w*e(R),w*a(R));m.arc(0,0,w,R,C,!N);if(k>h){m.moveTo(k*e(C),k*a(C));m.arc(0,0,k,C,R,N)}}else{var E,P,A=R,M=C,O=R,$=C,B=S,X=S,Y=v.apply(this,arguments)/2,z=Y>h&&(f?+f.apply(this,arguments):r(k*k+w*w)),L=o(n(w-k)/2,+u.apply(this,arguments)),I=L,q=L;if(z>h){var V=asin(z/k*a(Y)),D=asin(z/w*a(Y));(B-=2*V)>h?(V*=N?1:-1,O+=V,$-=V):(B=0,O=$=(R+C)/2);(X-=2*D)>h?(D*=N?1:-1,A+=D,M-=D):(X=0,A=M=(R+C)/2)}var j=w*e(A),H=w*a(A),W=k*e($),F=k*a($);if(L>h){var G,J=w*e(M),K=w*a(M),Q=k*e(O),U=k*a(O);if(Sh)if(q>h){E=cornerTangents(Q,U,j,H,w,q,N);P=cornerTangents(J,K,W,F,w,q,N);m.moveTo(E.cx+E.x01,E.cy+E.y01);if(qh&&B>h)if(I>h){E=cornerTangents(W,F,J,K,k,-I,N);P=cornerTangents(j,H,Q,U,k,-I,N);m.lineTo(E.cx+E.x01,E.cy+E.y01);if(I=_;--u)r.point(m[u],T[u]);r.lineEnd();r.areaEnd()}if(v){m[c]=+t(f,c,l),T[c]=+n(f,c,l);r.point(e?+e(f,c,l):m[c],i?+i(f,c,l):T[c])}}if(p)return r=null,p+""||null}function arealine(){return line().defined(s).curve(a).context(o)}area.x=function(n){return arguments.length?(t="function"===typeof n?n:constant(+n),e=null,area):t};area.x0=function(n){return arguments.length?(t="function"===typeof n?n:constant(+n),area):t};area.x1=function(t){return arguments.length?(e=null==t?null:"function"===typeof t?t:constant(+t),area):e};area.y=function(t){return arguments.length?(n="function"===typeof t?t:constant(+t),i=null,area):n};area.y0=function(t){return arguments.length?(n="function"===typeof t?t:constant(+t),area):n};area.y1=function(t){return arguments.length?(i=null==t?null:"function"===typeof t?t:constant(+t),area):i};area.lineX0=area.lineY0=function(){return arealine().x(t).y(n)};area.lineY1=function(){return arealine().x(t).y(i)};area.lineX1=function(){return arealine().x(e).y(n)};area.defined=function(t){return arguments.length?(s="function"===typeof t?t:constant(!!t),area):s};area.curve=function(t){return arguments.length?(a=t,null!=o&&(r=a(o)),area):a};area.context=function(t){return arguments.length?(null==t?o=r=null:r=a(o=t),area):o};return area}function descending$1(t,n){return nt?1:n>=t?0:NaN}function identity(t){return t}function pie(){var t=identity,n=descending$1,i=null,e=constant(0),s=constant(_),o=constant(0);function pie(a){var r,h,l,c,u,f=(a=array(a)).length,p=0,d=new Array(f),v=new Array(f),m=+e.apply(this,arguments),T=Math.min(_,Math.max(-_,s.apply(this,arguments)-m)),b=Math.min(Math.abs(T)/f,o.apply(this,arguments)),g=b*(T<0?-1:1);for(r=0;r0&&(p+=u);null!=n?d.sort((function(t,i){return n(v[t],v[i])})):null!=i&&d.sort((function(t,n){return i(a[t],a[n])}));for(r=0,l=p?(T-f*g)/p:0;r0?u*l:0)+g,v[h]={data:a[h],index:r,value:u,startAngle:m,endAngle:c,padAngle:b};return v}pie.value=function(n){return arguments.length?(t="function"===typeof n?n:constant(+n),pie):t};pie.sortValues=function(t){return arguments.length?(n=t,i=null,pie):n};pie.sort=function(t){return arguments.length?(i=t,n=null,pie):i};pie.startAngle=function(t){return arguments.length?(e="function"===typeof t?t:constant(+t),pie):e};pie.endAngle=function(t){return arguments.length?(s="function"===typeof t?t:constant(+t),pie):s};pie.padAngle=function(t){return arguments.length?(o="function"===typeof t?t:constant(+t),pie):o};return pie}var f=curveRadial(curveLinear);function Radial(t){this._curve=t}Radial.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};function curveRadial(t){function radial(n){return new Radial(t(n))}radial._curve=t;return radial}function lineRadial(t){var n=t.curve;t.angle=t.x,delete t.x;t.radius=t.y,delete t.y;t.curve=function(t){return arguments.length?n(curveRadial(t)):n()._curve};return t}function lineRadial$1(){return lineRadial(line().curve(f))}function areaRadial(){var t=area().curve(f),n=t.curve,i=t.lineX0,e=t.lineX1,s=t.lineY0,o=t.lineY1;t.angle=t.x,delete t.x;t.startAngle=t.x0,delete t.x0;t.endAngle=t.x1,delete t.x1;t.radius=t.y,delete t.y;t.innerRadius=t.y0,delete t.y0;t.outerRadius=t.y1,delete t.y1;t.lineStartAngle=function(){return lineRadial(i())},delete t.lineX0;t.lineEndAngle=function(){return lineRadial(e())},delete t.lineX1;t.lineInnerRadius=function(){return lineRadial(s())},delete t.lineY0;t.lineOuterRadius=function(){return lineRadial(o())},delete t.lineY1;t.curve=function(t){return arguments.length?n(curveRadial(t)):n()._curve};return t}function pointRadial(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}class Bump{constructor(t,n){this._context=t;this._x=n}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath();this._line=1-this._line}point(t,n){t=+t,n=+n;switch(this._point){case 0:this._point=1;this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,n,t,n):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+n)/2,t,this._y0,t,n);break}this._x0=t,this._y0=n}}class BumpRadial{constructor(t){this._context=t}lineStart(){this._point=0}lineEnd(){}point(t,n){t=+t,n=+n;if(0===this._point)this._point=1;else{const i=pointRadial(this._x0,this._y0);const e=pointRadial(this._x0,this._y0=(this._y0+n)/2);const s=pointRadial(t,this._y0);const o=pointRadial(t,n);this._context.moveTo(...i);this._context.bezierCurveTo(...e,...s,...o)}this._x0=t,this._y0=n}}function bumpX(t){return new Bump(t,true)}function bumpY(t){return new Bump(t,false)}function bumpRadial(t){return new BumpRadial(t)}function linkSource(t){return t.source}function linkTarget(t){return t.target}function link(t){let n=linkSource,i=linkTarget,e=x,s=y,o=null,a=null,r=withPath(link);function link(){let h;const l=u.call(arguments);const c=n.apply(this,l);const _=i.apply(this,l);null==o&&(a=t(h=r()));a.lineStart();l[0]=c,a.point(+e.apply(this,l),+s.apply(this,l));l[0]=_,a.point(+e.apply(this,l),+s.apply(this,l));a.lineEnd();if(h)return a=null,h+""||null}link.source=function(t){return arguments.length?(n=t,link):n};link.target=function(t){return arguments.length?(i=t,link):i};link.x=function(t){return arguments.length?(e="function"===typeof t?t:constant(+t),link):e};link.y=function(t){return arguments.length?(s="function"===typeof t?t:constant(+t),link):s};link.context=function(n){return arguments.length?(null==n?o=a=null:a=t(o=n),link):o};return link}function linkHorizontal(){return link(bumpX)}function linkVertical(){return link(bumpY)}function linkRadial(){const t=link(bumpRadial);t.angle=t.x,delete t.x;t.radius=t.y,delete t.y;return t}const p=r(3);var d={draw(t,n){const i=.59436*r(n+o(n/28,.75));const e=i/2;const s=e*p;t.moveTo(0,i);t.lineTo(0,-i);t.moveTo(-s,-e);t.lineTo(s,e);t.moveTo(-s,e);t.lineTo(s,-e)}};var v={draw(t,n){const i=r(n/l);t.moveTo(i,0);t.arc(0,0,i,0,_)}};var m={draw(t,n){const i=r(n/5)/2;t.moveTo(-3*i,-i);t.lineTo(-i,-i);t.lineTo(-i,-3*i);t.lineTo(i,-3*i);t.lineTo(i,-i);t.lineTo(3*i,-i);t.lineTo(3*i,i);t.lineTo(i,i);t.lineTo(i,3*i);t.lineTo(-i,3*i);t.lineTo(-i,i);t.lineTo(-3*i,i);t.closePath()}};const T=r(1/3);const b=2*T;var g={draw(t,n){const i=r(n/b);const e=i*T;t.moveTo(0,-i);t.lineTo(e,0);t.lineTo(0,i);t.lineTo(-e,0);t.closePath()}};var k={draw(t,n){const i=.62625*r(n);t.moveTo(0,-i);t.lineTo(i,0);t.lineTo(0,i);t.lineTo(-i,0);t.closePath()}};var w={draw(t,n){const i=.87559*r(n-o(n/7,2));t.moveTo(-i,0);t.lineTo(i,0);t.moveTo(0,i);t.lineTo(0,-i)}};var R={draw(t,n){const i=r(n);const e=-i/2;t.rect(e,e,i,i)}};var C={draw(t,n){const i=.4431*r(n);t.moveTo(i,i);t.lineTo(i,-i);t.lineTo(-i,-i);t.lineTo(-i,i);t.closePath()}};const S=.8908130915292852;const N=a(l/10)/a(7*l/10);const E=a(_/10)*N;const P=-e(_/10)*N;var A={draw(t,n){const i=r(n*S);const s=E*i;const o=P*i;t.moveTo(0,-i);t.lineTo(s,o);for(let n=1;n<5;++n){const r=_*n/5;const h=e(r);const l=a(r);t.lineTo(l*i,-h*i);t.lineTo(h*s-l*o,l*s+h*o)}t.closePath()}};const M=r(3);var O={draw(t,n){const i=-r(n/(3*M));t.moveTo(0,2*i);t.lineTo(-M*i,-i);t.lineTo(M*i,-i);t.closePath()}};const $=r(3);var B={draw(t,n){const i=.6824*r(n);const e=i/2;const s=i*$/2;t.moveTo(0,-i);t.lineTo(s,e);t.lineTo(-s,e);t.closePath()}};const X=-.5;const Y=r(3)/2;const z=1/r(12);const L=3*(z/2+1);var I={draw(t,n){const i=r(n/L);const e=i/2,s=i*z;const o=e,a=i*z+i;const h=-o,l=a;t.moveTo(e,s);t.lineTo(o,a);t.lineTo(h,l);t.lineTo(X*e-Y*s,Y*e+X*s);t.lineTo(X*o-Y*a,Y*o+X*a);t.lineTo(X*h-Y*l,Y*h+X*l);t.lineTo(X*e+Y*s,X*s-Y*e);t.lineTo(X*o+Y*a,X*a-Y*o);t.lineTo(X*h+Y*l,X*l-Y*h);t.closePath()}};var q={draw(t,n){const i=.6189*r(n-o(n/6,1.7));t.moveTo(-i,-i);t.lineTo(i,i);t.moveTo(-i,i);t.lineTo(i,-i)}};const V=[v,m,g,R,A,O,I];const D=[v,w,q,B,d,C,k];function Symbol$1(t,n){let i=null,e=withPath(symbol);t="function"===typeof t?t:constant(t||v);n="function"===typeof n?n:constant(void 0===n?64:+n);function symbol(){let s;i||(i=s=e());t.apply(this,arguments).draw(i,+n.apply(this,arguments));if(s)return i=null,s+""||null}symbol.type=function(n){return arguments.length?(t="function"===typeof n?n:constant(n),symbol):t};symbol.size=function(t){return arguments.length?(n="function"===typeof t?t:constant(+t),symbol):n};symbol.context=function(t){return arguments.length?(i=null==t?null:t,symbol):i};return symbol}function noop(){}function point$3(t,n,i){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+i)/6)}function Basis(t){this._context=t}Basis.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN;this._point=0},lineEnd:function(){switch(this._point){case 3:point$3(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1);break}(this._line||0!==this._line&&1===this._point)&&this._context.closePath();this._line=1-this._line},point:function(t,n){t=+t,n=+n;switch(this._point){case 0:this._point=1;this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:point$3(this,t,n);break}this._x0=this._x1,this._x1=t;this._y0=this._y1,this._y1=n}};function basis(t){return new Basis(t)}function BasisClosed(t){this._context=t}BasisClosed.prototype={areaStart:noop,areaEnd:noop,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN;this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2);this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3);this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3);this._context.closePath();break;case 3:this.point(this._x2,this._y2);this.point(this._x3,this._y3);this.point(this._x4,this._y4);break}},point:function(t,n){t=+t,n=+n;switch(this._point){case 0:this._point=1;this._x2=t,this._y2=n;break;case 1:this._point=2;this._x3=t,this._y3=n;break;case 2:this._point=3;this._x4=t,this._y4=n;this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:point$3(this,t,n);break}this._x0=this._x1,this._x1=t;this._y0=this._y1,this._y1=n}};function basisClosed(t){return new BasisClosed(t)}function BasisOpen(t){this._context=t}BasisOpen.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN;this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath();this._line=1-this._line},point:function(t,n){t=+t,n=+n;switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var i=(this._x0+4*this._x1+t)/6,e=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(i,e):this._context.moveTo(i,e);break;case 3:this._point=4;default:point$3(this,t,n);break}this._x0=this._x1,this._x1=t;this._y0=this._y1,this._y1=n}};function basisOpen(t){return new BasisOpen(t)}function Bundle(t,n){this._basis=new Basis(t);this._beta=n}Bundle.prototype={lineStart:function(){this._x=[];this._y=[];this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,i=t.length-1;if(i>0){var e,s=t[0],o=n[0],a=t[i]-s,r=n[i]-o,h=-1;while(++h<=i){e=h/i;this._basis.point(this._beta*t[h]+(1-this._beta)*(s+e*a),this._beta*n[h]+(1-this._beta)*(o+e*r))}}this._x=this._y=null;this._basis.lineEnd()},point:function(t,n){this._x.push(+t);this._y.push(+n)}};var j=function custom(t){function bundle(n){return 1===t?new Basis(n):new Bundle(n,t)}bundle.beta=function(t){return custom(+t)};return bundle}(.85);function point$2(t,n,i){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-i),t._x2,t._y2)}function Cardinal(t,n){this._context=t;this._k=(1-n)/6}Cardinal.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN;this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:point$2(this,this._x1,this._y1);break}(this._line||0!==this._line&&1===this._point)&&this._context.closePath();this._line=1-this._line},point:function(t,n){t=+t,n=+n;switch(this._point){case 0:this._point=1;this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;this._x1=t,this._y1=n;break;case 2:this._point=3;default:point$2(this,t,n);break}this._x0=this._x1,this._x1=this._x2,this._x2=t;this._y0=this._y1,this._y1=this._y2,this._y2=n}};var H=function custom(t){function cardinal(n){return new Cardinal(n,t)}cardinal.tension=function(t){return custom(+t)};return cardinal}(0);function CardinalClosed(t,n){this._context=t;this._k=(1-n)/6}CardinalClosed.prototype={areaStart:noop,areaEnd:noop,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN;this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3);this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3);this._context.closePath();break;case 3:this.point(this._x3,this._y3);this.point(this._x4,this._y4);this.point(this._x5,this._y5);break}},point:function(t,n){t=+t,n=+n;switch(this._point){case 0:this._point=1;this._x3=t,this._y3=n;break;case 1:this._point=2;this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3;this._x5=t,this._y5=n;break;default:point$2(this,t,n);break}this._x0=this._x1,this._x1=this._x2,this._x2=t;this._y0=this._y1,this._y1=this._y2,this._y2=n}};var W=function custom(t){function cardinal(n){return new CardinalClosed(n,t)}cardinal.tension=function(t){return custom(+t)};return cardinal}(0);function CardinalOpen(t,n){this._context=t;this._k=(1-n)/6}CardinalOpen.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN;this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath();this._line=1-this._line},point:function(t,n){t=+t,n=+n;switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:point$2(this,t,n);break}this._x0=this._x1,this._x1=this._x2,this._x2=t;this._y0=this._y1,this._y1=this._y2,this._y2=n}};var F=function custom(t){function cardinal(n){return new CardinalOpen(n,t)}cardinal.tension=function(t){return custom(+t)};return cardinal}(0);function point$1(t,n,i){var e=t._x1,s=t._y1,o=t._x2,a=t._y2;if(t._l01_a>h){var r=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,l=3*t._l01_a*(t._l01_a+t._l12_a);e=(e*r-t._x0*t._l12_2a+t._x2*t._l01_2a)/l;s=(s*r-t._y0*t._l12_2a+t._y2*t._l01_2a)/l}if(t._l23_a>h){var c=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,_=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*c+t._x1*t._l23_2a-n*t._l12_2a)/_;a=(a*c+t._y1*t._l23_2a-i*t._l12_2a)/_}t._context.bezierCurveTo(e,s,o,a,t._x2,t._y2)}function CatmullRom(t,n){this._context=t;this._alpha=n}CatmullRom.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN;this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2);break}(this._line||0!==this._line&&1===this._point)&&this._context.closePath();this._line=1-this._line},point:function(t,n){t=+t,n=+n;if(this._point){var i=this._x2-t,e=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(i*i+e*e,this._alpha))}switch(this._point){case 0:this._point=1;this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:point$1(this,t,n);break}this._l01_a=this._l12_a,this._l12_a=this._l23_a;this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a;this._x0=this._x1,this._x1=this._x2,this._x2=t;this._y0=this._y1,this._y1=this._y2,this._y2=n}};var G=function custom(t){function catmullRom(n){return t?new CatmullRom(n,t):new Cardinal(n,0)}catmullRom.alpha=function(t){return custom(+t)};return catmullRom}(.5);function CatmullRomClosed(t,n){this._context=t;this._alpha=n}CatmullRomClosed.prototype={areaStart:noop,areaEnd:noop,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN;this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3);this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3);this._context.closePath();break;case 3:this.point(this._x3,this._y3);this.point(this._x4,this._y4);this.point(this._x5,this._y5);break}},point:function(t,n){t=+t,n=+n;if(this._point){var i=this._x2-t,e=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(i*i+e*e,this._alpha))}switch(this._point){case 0:this._point=1;this._x3=t,this._y3=n;break;case 1:this._point=2;this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3;this._x5=t,this._y5=n;break;default:point$1(this,t,n);break}this._l01_a=this._l12_a,this._l12_a=this._l23_a;this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a;this._x0=this._x1,this._x1=this._x2,this._x2=t;this._y0=this._y1,this._y1=this._y2,this._y2=n}};var J=function custom(t){function catmullRom(n){return t?new CatmullRomClosed(n,t):new CardinalClosed(n,0)}catmullRom.alpha=function(t){return custom(+t)};return catmullRom}(.5);function CatmullRomOpen(t,n){this._context=t;this._alpha=n}CatmullRomOpen.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN;this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath();this._line=1-this._line},point:function(t,n){t=+t,n=+n;if(this._point){var i=this._x2-t,e=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(i*i+e*e,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:point$1(this,t,n);break}this._l01_a=this._l12_a,this._l12_a=this._l23_a;this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a;this._x0=this._x1,this._x1=this._x2,this._x2=t;this._y0=this._y1,this._y1=this._y2,this._y2=n}};var K=function custom(t){function catmullRom(n){return t?new CatmullRomOpen(n,t):new CardinalOpen(n,0)}catmullRom.alpha=function(t){return custom(+t)};return catmullRom}(.5);function LinearClosed(t){this._context=t}LinearClosed.prototype={areaStart:noop,areaEnd:noop,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,n){t=+t,n=+n;this._point?this._context.lineTo(t,n):(this._point=1,this._context.moveTo(t,n))}};function linearClosed(t){return new LinearClosed(t)}function sign(t){return t<0?-1:1}function slope3(t,n,i){var e=t._x1-t._x0,s=n-t._x1,o=(t._y1-t._y0)/(e||s<0&&-0),a=(i-t._y1)/(s||e<0&&-0),r=(o*s+a*e)/(e+s);return(sign(o)+sign(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(r))||0}function slope2(t,n){var i=t._x1-t._x0;return i?(3*(t._y1-t._y0)/i-n)/2:n}function point(t,n,i){var e=t._x0,s=t._y0,o=t._x1,a=t._y1,r=(o-e)/3;t._context.bezierCurveTo(e+r,s+r*n,o-r,a-r*i,o,a)}function MonotoneX(t){this._context=t}MonotoneX.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN;this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:point(this,this._t0,slope2(this,this._t0));break}(this._line||0!==this._line&&1===this._point)&&this._context.closePath();this._line=1-this._line},point:function(t,n){var i=NaN;t=+t,n=+n;if(t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1;this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;point(this,slope2(this,i=slope3(this,t,n)),i);break;default:point(this,this._t0,i=slope3(this,t,n));break}this._x0=this._x1,this._x1=t;this._y0=this._y1,this._y1=n;this._t0=i}}};function MonotoneY(t){this._context=new ReflectContext(t)}(MonotoneY.prototype=Object.create(MonotoneX.prototype)).point=function(t,n){MonotoneX.prototype.point.call(this,n,t)};function ReflectContext(t){this._context=t}ReflectContext.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,i,e,s,o){this._context.bezierCurveTo(n,t,e,i,o,s)}};function monotoneX(t){return new MonotoneX(t)}function monotoneY(t){return new MonotoneY(t)}function Natural(t){this._context=t}Natural.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[];this._y=[]},lineEnd:function(){var t=this._x,n=this._y,i=t.length;if(i){this._line?this._context.lineTo(t[0],n[0]):this._context.moveTo(t[0],n[0]);if(2===i)this._context.lineTo(t[1],n[1]);else{var e=controlPoints(t),s=controlPoints(n);for(var o=0,a=1;a=0;--n)s[n]=(a[n]-s[n+1])/o[n];o[e-1]=(t[e]+s[e-1])/2;for(n=0;n=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){t=+t,n=+n;switch(this._point){case 0:this._point=1;this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0){this._context.lineTo(this._x,n);this._context.lineTo(t,n)}else{var i=this._x*(1-this._t)+t*this._t;this._context.lineTo(i,this._y);this._context.lineTo(i,n)}break}this._x=t,this._y=n}};function step(t){return new Step(t,.5)}function stepBefore(t){return new Step(t,0)}function stepAfter(t){return new Step(t,1)}function none$1(t,n){if((s=t.length)>1)for(var i,e,s,o=1,a=t[n[0]],r=a.length;o=0)i[n]=n;return i}function stackValue(t,n){return t[n]}function stackSeries(t){const n=[];n.key=t;return n}function stack(){var t=constant([]),n=none,i=none$1,e=stackValue;function stack(s){var o,a,r=Array.from(t.apply(this,arguments),stackSeries),h=r.length,l=-1;for(const t of s)for(o=0,++l;o0){for(var i,e,s,o=0,a=t[0].length;o0)for(var i,e,s,o,a,r,h=0,l=t[n[0]].length;h0?(e[0]=o,e[1]=o+=s):s<0?(e[1]=a,e[0]=a+=s):(e[0]=0,e[1]=s)}function silhouette(t,n){if((i=t.length)>0){for(var i,e=0,s=t[n[0]],o=s.length;e0&&(e=(i=t[n[0]]).length)>0){for(var i,e,s,o=0,a=1;ao&&(o=n,e=i);return e}function ascending(t){var n=t.map(sum);return none(t).sort((function(t,i){return n[t]-n[i]}))}function sum(t){var n,i=0,e=-1,s=t.length;while(++e1?0:t<-1?_:Math.acos(t)}function asin(t){return t>=1?u:t<=-1?-u:Math.asin(t)}function arcInnerRadius(t){return t.innerRadius}function arcOuterRadius(t){return t.outerRadius}function arcStartAngle(t){return t.startAngle}function arcEndAngle(t){return t.endAngle}function arcPadAngle(t){return t&&t.padAngle}function intersect(t,n,i,e,a,s,o,r){var l=i-t,h=e-n,_=o-a,u=r-s,f=u*l-_*h;if(!(f*f$*$+B*B&&(S=O,N=A);return{cx:S,cy:N,x01:-u,y01:-f,x11:S*(a/C-1),y11:N*(a/C-1)}}function arc(){var t=arcInnerRadius,o=arcOuterRadius,p=constant(0),d=null,v=arcStartAngle,m=arcEndAngle,k=arcPadAngle,g=null;function arc(){var b,T,R=+t.apply(this||n,arguments),C=+o.apply(this||n,arguments),w=v.apply(this||n,arguments)-u,M=m.apply(this||n,arguments)-u,S=e(M-w),N=M>w;g||(g=b=i.path());Cc)if(S>f-c){g.moveTo(C*s(w),C*l(w));g.arc(0,0,C,w,M,!N);if(R>c){g.moveTo(R*s(M),R*l(M));g.arc(0,0,R,M,w,N)}}else{var O=w,A=M,E=w,P=M,$=S,B=S,q=k.apply(this||n,arguments)/2,z=q>c&&(d?+d.apply(this||n,arguments):h(R*R+C*C)),L=r(e(C-R)/2,+p.apply(this||n,arguments)),X=L,Y=L,V,I;if(z>c){var D=asin(z/R*l(q)),H=asin(z/C*l(q));($-=2*D)>c?(D*=N?1:-1,E+=D,P-=D):($=0,E=P=(w+M)/2);(B-=2*H)>c?(H*=N?1:-1,O+=H,A-=H):(B=0,O=A=(w+M)/2)}var W=C*s(O),j=C*l(O),F=R*s(P),G=R*l(P);if(L>c){var J=C*s(A),K=C*l(A),Q=R*s(E),U=R*l(E),Z;if(S<_&&(Z=intersect(W,j,Q,U,J,K,F,G))){var tt=W-Z[0],nt=j-Z[1],it=J-Z[0],et=K-Z[1],at=1/l(acos((tt*it+nt*et)/(h(tt*tt+nt*nt)*h(it*it+et*et)))/2),st=h(Z[0]*Z[0]+Z[1]*Z[1]);X=r(L,(R-st)/(at-1));Y=r(L,(C-st)/(at+1))}}if(B>c)if(Y>c){V=cornerTangents(Q,U,W,j,C,Y,N);I=cornerTangents(J,K,F,G,C,Y,N);g.moveTo(V.cx+V.x01,V.cy+V.y01);if(Yc&&$>c)if(X>c){V=cornerTangents(F,G,J,K,R,-X,N);I=cornerTangents(W,j,Q,U,R,-X,N);g.lineTo(V.cx+V.x01,V.cy+V.y01);if(X=_;--u)l.point(m[u],k[u]);l.lineEnd();l.areaEnd()}if(d){m[c]=+t(p,c,h),k[c]=+e(p,c,h);l.point(n?+n(p,c,h):m[c],a?+a(p,c,h):k[c])}}if(v)return l=null,v+""||null}function arealine(){return line().defined(s).curve(r).context(o)}area.x=function(i){return arguments.length?(t="function"===typeof i?i:constant(+i),n=null,area):t};area.x0=function(n){return arguments.length?(t="function"===typeof n?n:constant(+n),area):t};area.x1=function(t){return arguments.length?(n=null==t?null:"function"===typeof t?t:constant(+t),area):n};area.y=function(t){return arguments.length?(e="function"===typeof t?t:constant(+t),a=null,area):e};area.y0=function(t){return arguments.length?(e="function"===typeof t?t:constant(+t),area):e};area.y1=function(t){return arguments.length?(a=null==t?null:"function"===typeof t?t:constant(+t),area):a};area.lineX0=area.lineY0=function(){return arealine().x(t).y(e)};area.lineY1=function(){return arealine().x(t).y(a)};area.lineX1=function(){return arealine().x(n).y(e)};area.defined=function(t){return arguments.length?(s="function"===typeof t?t:constant(!!t),area):s};area.curve=function(t){return arguments.length?(r=t,null!=o&&(l=r(o)),area):r};area.context=function(t){return arguments.length?(null==t?o=l=null:l=r(o=t),area):o};return area}function descending(t,n){return nt?1:n>=t?0:NaN}function identity(t){return t}function pie(){var t=identity,i=descending,e=null,a=constant(0),s=constant(f),o=constant(0);function pie(r){var l,h=r.length,c,_,u=0,p=new Array(h),d=new Array(h),v=+a.apply(this||n,arguments),m=Math.min(f,Math.max(-f,s.apply(this||n,arguments)-v)),k,g=Math.min(Math.abs(m)/h,o.apply(this||n,arguments)),b=g*(m<0?-1:1),T;for(l=0;l0&&(u+=T);null!=i?p.sort((function(t,n){return i(d[t],d[n])})):null!=e&&p.sort((function(t,n){return e(r[t],r[n])}));for(l=0,_=u?(m-h*b)/u:0;l0?T*_:0)+b,d[c]={data:r[c],index:l,value:T,startAngle:v,endAngle:k,padAngle:g};return d}pie.value=function(n){return arguments.length?(t="function"===typeof n?n:constant(+n),pie):t};pie.sortValues=function(t){return arguments.length?(i=t,e=null,pie):i};pie.sort=function(t){return arguments.length?(e=t,i=null,pie):e};pie.startAngle=function(t){return arguments.length?(a="function"===typeof t?t:constant(+t),pie):a};pie.endAngle=function(t){return arguments.length?(s="function"===typeof t?t:constant(+t),pie):s};pie.padAngle=function(t){return arguments.length?(o="function"===typeof t?t:constant(+t),pie):o};return pie}var p=curveRadial(curveLinear);function Radial(t){(this||n)._curve=t}Radial.prototype={areaStart:function(){(this||n)._curve.areaStart()},areaEnd:function(){(this||n)._curve.areaEnd()},lineStart:function(){(this||n)._curve.lineStart()},lineEnd:function(){(this||n)._curve.lineEnd()},point:function(t,i){(this||n)._curve.point(i*Math.sin(t),i*-Math.cos(t))}};function curveRadial(t){function radial(n){return new Radial(t(n))}radial._curve=t;return radial}function lineRadial(t){var n=t.curve;t.angle=t.x,delete t.x;t.radius=t.y,delete t.y;t.curve=function(t){return arguments.length?n(curveRadial(t)):n()._curve};return t}function lineRadial$1(){return lineRadial(line().curve(p))}function areaRadial(){var t=area().curve(p),n=t.curve,i=t.lineX0,e=t.lineX1,a=t.lineY0,s=t.lineY1;t.angle=t.x,delete t.x;t.startAngle=t.x0,delete t.x0;t.endAngle=t.x1,delete t.x1;t.radius=t.y,delete t.y;t.innerRadius=t.y0,delete t.y0;t.outerRadius=t.y1,delete t.y1;t.lineStartAngle=function(){return lineRadial(i())},delete t.lineX0;t.lineEndAngle=function(){return lineRadial(e())},delete t.lineX1;t.lineInnerRadius=function(){return lineRadial(a())},delete t.lineY0;t.lineOuterRadius=function(){return lineRadial(s())},delete t.lineY1;t.curve=function(t){return arguments.length?n(curveRadial(t)):n()._curve};return t}function pointRadial(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}var d=Array.prototype.slice;function linkSource(t){return t.source}function linkTarget(t){return t.target}function link(t){var e=linkSource,a=linkTarget,s=x,o=y,r=null;function link(){var l,h=d.call(arguments),c=e.apply(this||n,h),_=a.apply(this||n,h);r||(r=l=i.path());t(r,+s.apply(this||n,(h[0]=c,h)),+o.apply(this||n,h),+s.apply(this||n,(h[0]=_,h)),+o.apply(this||n,h));if(l)return r=null,l+""||null}link.source=function(t){return arguments.length?(e=t,link):e};link.target=function(t){return arguments.length?(a=t,link):a};link.x=function(t){return arguments.length?(s="function"===typeof t?t:constant(+t),link):s};link.y=function(t){return arguments.length?(o="function"===typeof t?t:constant(+t),link):o};link.context=function(t){return arguments.length?(r=null==t?null:t,link):r};return link}function curveHorizontal(t,n,i,e,a){t.moveTo(n,i);t.bezierCurveTo(n=(n+e)/2,i,n,a,e,a)}function curveVertical(t,n,i,e,a){t.moveTo(n,i);t.bezierCurveTo(n,i=(i+a)/2,e,i,e,a)}function curveRadial$1(t,n,i,e,a){var s=pointRadial(n,i),o=pointRadial(n,i=(i+a)/2),r=pointRadial(e,i),l=pointRadial(e,a);t.moveTo(s[0],s[1]);t.bezierCurveTo(o[0],o[1],r[0],r[1],l[0],l[1])}function linkHorizontal(){return link(curveHorizontal)}function linkVertical(){return link(curveVertical)}function linkRadial(){var t=link(curveRadial$1);t.angle=t.x,delete t.x;t.radius=t.y,delete t.y;return t}var v={draw:function(t,n){var i=Math.sqrt(n/_);t.moveTo(i,0);t.arc(0,0,i,0,f)}};var m={draw:function(t,n){var i=Math.sqrt(n/5)/2;t.moveTo(-3*i,-i);t.lineTo(-i,-i);t.lineTo(-i,-3*i);t.lineTo(i,-3*i);t.lineTo(i,-i);t.lineTo(3*i,-i);t.lineTo(3*i,i);t.lineTo(i,i);t.lineTo(i,3*i);t.lineTo(-i,3*i);t.lineTo(-i,i);t.lineTo(-3*i,i);t.closePath()}};var k=Math.sqrt(1/3),g=2*k;var b={draw:function(t,n){var i=Math.sqrt(n/g),e=i*k;t.moveTo(0,-i);t.lineTo(e,0);t.lineTo(0,i);t.lineTo(-e,0);t.closePath()}};var T=.8908130915292852,R=Math.sin(_/10)/Math.sin(7*_/10),C=Math.sin(f/10)*R,w=-Math.cos(f/10)*R;var M={draw:function(t,n){var i=Math.sqrt(n*T),e=C*i,a=w*i;t.moveTo(0,-i);t.lineTo(e,a);for(var s=1;s<5;++s){var o=f*s/5,r=Math.cos(o),l=Math.sin(o);t.lineTo(l*i,-r*i);t.lineTo(r*e-l*a,l*e+r*a)}t.closePath()}};var S={draw:function(t,n){var i=Math.sqrt(n),e=-i/2;t.rect(e,e,i,i)}};var N=Math.sqrt(3);var O={draw:function(t,n){var i=-Math.sqrt(n/(3*N));t.moveTo(0,2*i);t.lineTo(-N*i,-i);t.lineTo(N*i,-i);t.closePath()}};var A=-.5,E=Math.sqrt(3)/2,P=1/Math.sqrt(12),$=3*(P/2+1);var B={draw:function(t,n){var i=Math.sqrt(n/$),e=i/2,a=i*P,s=e,o=i*P+i,r=-s,l=o;t.moveTo(e,a);t.lineTo(s,o);t.lineTo(r,l);t.lineTo(A*e-E*a,E*e+A*a);t.lineTo(A*s-E*o,E*s+A*o);t.lineTo(A*r-E*l,E*r+A*l);t.lineTo(A*e+E*a,A*a-E*e);t.lineTo(A*s+E*o,A*o-E*s);t.lineTo(A*r+E*l,A*l-E*r);t.closePath()}};var q=[v,m,b,S,M,O,B];function symbol(){var t=constant(v),e=constant(64),a=null;function symbol(){var s;a||(a=s=i.path());t.apply(this||n,arguments).draw(a,+e.apply(this||n,arguments));if(s)return a=null,s+""||null}symbol.type=function(n){return arguments.length?(t="function"===typeof n?n:constant(n),symbol):t};symbol.size=function(t){return arguments.length?(e="function"===typeof t?t:constant(+t),symbol):e};symbol.context=function(t){return arguments.length?(a=null==t?null:t,symbol):a};return symbol}function noop(){}function point(t,n,i){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+i)/6)}function Basis(t){(this||n)._context=t}Basis.prototype={areaStart:function(){(this||n)._line=0},areaEnd:function(){(this||n)._line=NaN},lineStart:function(){(this||n)._x0=(this||n)._x1=(this||n)._y0=(this||n)._y1=NaN;(this||n)._point=0},lineEnd:function(){switch((this||n)._point){case 3:point(this||n,(this||n)._x1,(this||n)._y1);case 2:(this||n)._context.lineTo((this||n)._x1,(this||n)._y1);break}((this||n)._line||0!==(this||n)._line&&1===(this||n)._point)&&(this||n)._context.closePath();(this||n)._line=1-(this||n)._line},point:function(t,i){t=+t,i=+i;switch((this||n)._point){case 0:(this||n)._point=1;(this||n)._line?(this||n)._context.lineTo(t,i):(this||n)._context.moveTo(t,i);break;case 1:(this||n)._point=2;break;case 2:(this||n)._point=3;(this||n)._context.lineTo((5*(this||n)._x0+(this||n)._x1)/6,(5*(this||n)._y0+(this||n)._y1)/6);default:point(this||n,t,i);break}(this||n)._x0=(this||n)._x1,(this||n)._x1=t;(this||n)._y0=(this||n)._y1,(this||n)._y1=i}};function basis(t){return new Basis(t)}function BasisClosed(t){(this||n)._context=t}BasisClosed.prototype={areaStart:noop,areaEnd:noop,lineStart:function(){(this||n)._x0=(this||n)._x1=(this||n)._x2=(this||n)._x3=(this||n)._x4=(this||n)._y0=(this||n)._y1=(this||n)._y2=(this||n)._y3=(this||n)._y4=NaN;(this||n)._point=0},lineEnd:function(){switch((this||n)._point){case 1:(this||n)._context.moveTo((this||n)._x2,(this||n)._y2);(this||n)._context.closePath();break;case 2:(this||n)._context.moveTo(((this||n)._x2+2*(this||n)._x3)/3,((this||n)._y2+2*(this||n)._y3)/3);(this||n)._context.lineTo(((this||n)._x3+2*(this||n)._x2)/3,((this||n)._y3+2*(this||n)._y2)/3);(this||n)._context.closePath();break;case 3:this.point((this||n)._x2,(this||n)._y2);this.point((this||n)._x3,(this||n)._y3);this.point((this||n)._x4,(this||n)._y4);break}},point:function(t,i){t=+t,i=+i;switch((this||n)._point){case 0:(this||n)._point=1;(this||n)._x2=t,(this||n)._y2=i;break;case 1:(this||n)._point=2;(this||n)._x3=t,(this||n)._y3=i;break;case 2:(this||n)._point=3;(this||n)._x4=t,(this||n)._y4=i;(this||n)._context.moveTo(((this||n)._x0+4*(this||n)._x1+t)/6,((this||n)._y0+4*(this||n)._y1+i)/6);break;default:point(this||n,t,i);break}(this||n)._x0=(this||n)._x1,(this||n)._x1=t;(this||n)._y0=(this||n)._y1,(this||n)._y1=i}};function basisClosed(t){return new BasisClosed(t)}function BasisOpen(t){(this||n)._context=t}BasisOpen.prototype={areaStart:function(){(this||n)._line=0},areaEnd:function(){(this||n)._line=NaN},lineStart:function(){(this||n)._x0=(this||n)._x1=(this||n)._y0=(this||n)._y1=NaN;(this||n)._point=0},lineEnd:function(){((this||n)._line||0!==(this||n)._line&&3===(this||n)._point)&&(this||n)._context.closePath();(this||n)._line=1-(this||n)._line},point:function(t,i){t=+t,i=+i;switch((this||n)._point){case 0:(this||n)._point=1;break;case 1:(this||n)._point=2;break;case 2:(this||n)._point=3;var e=((this||n)._x0+4*(this||n)._x1+t)/6,a=((this||n)._y0+4*(this||n)._y1+i)/6;(this||n)._line?(this||n)._context.lineTo(e,a):(this||n)._context.moveTo(e,a);break;case 3:(this||n)._point=4;default:point(this||n,t,i);break}(this||n)._x0=(this||n)._x1,(this||n)._x1=t;(this||n)._y0=(this||n)._y1,(this||n)._y1=i}};function basisOpen(t){return new BasisOpen(t)}function Bundle(t,i){(this||n)._basis=new Basis(t);(this||n)._beta=i}Bundle.prototype={lineStart:function(){(this||n)._x=[];(this||n)._y=[];(this||n)._basis.lineStart()},lineEnd:function(){var t=(this||n)._x,i=(this||n)._y,e=t.length-1;if(e>0){var a=t[0],s=i[0],o=t[e]-a,r=i[e]-s,l=-1,h;while(++l<=e){h=l/e;(this||n)._basis.point((this||n)._beta*t[l]+(1-(this||n)._beta)*(a+h*o),(this||n)._beta*i[l]+(1-(this||n)._beta)*(s+h*r))}}(this||n)._x=(this||n)._y=null;(this||n)._basis.lineEnd()},point:function(t,i){(this||n)._x.push(+t);(this||n)._y.push(+i)}};var z=function custom(t){function bundle(n){return 1===t?new Basis(n):new Bundle(n,t)}bundle.beta=function(t){return custom(+t)};return bundle}(.85);function point$1(t,n,i){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-i),t._x2,t._y2)}function Cardinal(t,i){(this||n)._context=t;(this||n)._k=(1-i)/6}Cardinal.prototype={areaStart:function(){(this||n)._line=0},areaEnd:function(){(this||n)._line=NaN},lineStart:function(){(this||n)._x0=(this||n)._x1=(this||n)._x2=(this||n)._y0=(this||n)._y1=(this||n)._y2=NaN;(this||n)._point=0},lineEnd:function(){switch((this||n)._point){case 2:(this||n)._context.lineTo((this||n)._x2,(this||n)._y2);break;case 3:point$1(this||n,(this||n)._x1,(this||n)._y1);break}((this||n)._line||0!==(this||n)._line&&1===(this||n)._point)&&(this||n)._context.closePath();(this||n)._line=1-(this||n)._line},point:function(t,i){t=+t,i=+i;switch((this||n)._point){case 0:(this||n)._point=1;(this||n)._line?(this||n)._context.lineTo(t,i):(this||n)._context.moveTo(t,i);break;case 1:(this||n)._point=2;(this||n)._x1=t,(this||n)._y1=i;break;case 2:(this||n)._point=3;default:point$1(this||n,t,i);break}(this||n)._x0=(this||n)._x1,(this||n)._x1=(this||n)._x2,(this||n)._x2=t;(this||n)._y0=(this||n)._y1,(this||n)._y1=(this||n)._y2,(this||n)._y2=i}};var L=function custom(t){function cardinal(n){return new Cardinal(n,t)}cardinal.tension=function(t){return custom(+t)};return cardinal}(0);function CardinalClosed(t,i){(this||n)._context=t;(this||n)._k=(1-i)/6}CardinalClosed.prototype={areaStart:noop,areaEnd:noop,lineStart:function(){(this||n)._x0=(this||n)._x1=(this||n)._x2=(this||n)._x3=(this||n)._x4=(this||n)._x5=(this||n)._y0=(this||n)._y1=(this||n)._y2=(this||n)._y3=(this||n)._y4=(this||n)._y5=NaN;(this||n)._point=0},lineEnd:function(){switch((this||n)._point){case 1:(this||n)._context.moveTo((this||n)._x3,(this||n)._y3);(this||n)._context.closePath();break;case 2:(this||n)._context.lineTo((this||n)._x3,(this||n)._y3);(this||n)._context.closePath();break;case 3:this.point((this||n)._x3,(this||n)._y3);this.point((this||n)._x4,(this||n)._y4);this.point((this||n)._x5,(this||n)._y5);break}},point:function(t,i){t=+t,i=+i;switch((this||n)._point){case 0:(this||n)._point=1;(this||n)._x3=t,(this||n)._y3=i;break;case 1:(this||n)._point=2;(this||n)._context.moveTo((this||n)._x4=t,(this||n)._y4=i);break;case 2:(this||n)._point=3;(this||n)._x5=t,(this||n)._y5=i;break;default:point$1(this||n,t,i);break}(this||n)._x0=(this||n)._x1,(this||n)._x1=(this||n)._x2,(this||n)._x2=t;(this||n)._y0=(this||n)._y1,(this||n)._y1=(this||n)._y2,(this||n)._y2=i}};var X=function custom(t){function cardinal(n){return new CardinalClosed(n,t)}cardinal.tension=function(t){return custom(+t)};return cardinal}(0);function CardinalOpen(t,i){(this||n)._context=t;(this||n)._k=(1-i)/6}CardinalOpen.prototype={areaStart:function(){(this||n)._line=0},areaEnd:function(){(this||n)._line=NaN},lineStart:function(){(this||n)._x0=(this||n)._x1=(this||n)._x2=(this||n)._y0=(this||n)._y1=(this||n)._y2=NaN;(this||n)._point=0},lineEnd:function(){((this||n)._line||0!==(this||n)._line&&3===(this||n)._point)&&(this||n)._context.closePath();(this||n)._line=1-(this||n)._line},point:function(t,i){t=+t,i=+i;switch((this||n)._point){case 0:(this||n)._point=1;break;case 1:(this||n)._point=2;break;case 2:(this||n)._point=3;(this||n)._line?(this||n)._context.lineTo((this||n)._x2,(this||n)._y2):(this||n)._context.moveTo((this||n)._x2,(this||n)._y2);break;case 3:(this||n)._point=4;default:point$1(this||n,t,i);break}(this||n)._x0=(this||n)._x1,(this||n)._x1=(this||n)._x2,(this||n)._x2=t;(this||n)._y0=(this||n)._y1,(this||n)._y1=(this||n)._y2,(this||n)._y2=i}};var Y=function custom(t){function cardinal(n){return new CardinalOpen(n,t)}cardinal.tension=function(t){return custom(+t)};return cardinal}(0);function point$2(t,n,i){var e=t._x1,a=t._y1,s=t._x2,o=t._y2;if(t._l01_a>c){var r=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,l=3*t._l01_a*(t._l01_a+t._l12_a);e=(e*r-t._x0*t._l12_2a+t._x2*t._l01_2a)/l;a=(a*r-t._y0*t._l12_2a+t._y2*t._l01_2a)/l}if(t._l23_a>c){var h=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,_=3*t._l23_a*(t._l23_a+t._l12_a);s=(s*h+t._x1*t._l23_2a-n*t._l12_2a)/_;o=(o*h+t._y1*t._l23_2a-i*t._l12_2a)/_}t._context.bezierCurveTo(e,a,s,o,t._x2,t._y2)}function CatmullRom(t,i){(this||n)._context=t;(this||n)._alpha=i}CatmullRom.prototype={areaStart:function(){(this||n)._line=0},areaEnd:function(){(this||n)._line=NaN},lineStart:function(){(this||n)._x0=(this||n)._x1=(this||n)._x2=(this||n)._y0=(this||n)._y1=(this||n)._y2=NaN;(this||n)._l01_a=(this||n)._l12_a=(this||n)._l23_a=(this||n)._l01_2a=(this||n)._l12_2a=(this||n)._l23_2a=(this||n)._point=0},lineEnd:function(){switch((this||n)._point){case 2:(this||n)._context.lineTo((this||n)._x2,(this||n)._y2);break;case 3:this.point((this||n)._x2,(this||n)._y2);break}((this||n)._line||0!==(this||n)._line&&1===(this||n)._point)&&(this||n)._context.closePath();(this||n)._line=1-(this||n)._line},point:function(t,i){t=+t,i=+i;if((this||n)._point){var e=(this||n)._x2-t,a=(this||n)._y2-i;(this||n)._l23_a=Math.sqrt((this||n)._l23_2a=Math.pow(e*e+a*a,(this||n)._alpha))}switch((this||n)._point){case 0:(this||n)._point=1;(this||n)._line?(this||n)._context.lineTo(t,i):(this||n)._context.moveTo(t,i);break;case 1:(this||n)._point=2;break;case 2:(this||n)._point=3;default:point$2(this||n,t,i);break}(this||n)._l01_a=(this||n)._l12_a,(this||n)._l12_a=(this||n)._l23_a;(this||n)._l01_2a=(this||n)._l12_2a,(this||n)._l12_2a=(this||n)._l23_2a;(this||n)._x0=(this||n)._x1,(this||n)._x1=(this||n)._x2,(this||n)._x2=t;(this||n)._y0=(this||n)._y1,(this||n)._y1=(this||n)._y2,(this||n)._y2=i}};var V=function custom(t){function catmullRom(n){return t?new CatmullRom(n,t):new Cardinal(n,0)}catmullRom.alpha=function(t){return custom(+t)};return catmullRom}(.5);function CatmullRomClosed(t,i){(this||n)._context=t;(this||n)._alpha=i}CatmullRomClosed.prototype={areaStart:noop,areaEnd:noop,lineStart:function(){(this||n)._x0=(this||n)._x1=(this||n)._x2=(this||n)._x3=(this||n)._x4=(this||n)._x5=(this||n)._y0=(this||n)._y1=(this||n)._y2=(this||n)._y3=(this||n)._y4=(this||n)._y5=NaN;(this||n)._l01_a=(this||n)._l12_a=(this||n)._l23_a=(this||n)._l01_2a=(this||n)._l12_2a=(this||n)._l23_2a=(this||n)._point=0},lineEnd:function(){switch((this||n)._point){case 1:(this||n)._context.moveTo((this||n)._x3,(this||n)._y3);(this||n)._context.closePath();break;case 2:(this||n)._context.lineTo((this||n)._x3,(this||n)._y3);(this||n)._context.closePath();break;case 3:this.point((this||n)._x3,(this||n)._y3);this.point((this||n)._x4,(this||n)._y4);this.point((this||n)._x5,(this||n)._y5);break}},point:function(t,i){t=+t,i=+i;if((this||n)._point){var e=(this||n)._x2-t,a=(this||n)._y2-i;(this||n)._l23_a=Math.sqrt((this||n)._l23_2a=Math.pow(e*e+a*a,(this||n)._alpha))}switch((this||n)._point){case 0:(this||n)._point=1;(this||n)._x3=t,(this||n)._y3=i;break;case 1:(this||n)._point=2;(this||n)._context.moveTo((this||n)._x4=t,(this||n)._y4=i);break;case 2:(this||n)._point=3;(this||n)._x5=t,(this||n)._y5=i;break;default:point$2(this||n,t,i);break}(this||n)._l01_a=(this||n)._l12_a,(this||n)._l12_a=(this||n)._l23_a;(this||n)._l01_2a=(this||n)._l12_2a,(this||n)._l12_2a=(this||n)._l23_2a;(this||n)._x0=(this||n)._x1,(this||n)._x1=(this||n)._x2,(this||n)._x2=t;(this||n)._y0=(this||n)._y1,(this||n)._y1=(this||n)._y2,(this||n)._y2=i}};var I=function custom(t){function catmullRom(n){return t?new CatmullRomClosed(n,t):new CardinalClosed(n,0)}catmullRom.alpha=function(t){return custom(+t)};return catmullRom}(.5);function CatmullRomOpen(t,i){(this||n)._context=t;(this||n)._alpha=i}CatmullRomOpen.prototype={areaStart:function(){(this||n)._line=0},areaEnd:function(){(this||n)._line=NaN},lineStart:function(){(this||n)._x0=(this||n)._x1=(this||n)._x2=(this||n)._y0=(this||n)._y1=(this||n)._y2=NaN;(this||n)._l01_a=(this||n)._l12_a=(this||n)._l23_a=(this||n)._l01_2a=(this||n)._l12_2a=(this||n)._l23_2a=(this||n)._point=0},lineEnd:function(){((this||n)._line||0!==(this||n)._line&&3===(this||n)._point)&&(this||n)._context.closePath();(this||n)._line=1-(this||n)._line},point:function(t,i){t=+t,i=+i;if((this||n)._point){var e=(this||n)._x2-t,a=(this||n)._y2-i;(this||n)._l23_a=Math.sqrt((this||n)._l23_2a=Math.pow(e*e+a*a,(this||n)._alpha))}switch((this||n)._point){case 0:(this||n)._point=1;break;case 1:(this||n)._point=2;break;case 2:(this||n)._point=3;(this||n)._line?(this||n)._context.lineTo((this||n)._x2,(this||n)._y2):(this||n)._context.moveTo((this||n)._x2,(this||n)._y2);break;case 3:(this||n)._point=4;default:point$2(this||n,t,i);break}(this||n)._l01_a=(this||n)._l12_a,(this||n)._l12_a=(this||n)._l23_a;(this||n)._l01_2a=(this||n)._l12_2a,(this||n)._l12_2a=(this||n)._l23_2a;(this||n)._x0=(this||n)._x1,(this||n)._x1=(this||n)._x2,(this||n)._x2=t;(this||n)._y0=(this||n)._y1,(this||n)._y1=(this||n)._y2,(this||n)._y2=i}};var D=function custom(t){function catmullRom(n){return t?new CatmullRomOpen(n,t):new CardinalOpen(n,0)}catmullRom.alpha=function(t){return custom(+t)};return catmullRom}(.5);function LinearClosed(t){(this||n)._context=t}LinearClosed.prototype={areaStart:noop,areaEnd:noop,lineStart:function(){(this||n)._point=0},lineEnd:function(){(this||n)._point&&(this||n)._context.closePath()},point:function(t,i){t=+t,i=+i;(this||n)._point?(this||n)._context.lineTo(t,i):((this||n)._point=1,(this||n)._context.moveTo(t,i))}};function linearClosed(t){return new LinearClosed(t)}function sign(t){return t<0?-1:1}function slope3(t,n,i){var e=t._x1-t._x0,a=n-t._x1,s=(t._y1-t._y0)/(e||a<0&&-0),o=(i-t._y1)/(a||e<0&&-0),r=(s*a+o*e)/(e+a);return(sign(s)+sign(o))*Math.min(Math.abs(s),Math.abs(o),.5*Math.abs(r))||0}function slope2(t,n){var i=t._x1-t._x0;return i?(3*(t._y1-t._y0)/i-n)/2:n}function point$3(t,n,i){var e=t._x0,a=t._y0,s=t._x1,o=t._y1,r=(s-e)/3;t._context.bezierCurveTo(e+r,a+r*n,s-r,o-r*i,s,o)}function MonotoneX(t){(this||n)._context=t}MonotoneX.prototype={areaStart:function(){(this||n)._line=0},areaEnd:function(){(this||n)._line=NaN},lineStart:function(){(this||n)._x0=(this||n)._x1=(this||n)._y0=(this||n)._y1=(this||n)._t0=NaN;(this||n)._point=0},lineEnd:function(){switch((this||n)._point){case 2:(this||n)._context.lineTo((this||n)._x1,(this||n)._y1);break;case 3:point$3(this||n,(this||n)._t0,slope2(this||n,(this||n)._t0));break}((this||n)._line||0!==(this||n)._line&&1===(this||n)._point)&&(this||n)._context.closePath();(this||n)._line=1-(this||n)._line},point:function(t,i){var e=NaN;t=+t,i=+i;if(t!==(this||n)._x1||i!==(this||n)._y1){switch((this||n)._point){case 0:(this||n)._point=1;(this||n)._line?(this||n)._context.lineTo(t,i):(this||n)._context.moveTo(t,i);break;case 1:(this||n)._point=2;break;case 2:(this||n)._point=3;point$3(this||n,slope2(this||n,e=slope3(this||n,t,i)),e);break;default:point$3(this||n,(this||n)._t0,e=slope3(this||n,t,i));break}(this||n)._x0=(this||n)._x1,(this||n)._x1=t;(this||n)._y0=(this||n)._y1,(this||n)._y1=i;(this||n)._t0=e}}};function MonotoneY(t){(this||n)._context=new ReflectContext(t)}(MonotoneY.prototype=Object.create(MonotoneX.prototype)).point=function(t,i){MonotoneX.prototype.point.call(this||n,i,t)};function ReflectContext(t){(this||n)._context=t}ReflectContext.prototype={moveTo:function(t,i){(this||n)._context.moveTo(i,t)},closePath:function(){(this||n)._context.closePath()},lineTo:function(t,i){(this||n)._context.lineTo(i,t)},bezierCurveTo:function(t,i,e,a,s,o){(this||n)._context.bezierCurveTo(i,t,a,e,o,s)}};function monotoneX(t){return new MonotoneX(t)}function monotoneY(t){return new MonotoneY(t)}function Natural(t){(this||n)._context=t}Natural.prototype={areaStart:function(){(this||n)._line=0},areaEnd:function(){(this||n)._line=NaN},lineStart:function(){(this||n)._x=[];(this||n)._y=[]},lineEnd:function(){var t=(this||n)._x,i=(this||n)._y,e=t.length;if(e){(this||n)._line?(this||n)._context.lineTo(t[0],i[0]):(this||n)._context.moveTo(t[0],i[0]);if(2===e)(this||n)._context.lineTo(t[1],i[1]);else{var a=controlPoints(t),s=controlPoints(i);for(var o=0,r=1;r=0;--n)a[n]=(o[n]-a[n+1])/s[n];s[i-1]=(t[i]+a[i-1])/2;for(n=0;n=0&&((this||n)._t=1-(this||n)._t,(this||n)._line=1-(this||n)._line)},point:function(t,i){t=+t,i=+i;switch((this||n)._point){case 0:(this||n)._point=1;(this||n)._line?(this||n)._context.lineTo(t,i):(this||n)._context.moveTo(t,i);break;case 1:(this||n)._point=2;default:if((this||n)._t<=0){(this||n)._context.lineTo((this||n)._x,i);(this||n)._context.lineTo(t,i)}else{var e=(this||n)._x*(1-(this||n)._t)+t*(this||n)._t;(this||n)._context.lineTo(e,(this||n)._y);(this||n)._context.lineTo(e,i)}break}(this||n)._x=t,(this||n)._y=i}};function step(t){return new Step(t,.5)}function stepBefore(t){return new Step(t,0)}function stepAfter(t){return new Step(t,1)}function none(t,n){if((o=t.length)>1)for(var i=1,e,a,s=t[n[0]],o,r=s.length;i=0)i[n]=n;return i}function stackValue(t,n){return t[n]}function stack(){var t=constant([]),i=none$1,e=none,a=stackValue;function stack(s){var o=t.apply(this||n,arguments),r,l=s.length,h=o.length,c=new Array(h),_;for(r=0;r0){for(var i,e,a=0,s=t[0].length,o;a0)for(var i,e=0,a,s,o,r,l,h=t[n[0]].length;e0?(a[0]=o,a[1]=o+=s):s<0?(a[1]=r,a[0]=r+=s):(a[0]=0,a[1]=s)}function silhouette(t,n){if((a=t.length)>0){for(var i=0,e=t[n[0]],a,s=e.length;i0&&(s=(a=t[n[0]]).length)>0){for(var i=0,e=1,a,s,o;es&&(s=a,i=n);return i}function ascending(t){var n=t.map(sum);return none$1(t).sort((function(t,i){return n[t]-n[i]}))}function sum(t){var n=0,i=-1,e=t.length,a;while(++i53)return null;"w"in c||(c.w=1);if("Z"in c){f=utcDate(newDate(c.y,0,1)),i=f.getUTCDay();f=i>4||0===i?e.ceil(f):e(f);f=r.offset(f,7*(c.V-1));c.y=f.getUTCFullYear();c.m=f.getUTCMonth();c.d=f.getUTCDate()+(c.w+6)%7}else{f=localDate(newDate(c.y,0,1)),i=f.getDay();f=i>4||0===i?t.ceil(f):t(f);f=n.offset(f,7*(c.V-1));c.y=f.getFullYear();c.m=f.getMonth();c.d=f.getDate()+(c.w+6)%7}}else if("W"in c||"U"in c){"w"in c||(c.w="u"in c?c.u%7:"W"in c?1:0);i="Z"in c?utcDate(newDate(c.y,0,1)).getUTCDay():localDate(newDate(c.y,0,1)).getDay();c.m=0;c.d="W"in c?(c.w+6)%7+7*c.W-(i+5)%7:c.w+7*c.U-(i+6)%7}if("Z"in c){c.H+=c.Z/100|0;c.M+=c.Z%100;return utcDate(c)}return localDate(c)}}function parseSpecifier(e,r,t,n){var a,o,u=0,f=r.length,i=t.length;while(u=i)return-1;a=r.charCodeAt(u++);if(37===a){a=r.charAt(u++);o=W[a in m?r.charAt(u++):a];if(!o||(n=o(e,t,n))<0)return-1}else if(a!=t.charCodeAt(n++))return-1}return n}function parsePeriod(e,r,t){var n=p.exec(r.slice(t));return n?(e.p=y.get(n[0].toLowerCase()),t+n[0].length):-1}function parseShortWeekday(e,r,t){var n=g.exec(r.slice(t));return n?(e.w=U.get(n[0].toLowerCase()),t+n[0].length):-1}function parseWeekday(e,r,t){var n=T.exec(r.slice(t));return n?(e.w=h.get(n[0].toLowerCase()),t+n[0].length):-1}function parseShortMonth(e,r,t){var n=S.exec(r.slice(t));return n?(e.m=D.get(n[0].toLowerCase()),t+n[0].length):-1}function parseMonth(e,r,t){var n=M.exec(r.slice(t));return n?(e.m=C.get(n[0].toLowerCase()),t+n[0].length):-1}function parseLocaleDateTime(e,r,t){return parseSpecifier(e,o,r,t)}function parseLocaleDate(e,r,t){return parseSpecifier(e,u,r,t)}function parseLocaleTime(e,r,t){return parseSpecifier(e,f,r,t)}function formatShortWeekday(e){return s[e.getDay()]}function formatWeekday(e){return c[e.getDay()]}function formatShortMonth(e){return d[e.getMonth()]}function formatMonth(e){return l[e.getMonth()]}function formatPeriod(e){return i[+(e.getHours()>=12)]}function formatQuarter(e){return 1+~~(e.getMonth()/3)}function formatUTCShortWeekday(e){return s[e.getUTCDay()]}function formatUTCWeekday(e){return c[e.getUTCDay()]}function formatUTCShortMonth(e){return d[e.getUTCMonth()]}function formatUTCMonth(e){return l[e.getUTCMonth()]}function formatUTCPeriod(e){return i[+(e.getUTCHours()>=12)]}function formatUTCQuarter(e){return 1+~~(e.getUTCMonth()/3)}return{format:function(e){var r=newFormat(e+="",v);r.toString=function(){return e};return r},parse:function(e){var r=newParse(e+="",false);r.toString=function(){return e};return r},utcFormat:function(e){var r=newFormat(e+="",w);r.toString=function(){return e};return r},utcParse:function(e){var r=newParse(e+="",true);r.toString=function(){return e};return r}}}var m={"-":"",_:" ",0:"0"},s=/^\s*\d+/,l=/^%/,d=/[\\^$*+?|[\]().{}]/g;function pad(e,r,t){var n=e<0?"-":"",a=(n?-e:e)+"",o=a.length;return n+(o[e.toLowerCase(),r])))}function parseWeekdayNumberSunday(e,r,t){var n=s.exec(r.slice(t,t+1));return n?(e.w=+n[0],t+n[0].length):-1}function parseWeekdayNumberMonday(e,r,t){var n=s.exec(r.slice(t,t+1));return n?(e.u=+n[0],t+n[0].length):-1}function parseWeekNumberSunday(e,r,t){var n=s.exec(r.slice(t,t+2));return n?(e.U=+n[0],t+n[0].length):-1}function parseWeekNumberISO(e,r,t){var n=s.exec(r.slice(t,t+2));return n?(e.V=+n[0],t+n[0].length):-1}function parseWeekNumberMonday(e,r,t){var n=s.exec(r.slice(t,t+2));return n?(e.W=+n[0],t+n[0].length):-1}function parseFullYear(e,r,t){var n=s.exec(r.slice(t,t+4));return n?(e.y=+n[0],t+n[0].length):-1}function parseYear(e,r,t){var n=s.exec(r.slice(t,t+2));return n?(e.y=+n[0]+(+n[0]>68?1900:2e3),t+n[0].length):-1}function parseZone(e,r,t){var n=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(r.slice(t,t+6));return n?(e.Z=n[1]?0:-(n[2]+(n[3]||"00")),t+n[0].length):-1}function parseQuarter(e,r,t){var n=s.exec(r.slice(t,t+1));return n?(e.q=3*n[0]-3,t+n[0].length):-1}function parseMonthNumber(e,r,t){var n=s.exec(r.slice(t,t+2));return n?(e.m=n[0]-1,t+n[0].length):-1}function parseDayOfMonth(e,r,t){var n=s.exec(r.slice(t,t+2));return n?(e.d=+n[0],t+n[0].length):-1}function parseDayOfYear(e,r,t){var n=s.exec(r.slice(t,t+3));return n?(e.m=0,e.d=+n[0],t+n[0].length):-1}function parseHour24(e,r,t){var n=s.exec(r.slice(t,t+2));return n?(e.H=+n[0],t+n[0].length):-1}function parseMinutes(e,r,t){var n=s.exec(r.slice(t,t+2));return n?(e.M=+n[0],t+n[0].length):-1}function parseSeconds(e,r,t){var n=s.exec(r.slice(t,t+2));return n?(e.S=+n[0],t+n[0].length):-1}function parseMilliseconds(e,r,t){var n=s.exec(r.slice(t,t+3));return n?(e.L=+n[0],t+n[0].length):-1}function parseMicroseconds(e,r,t){var n=s.exec(r.slice(t,t+6));return n?(e.L=Math.floor(n[0]/1e3),t+n[0].length):-1}function parseLiteralPercent(e,r,t){var n=l.exec(r.slice(t,t+1));return n?t+n[0].length:-1}function parseUnixTimestamp(e,r,t){var n=s.exec(r.slice(t));return n?(e.Q=+n[0],t+n[0].length):-1}function parseUnixTimestampSeconds(e,r,t){var n=s.exec(r.slice(t));return n?(e.s=+n[0],t+n[0].length):-1}function formatDayOfMonth(e,r){return pad(e.getDate(),r,2)}function formatHour24(e,r){return pad(e.getHours(),r,2)}function formatHour12(e,r){return pad(e.getHours()%12||12,r,2)}function formatDayOfYear(e,r){return pad(1+n.count(a(e),e),r,3)}function formatMilliseconds(e,r){return pad(e.getMilliseconds(),r,3)}function formatMicroseconds(e,r){return formatMilliseconds(e,r)+"000"}function formatMonthNumber(e,r){return pad(e.getMonth()+1,r,2)}function formatMinutes(e,r){return pad(e.getMinutes(),r,2)}function formatSeconds(e,r){return pad(e.getSeconds(),r,2)}function formatWeekdayNumberMonday(e){var r=e.getDay();return 0===r?7:r}function formatWeekNumberSunday(e,r){return pad(o.count(a(e)-1,e),r,2)}function dISO(e){var r=e.getDay();return r>=4||0===r?u(e):u.ceil(e)}function formatWeekNumberISO(e,r){e=dISO(e);return pad(u.count(a(e),e)+(4===a(e).getDay()),r,2)}function formatWeekdayNumberSunday(e){return e.getDay()}function formatWeekNumberMonday(e,r){return pad(t.count(a(e)-1,e),r,2)}function formatYear(e,r){return pad(e.getFullYear()%100,r,2)}function formatYearISO(e,r){e=dISO(e);return pad(e.getFullYear()%100,r,2)}function formatFullYear(e,r){return pad(e.getFullYear()%1e4,r,4)}function formatFullYearISO(e,r){var t=e.getDay();e=t>=4||0===t?u(e):u.ceil(e);return pad(e.getFullYear()%1e4,r,4)}function formatZone(e){var r=e.getTimezoneOffset();return(r>0?"-":(r*=-1,"+"))+pad(r/60|0,"0",2)+pad(r%60,"0",2)}function formatUTCDayOfMonth(e,r){return pad(e.getUTCDate(),r,2)}function formatUTCHour24(e,r){return pad(e.getUTCHours(),r,2)}function formatUTCHour12(e,r){return pad(e.getUTCHours()%12||12,r,2)}function formatUTCDayOfYear(e,t){return pad(1+r.count(f(e),e),t,3)}function formatUTCMilliseconds(e,r){return pad(e.getUTCMilliseconds(),r,3)}function formatUTCMicroseconds(e,r){return formatUTCMilliseconds(e,r)+"000"}function formatUTCMonthNumber(e,r){return pad(e.getUTCMonth()+1,r,2)}function formatUTCMinutes(e,r){return pad(e.getUTCMinutes(),r,2)}function formatUTCSeconds(e,r){return pad(e.getUTCSeconds(),r,2)}function formatUTCWeekdayNumberMonday(e){var r=e.getUTCDay();return 0===r?7:r}function formatUTCWeekNumberSunday(e,r){return pad(i.count(f(e)-1,e),r,2)}function UTCdISO(e){var r=e.getUTCDay();return r>=4||0===r?c(e):c.ceil(e)}function formatUTCWeekNumberISO(e,r){e=UTCdISO(e);return pad(c.count(f(e),e)+(4===f(e).getUTCDay()),r,2)}function formatUTCWeekdayNumberSunday(e){return e.getUTCDay()}function formatUTCWeekNumberMonday(r,t){return pad(e.count(f(r)-1,r),t,2)}function formatUTCYear(e,r){return pad(e.getUTCFullYear()%100,r,2)}function formatUTCYearISO(e,r){e=UTCdISO(e);return pad(e.getUTCFullYear()%100,r,2)}function formatUTCFullYear(e,r){return pad(e.getUTCFullYear()%1e4,r,4)}function formatUTCFullYearISO(e,r){var t=e.getUTCDay();e=t>=4||0===t?c(e):c.ceil(e);return pad(e.getUTCFullYear()%1e4,r,4)}function formatUTCZone(){return"+0000"}function formatLiteralPercent(){return"%"}function formatUnixTimestamp(e){return+e}function formatUnixTimestampSeconds(e){return Math.floor(+e/1e3)}var p;var y;var T;var h;var g;defaultLocale({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});function defaultLocale(e){p=formatLocale(e);y=p.format;T=p.parse;h=p.utcFormat;g=p.utcParse;return p}var U="%Y-%m-%dT%H:%M:%S.%LZ";function formatIsoNative(e){return e.toISOString()}var M=Date.prototype.toISOString?formatIsoNative:h(U);function parseIsoNative(e){var r=new Date(e);return isNaN(r)?null:r}var C=+new Date("2000-01-01T00:00:00.000Z")?parseIsoNative:g(U);export{M as isoFormat,C as isoParse,y as timeFormat,defaultLocale as timeFormatDefaultLocale,formatLocale as timeFormatLocale,T as timeParse,h as utcFormat,g as utcParse}; diff --git a/vendor/javascript/d3-time.js b/vendor/javascript/d3-time.js index 7f5538e3..1f61a2ef 100644 --- a/vendor/javascript/d3-time.js +++ b/vendor/javascript/d3-time.js @@ -1,2 +1,4 @@ +// d3-time@3.1.0 downloaded from https://ga.jspm.io/npm:d3-time@3.1.0/src/index.js + import{bisector as e,tickStep as t}from"d3-array";const n=new Date,s=new Date;function timeInterval(e,t,r,a){function interval(t){return e(t=0===arguments.length?new Date:new Date(+t)),t}interval.floor=t=>(e(t=new Date(+t)),t);interval.ceil=n=>(e(n=new Date(n-1)),t(n,1),e(n),n);interval.round=e=>{const t=interval(e),n=interval.ceil(e);return e-t(t(e=new Date(+e),null==n?1:Math.floor(n)),e);interval.range=(n,s,r)=>{const a=[];n=interval.ceil(n);r=null==r?1:Math.floor(r);if(!(n0))return a;let o;do{a.push(o=new Date(+n)),t(n,r),e(n)}while(otimeInterval((t=>{if(t>=t)while(e(t),!n(t))t.setTime(t-1)}),((e,s)=>{if(e>=e)if(s<0)while(++s<=0)while(t(e,-1),!n(e));else while(--s>=0)while(t(e,1),!n(e));}));if(r){interval.count=(t,a)=>{n.setTime(+t),s.setTime(+a);e(n),e(s);return Math.floor(r(n,s))};interval.every=e=>{e=Math.floor(e);return isFinite(e)&&e>0?e>1?interval.filter(a?t=>a(t)%e===0:t=>interval.count(0,t)%e===0):interval:null}}return interval}const r=timeInterval((()=>{}),((e,t)=>{e.setTime(+e+t)}),((e,t)=>t-e));r.every=e=>{e=Math.floor(e);return isFinite(e)&&e>0?e>1?timeInterval((t=>{t.setTime(Math.floor(t/e)*e)}),((t,n)=>{t.setTime(+t+n*e)}),((t,n)=>(n-t)/e)):r:null};const a=r.range;const o=1e3;const l=60*o;const i=60*l;const c=24*i;const u=7*c;const g=30*c;const T=365*c;const m=timeInterval((e=>{e.setTime(e-e.getMilliseconds())}),((e,t)=>{e.setTime(+e+t*o)}),((e,t)=>(t-e)/o),(e=>e.getUTCSeconds()));const v=m.range;const f=timeInterval((e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*o)}),((e,t)=>{e.setTime(+e+t*l)}),((e,t)=>(t-e)/l),(e=>e.getMinutes()));const C=f.range;const U=timeInterval((e=>{e.setUTCSeconds(0,0)}),((e,t)=>{e.setTime(+e+t*l)}),((e,t)=>(t-e)/l),(e=>e.getUTCMinutes()));const M=U.range;const h=timeInterval((e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*o-e.getMinutes()*l)}),((e,t)=>{e.setTime(+e+t*i)}),((e,t)=>(t-e)/i),(e=>e.getHours()));const d=h.range;const k=timeInterval((e=>{e.setUTCMinutes(0,0,0)}),((e,t)=>{e.setTime(+e+t*i)}),((e,t)=>(t-e)/i),(e=>e.getUTCHours()));const D=k.range;const y=timeInterval((e=>e.setHours(0,0,0,0)),((e,t)=>e.setDate(e.getDate()+t)),((e,t)=>(t-e-(t.getTimezoneOffset()-e.getTimezoneOffset())*l)/c),(e=>e.getDate()-1));const F=y.range;const I=timeInterval((e=>{e.setUTCHours(0,0,0,0)}),((e,t)=>{e.setUTCDate(e.getUTCDate()+t)}),((e,t)=>(t-e)/c),(e=>e.getUTCDate()-1));const Y=I.range;const W=timeInterval((e=>{e.setUTCHours(0,0,0,0)}),((e,t)=>{e.setUTCDate(e.getUTCDate()+t)}),((e,t)=>(t-e)/c),(e=>Math.floor(e/c)));const w=W.range;function timeWeekday(e){return timeInterval((t=>{t.setDate(t.getDate()-(t.getDay()+7-e)%7);t.setHours(0,0,0,0)}),((e,t)=>{e.setDate(e.getDate()+7*t)}),((e,t)=>(t-e-(t.getTimezoneOffset()-e.getTimezoneOffset())*l)/u))}const H=timeWeekday(0);const S=timeWeekday(1);const p=timeWeekday(2);const z=timeWeekday(3);const O=timeWeekday(4);const x=timeWeekday(5);const b=timeWeekday(6);const j=H.range;const q=S.range;const A=p.range;const B=z.range;const E=O.range;const G=x.range;const J=b.range;function utcWeekday(e){return timeInterval((t=>{t.setUTCDate(t.getUTCDate()-(t.getUTCDay()+7-e)%7);t.setUTCHours(0,0,0,0)}),((e,t)=>{e.setUTCDate(e.getUTCDate()+7*t)}),((e,t)=>(t-e)/u))}const K=utcWeekday(0);const L=utcWeekday(1);const N=utcWeekday(2);const P=utcWeekday(3);const Q=utcWeekday(4);const R=utcWeekday(5);const V=utcWeekday(6);const X=K.range;const Z=L.range;const $=N.range;const _=P.range;const ee=Q.range;const te=R.range;const ne=V.range;const se=timeInterval((e=>{e.setDate(1);e.setHours(0,0,0,0)}),((e,t)=>{e.setMonth(e.getMonth()+t)}),((e,t)=>t.getMonth()-e.getMonth()+12*(t.getFullYear()-e.getFullYear())),(e=>e.getMonth()));const re=se.range;const ae=timeInterval((e=>{e.setUTCDate(1);e.setUTCHours(0,0,0,0)}),((e,t)=>{e.setUTCMonth(e.getUTCMonth()+t)}),((e,t)=>t.getUTCMonth()-e.getUTCMonth()+12*(t.getUTCFullYear()-e.getUTCFullYear())),(e=>e.getUTCMonth()));const oe=ae.range;const le=timeInterval((e=>{e.setMonth(0,1);e.setHours(0,0,0,0)}),((e,t)=>{e.setFullYear(e.getFullYear()+t)}),((e,t)=>t.getFullYear()-e.getFullYear()),(e=>e.getFullYear()));le.every=e=>isFinite(e=Math.floor(e))&&e>0?timeInterval((t=>{t.setFullYear(Math.floor(t.getFullYear()/e)*e);t.setMonth(0,1);t.setHours(0,0,0,0)}),((t,n)=>{t.setFullYear(t.getFullYear()+n*e)})):null;const ie=le.range;const ce=timeInterval((e=>{e.setUTCMonth(0,1);e.setUTCHours(0,0,0,0)}),((e,t)=>{e.setUTCFullYear(e.getUTCFullYear()+t)}),((e,t)=>t.getUTCFullYear()-e.getUTCFullYear()),(e=>e.getUTCFullYear()));ce.every=e=>isFinite(e=Math.floor(e))&&e>0?timeInterval((t=>{t.setUTCFullYear(Math.floor(t.getUTCFullYear()/e)*e);t.setUTCMonth(0,1);t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCFullYear(t.getUTCFullYear()+n*e)})):null;const ue=ce.range;function ticker(n,s,a,v,f,C){const U=[[m,1,o],[m,5,5*o],[m,15,15*o],[m,30,30*o],[C,1,l],[C,5,5*l],[C,15,15*l],[C,30,30*l],[f,1,i],[f,3,3*i],[f,6,6*i],[f,12,12*i],[v,1,c],[v,2,2*c],[a,1,u],[s,1,g],[s,3,3*g],[n,1,T]];function ticks(e,t,n){const s=te)).right(U,l);if(i===U.length)return n.every(t(s/T,a/T,o));if(0===i)return r.every(Math.max(t(s,a,o),1));const[c,u]=U[l/U[i-1][2]=0&&i._call.call(void 0,e);i=i._next}--n}function wake(){a=(l=s.now())+u;n=i=0;try{timerFlush()}finally{n=0;nap();a=0}}function poke(){var t=s.now(),e=t-l;e>o&&(u-=e,l=t)}function nap(){var n,i,r=t,o=Infinity;while(r)if(r._call){o>r._time&&(o=r._time);n=r,r=r._next}else{i=r._next,r._next=null;r=n?n._next=i:t=i}e=n;sleep(o)}function sleep(t){if(!n){i&&(i=clearTimeout(i));var e=t-a;if(e>24){t{i.stop();t(n+e)}),e,n);return i}function interval(t,e,n){var i=new Timer,r=e;if(null==e)return i.restart(t,e,n),i;i._restart=i.restart;i.restart=function(t,e,n){e=+e,n=null==n?now():+n;i._restart((function tick(o){o+=r;i._restart(tick,r+=e,n);t(o)}),e,n)};i.restart(t,e,n);return i}export{interval,now,timeout,timer,timerFlush}; diff --git a/vendor/javascript/d3-transition.js b/vendor/javascript/d3-transition.js index f47dc858..e57459d3 100644 --- a/vendor/javascript/d3-transition.js +++ b/vendor/javascript/d3-transition.js @@ -1,2 +1,4 @@ +// d3-transition@3.0.1 downloaded from https://ga.jspm.io/npm:d3-transition@3.0.1/src/index.js + import{namespace as t,matcher as n,selector as e,selectorAll as r,selection as i,style as o}from"d3-selection";import{dispatch as a}from"d3-dispatch";import{timer as s,timeout as u,now as l}from"d3-timer";import{interpolateNumber as c,interpolateRgb as f,interpolateString as h,interpolateTransformSvg as _,interpolateTransformCss as v}from"d3-interpolate";import{color as d}from"d3-color";import{easeCubicInOut as p}from"d3-ease";var y=a("start","end","cancel","interrupt");var w=[];var m=0;var g=1;var T=2;var x=3;var C=4;var A=5;var N=6;function schedule(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};create(t,e,{name:n,index:r,group:i,on:y,tween:w,time:o.time,delay:o.delay,duration:o.duration,ease:o.ease,timer:null,state:m})}function init(t,n){var e=get(t,n);if(e.state>m)throw new Error("too late; already scheduled");return e}function set(t,n){var e=get(t,n);if(e.state>x)throw new Error("too late; already running");return e}function get(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function create(t,n,e){var r,i=t.__transition;i[n]=e;e.timer=s(schedule,0,e.time);function schedule(t){e.state=g;e.timer.restart(start,e.delay,e.time);e.delay<=t&&start(t-e.delay)}function start(o){var a,s,l,c;if(e.state!==g)return stop();for(a in i){c=i[a];if(c.name===e.name){if(c.state===x)return u(start);if(c.state===C){c.state=N;c.timer.stop();c.on.call("interrupt",t,t.__data__,c.index,c.group);delete i[a]}else if(+aT&&e.state=0&&(t=t.slice(0,n));return!t||"start"===t}))}function onFunction(t,n,e){var r,i,o=start(n)?init:set;return function(){var a=o(this,t),s=a.on;s!==r&&(i=(r=s).copy()).on(n,e);a.on=i}}function transition_on(t,n){var e=this._id;return arguments.length<2?get(this.node(),e).on.on(t):this.each(onFunction(e,t,n))}function removeFunction(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function transition_remove(){return this.on("end.remove",removeFunction(this._id))}function transition_select(t){var n=this._name,r=this._id;"function"!==typeof t&&(t=e(t));for(var i=this._groups,o=i.length,a=new Array(o),s=0;sg&&e.name===n)return new Transition([[t]],I,n,+r)}return null}export{active,interrupt,transition}; diff --git a/vendor/javascript/d3-zoom.js b/vendor/javascript/d3-zoom.js index f155a61d..2d59d2ff 100644 --- a/vendor/javascript/d3-zoom.js +++ b/vendor/javascript/d3-zoom.js @@ -1,2 +1,4 @@ +// d3-zoom@3.0.0 downloaded from https://ga.jspm.io/npm:d3-zoom@3.0.0/src/index.js + import{dispatch as t}from"d3-dispatch";import{dragDisable as o,dragEnable as e}from"d3-drag";import{interpolateZoom as n}from"d3-interpolate";import{select as i,pointer as r}from"d3-selection";import{interrupt as u}from"d3-transition";var constant=t=>()=>t;function ZoomEvent(t,{sourceEvent:o,target:e,transform:n,dispatch:i}){Object.defineProperties(this,{type:{value:t,enumerable:true,configurable:true},sourceEvent:{value:o,enumerable:true,configurable:true},target:{value:e,enumerable:true,configurable:true},transform:{value:n,enumerable:true,configurable:true},_:{value:i}})}function Transform(t,o,e){this.k=t;this.x=o;this.y=e}Transform.prototype={constructor:Transform,scale:function(t){return 1===t?this:new Transform(this.k*t,this.x,this.y)},translate:function(t,o){return 0===t&0===o?this:new Transform(this.k,this.x+this.k*t,this.y+this.k*o)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var s=new Transform(1,0,0);transform.prototype=Transform.prototype;function transform(t){while(!t.__zoom)if(!(t=t.parentNode))return s;return t.__zoom}function nopropagation(t){t.stopImmediatePropagation()}function noevent(t){t.preventDefault();t.stopImmediatePropagation()}function defaultFilter(t){return(!t.ctrlKey||"wheel"===t.type)&&!t.button}function defaultExtent(){var t=this;if(t instanceof SVGElement){t=t.ownerSVGElement||t;if(t.hasAttribute("viewBox")){t=t.viewBox.baseVal;return[[t.x,t.y],[t.x+t.width,t.y+t.height]]}return[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]}return[[0,0],[t.clientWidth,t.clientHeight]]}function defaultTransform(){return this.__zoom||s}function defaultWheelDelta(t){return-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function defaultTouchable(){return navigator.maxTouchPoints||"ontouchstart"in this}function defaultConstrain(t,o,e){var n=t.invertX(o[0][0])-e[0][0],i=t.invertX(o[1][0])-e[1][0],r=t.invertY(o[0][1])-e[0][1],u=t.invertY(o[1][1])-e[1][1];return t.translate(i>n?(n+i)/2:Math.min(0,n)||Math.max(0,i),u>r?(r+u)/2:Math.min(0,r)||Math.max(0,u))}function zoom(){var a,h,c,l=defaultFilter,m=defaultExtent,f=defaultConstrain,p=defaultWheelDelta,d=defaultTouchable,v=[0,Infinity],z=[[-Infinity,-Infinity],[Infinity,Infinity]],y=250,g=n,_=t("start","zoom","end"),w=500,T=150,k=0,x=10;function zoom(t){t.property("__zoom",defaultTransform).on("wheel.zoom",wheeled,{passive:false}).on("mousedown.zoom",mousedowned).on("dblclick.zoom",dblclicked).filter(d).on("touchstart.zoom",touchstarted).on("touchmove.zoom",touchmoved).on("touchend.zoom touchcancel.zoom",touchended).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}zoom.transform=function(t,o,e,n){var i=t.selection?t.selection():t;i.property("__zoom",defaultTransform);t!==i?schedule(t,o,e,n):i.interrupt().each((function(){gesture(this,arguments).event(n).start().zoom(null,"function"===typeof o?o.apply(this,arguments):o).end()}))};zoom.scaleBy=function(t,o,e,n){zoom.scaleTo(t,(function(){var t=this.__zoom.k,e="function"===typeof o?o.apply(this,arguments):o;return t*e}),e,n)};zoom.scaleTo=function(t,o,e,n){zoom.transform(t,(function(){var t=m.apply(this,arguments),n=this.__zoom,i=null==e?centroid(t):"function"===typeof e?e.apply(this,arguments):e,r=n.invert(i),u="function"===typeof o?o.apply(this,arguments):o;return f(translate(scale(n,u),i,r),t,z)}),e,n)};zoom.translateBy=function(t,o,e,n){zoom.transform(t,(function(){return f(this.__zoom.translate("function"===typeof o?o.apply(this,arguments):o,"function"===typeof e?e.apply(this,arguments):e),m.apply(this,arguments),z)}),null,n)};zoom.translateTo=function(t,o,e,n,i){zoom.transform(t,(function(){var t=m.apply(this,arguments),i=this.__zoom,r=null==n?centroid(t):"function"===typeof n?n.apply(this,arguments):n;return f(s.translate(r[0],r[1]).scale(i.k).translate("function"===typeof o?-o.apply(this,arguments):-o,"function"===typeof e?-e.apply(this,arguments):-e),t,z)}),n,i)};function scale(t,o){o=Math.max(v[0],Math.min(v[1],o));return o===t.k?t:new Transform(o,t.x,t.y)}function translate(t,o,e){var n=o[0]-e[0]*t.k,i=o[1]-e[1]*t.k;return n===t.x&&i===t.y?t:new Transform(t.k,n,i)}function centroid(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function schedule(t,o,e,n){t.on("start.zoom",(function(){gesture(this,arguments).event(n).start()})).on("interrupt.zoom end.zoom",(function(){gesture(this,arguments).event(n).end()})).tween("zoom",(function(){var t=this,i=arguments,r=gesture(t,i).event(n),u=m.apply(t,i),s=null==e?centroid(u):"function"===typeof e?e.apply(t,i):e,a=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),h=t.__zoom,c="function"===typeof o?o.apply(t,i):o,l=g(h.invert(s).concat(a/h.k),c.invert(s).concat(a/c.k));return function(t){if(1===t)t=c;else{var o=l(t),e=a/o[2];t=new Transform(e,s[0]-o[0]*e,s[1]-o[1]*e)}r.zoom(null,t)}}))}function gesture(t,o,e){return!e&&t.__zooming||new Gesture(t,o)}function Gesture(t,o){this.that=t;this.args=o;this.active=0;this.sourceEvent=null;this.extent=m.apply(t,o);this.taps=0}Gesture.prototype={event:function(t){t&&(this.sourceEvent=t);return this},start:function(){if(1===++this.active){this.that.__zooming=this;this.emit("start")}return this},zoom:function(t,o){this.mouse&&"mouse"!==t&&(this.mouse[1]=o.invert(this.mouse[0]));this.touch0&&"touch"!==t&&(this.touch0[1]=o.invert(this.touch0[0]));this.touch1&&"touch"!==t&&(this.touch1[1]=o.invert(this.touch1[0]));this.that.__zoom=o;this.emit("zoom");return this},end:function(){if(0===--this.active){delete this.that.__zooming;this.emit("end")}return this},emit:function(t){var o=i(this.that).datum();_.call(t,this.that,new ZoomEvent(t,{sourceEvent:this.sourceEvent,target:zoom,type:t,transform:this.that.__zoom,dispatch:_}),o)}};function wheeled(t,...o){if(l.apply(this,arguments)){var e=gesture(this,o).event(t),n=this.__zoom,i=Math.max(v[0],Math.min(v[1],n.k*Math.pow(2,p.apply(this,arguments)))),s=r(t);if(e.wheel){e.mouse[0][0]===s[0]&&e.mouse[0][1]===s[1]||(e.mouse[1]=n.invert(e.mouse[0]=s));clearTimeout(e.wheel)}else{if(n.k===i)return;e.mouse=[s,n.invert(s)];u(this);e.start()}noevent(t);e.wheel=setTimeout(wheelidled,T);e.zoom("mouse",f(translate(scale(n,i),e.mouse[0],e.mouse[1]),e.extent,z))}function wheelidled(){e.wheel=null;e.end()}}function mousedowned(t,...n){if(!c&&l.apply(this,arguments)){var s=t.currentTarget,a=gesture(this,n,true).event(t),h=i(t.view).on("mousemove.zoom",mousemoved,true).on("mouseup.zoom",mouseupped,true),m=r(t,s),p=t.clientX,d=t.clientY;o(t.view);nopropagation(t);a.mouse=[m,this.__zoom.invert(m)];u(this);a.start()}function mousemoved(t){noevent(t);if(!a.moved){var o=t.clientX-p,e=t.clientY-d;a.moved=o*o+e*e>k}a.event(t).zoom("mouse",f(translate(a.that.__zoom,a.mouse[0]=r(t,s),a.mouse[1]),a.extent,z))}function mouseupped(t){h.on("mousemove.zoom mouseup.zoom",null);e(t.view,a.moved);noevent(t);a.event(t).end()}}function dblclicked(t,...o){if(l.apply(this,arguments)){var e=this.__zoom,n=r(t.changedTouches?t.changedTouches[0]:t,this),u=e.invert(n),s=e.k*(t.shiftKey?.5:2),a=f(translate(scale(e,s),n,u),m.apply(this,o),z);noevent(t);y>0?i(this).transition().duration(y).call(schedule,a,n,t):i(this).call(zoom.transform,a,n,t)}}function touchstarted(t,...o){if(l.apply(this,arguments)){var e,n,i,s,c=t.touches,m=c.length,f=gesture(this,o,t.changedTouches.length===m).event(t);nopropagation(t);for(n=0;n>1;if(s>0&&typeof t[0]!=="number")throw new Error("Expected coords to contain numbers.");this.coords=t;const i=Math.max(2*s-5,0);this._triangles=new Uint32Array(i*3);this._halfedges=new Int32Array(i*3);this._hashSize=Math.ceil(Math.sqrt(s));this._hullPrev=new Uint32Array(s);this._hullNext=new Uint32Array(s);this._hullTri=new Uint32Array(s);this._hullHash=new Int32Array(this._hashSize);this._ids=new Uint32Array(s);this._dists=new Float64Array(s);this.update()}update(){const{coords:i,_hullPrev:n,_hullNext:e,_hullTri:h,_hullHash:l}=this;const r=i.length>>1;let o=Infinity;let c=Infinity;let a=-Infinity;let u=-Infinity;for(let t=0;ta&&(a=s);n>u&&(u=n);this._ids[t]=t}const _=(o+a)/2;const f=(c+u)/2;let d,y,g;for(let t=0,s=Infinity;t0){y=t;s=n}}let b=i[2*y];let p=i[2*y+1];let A=Infinity;for(let t=0;tn){t[s++]=e;n=h}}this.hull=t.subarray(0,s);this.triangles=new Uint32Array(0);this.halfedges=new Uint32Array(0);return}if(t(w,k,b,p,I,S)<0){const t=y;const s=b;const i=p;y=g;b=I;p=S;g=t;I=s;S=i}const m=circumcenter(w,k,b,p,I,S);this._cx=m.x;this._cy=m.y;for(let t=0;t0&&Math.abs(u-r)<=s&&Math.abs(_-o)<=s)continue;r=u;o=_;if(a===d||a===y||a===g)continue;let f=0;for(let t=0,s=this._hashKey(u,_);t=0){k=w;if(k===f){k=-1;break}}if(k===-1)continue;let b=this._addTriangle(k,a,e[k],-1,-1,h[k]);h[a]=this._legalize(b+2);h[k]=b;x++;let p=e[k];while(w=e[p],t(u,_,i[2*p],i[2*p+1],i[2*w],i[2*w+1])<0){b=this._addTriangle(p,a,w,h[a],-1,h[p]);h[a]=this._legalize(b+2);e[p]=p;x--;p=w}if(k===f)while(w=n[k],t(u,_,i[2*w],i[2*w+1],i[2*k],i[2*k+1])<0){b=this._addTriangle(w,a,k,-1,h[k],h[w]);this._legalize(b+2);h[w]=b;e[k]=k;x--;k=w}this._hullStart=n[a]=k;e[k]=n[p]=a;e[a]=p;l[this._hashKey(u,_)]=a;l[this._hashKey(i[2*k],i[2*k+1])]=k}this.hull=new Uint32Array(x);for(let t=0,s=this._hullStart;t0?3-i:1+i)/4}function dist(t,s,i,n){const e=t-i;const h=s-n;return e*e+h*h}function inCircle(t,s,i,n,e,h,l,r){const o=t-l;const c=s-r;const a=i-l;const u=n-r;const _=e-l;const f=h-r;const d=o*o+c*c;const y=a*a+u*u;const g=_*_+f*f;return o*(u*g-y*f)-c*(a*g-y*_)+d*(a*f-u*_)<0}function circumradius(t,s,i,n,e,h){const l=i-t;const r=n-s;const o=e-t;const c=h-s;const a=l*l+r*r;const u=o*o+c*c;const _=.5/(l*c-r*o);const f=(c*a-r*u)*_;const d=(l*u-o*a)*_;return f*f+d*d}function circumcenter(t,s,i,n,e,h){const l=i-t;const r=n-s;const o=e-t;const c=h-s;const a=l*l+r*r;const u=o*o+c*c;const _=.5/(l*c-r*o);const f=t+(c*a-r*u)*_;const d=s+(l*u-o*a)*_;return{x:f,y:d}}function quicksort(t,s,i,n){if(n-i<=20)for(let e=i+1;e<=n;e++){const n=t[e];const h=s[n];let l=e-1;while(l>=i&&s[t[l]]>h)t[l+1]=t[l--];t[l+1]=n}else{const e=i+n>>1;let h=i+1;let l=n;swap(t,e,h);s[t[i]]>s[t[n]]&&swap(t,i,n);s[t[h]]>s[t[n]]&&swap(t,h,n);s[t[i]]>s[t[h]]&&swap(t,i,h);const r=t[h];const o=s[r];while(true){do{h++}while(s[t[h]]o);if(l=l-i){quicksort(t,s,h,n);quicksort(t,s,i,l-1)}else{quicksort(t,s,i,l-1);quicksort(t,s,h,n)}}}function swap(t,s,i){const n=t[s];t[s]=t[i];t[i]=n}function defaultGetX(t){return t[0]}function defaultGetY(t){return t[1]}export{Delaunator as default}; diff --git a/vendor/javascript/internmap.js b/vendor/javascript/internmap.js index 8f8ace93..d1aea069 100644 --- a/vendor/javascript/internmap.js +++ b/vendor/javascript/internmap.js @@ -1,2 +1,4 @@ +// internmap@2.0.3 downloaded from https://ga.jspm.io/npm:internmap@2.0.3/src/index.js + class InternMap extends Map{constructor(e,t=keyof){super();Object.defineProperties(this,{_intern:{value:new Map},_key:{value:t}});if(null!=e)for(const[t,n]of e)this.set(t,n)}get(e){return super.get(intern_get(this,e))}has(e){return super.has(intern_get(this,e))}set(e,t){return super.set(intern_set(this,e),t)}delete(e){return super.delete(intern_delete(this,e))}}class InternSet extends Set{constructor(e,t=keyof){super();Object.defineProperties(this,{_intern:{value:new Map},_key:{value:t}});if(null!=e)for(const t of e)this.add(t)}has(e){return super.has(intern_get(this,e))}add(e){return super.add(intern_set(this,e))}delete(e){return super.delete(intern_delete(this,e))}}function intern_get({_intern:e,_key:t},n){const r=t(n);return e.has(r)?e.get(r):n}function intern_set({_intern:e,_key:t},n){const r=t(n);if(e.has(r))return e.get(r);e.set(r,n);return n}function intern_delete({_intern:e,_key:t},n){const r=t(n);if(e.has(r)){n=e.get(r);e.delete(r)}return n}function keyof(e){return null!==e&&"object"===typeof e?e.valueOf():e}export{InternMap,InternSet}; diff --git a/vendor/javascript/robust-predicates.js b/vendor/javascript/robust-predicates.js index d82a84a8..c89bb214 100644 --- a/vendor/javascript/robust-predicates.js +++ b/vendor/javascript/robust-predicates.js @@ -1,2 +1,4 @@ +// robust-predicates@3.0.2 downloaded from https://ga.jspm.io/npm:robust-predicates@3.0.2/index.js + const c=11102230246251565e-32;const s=134217729;const t=(3+8*c)*c;function sum(c,s,t,n,e){let o,a,l,i;let r=s[0];let f=n[0];let u=0;let d=0;if(f>r===f>-r){o=r;r=s[++u]}else{o=f;f=n[++d]}let v=0;if(ur===f>-r){a=r+o;l=o-(a-r);r=s[++u]}else{a=f+o;l=o-(a-f);f=n[++d]}o=a;0!==l&&(e[v++]=l);while(ur===f>-r){a=o+r;i=a-o;l=o-(a-i)+(r-i);r=s[++u]}else{a=o+f;i=a-o;l=o-(a-i)+(f-i);f=n[++d]}o=a;0!==l&&(e[v++]=l)}}while(u=K||-J>=K)return J;$=c-E;_=c-(E+$)+($-v);$=u-G;M=u-(G+$)+($-v);$=n-H;b=n-(H+$)+($-h);$=d-I;p=d-(I+$)+($-h);if(0===_&&0===b&&0===M&&0===p)return J;K=o*m+t*Math.abs(J);J+=E*p+I*_-(H*M+G*b);if(J>=K||-J>=K)return J;q=_*I;x=s*_;g=x-(x-_);w=_-g;x=s*I;y=x-(x-I);A=I-y;z=w*A-(q-g*y-w*y-g*A);B=b*G;x=s*b;g=x-(x-b);w=b-g;x=s*G;y=x-(x-G);A=G-y;C=w*A-(B-g*y-w*y-g*A);F=z-C;$=z-F;f[0]=z-(F+$)+($-C);j=q+F;$=j-q;k=q-(j-$)+(F-$);F=k-B;$=k-F;f[1]=k-(F+$)+($-B);D=j+F;$=D-j;f[2]=j-(D-$)+(F-$);f[3]=D;const L=sum(4,a,4,f,l);q=E*p;x=s*E;g=x-(x-E);w=E-g;x=s*p;y=x-(x-p);A=p-y;z=w*A-(q-g*y-w*y-g*A);B=H*M;x=s*H;g=x-(x-H);w=H-g;x=s*M;y=x-(x-M);A=M-y;C=w*A-(B-g*y-w*y-g*A);F=z-C;$=z-F;f[0]=z-(F+$)+($-C);j=q+F;$=j-q;k=q-(j-$)+(F-$);F=k-B;$=k-F;f[1]=k-(F+$)+($-B);D=j+F;$=D-j;f[2]=j-(D-$)+(F-$);f[3]=D;const N=sum(L,l,4,f,i);q=_*p;x=s*_;g=x-(x-_);w=_-g;x=s*p;y=x-(x-p);A=p-y;z=w*A-(q-g*y-w*y-g*A);B=b*M;x=s*b;g=x-(x-b);w=b-g;x=s*M;y=x-(x-M);A=M-y;C=w*A-(B-g*y-w*y-g*A);F=z-C;$=z-F;f[0]=z-(F+$)+($-C);j=q+F;$=j-q;k=q-(j-$)+(F-$);F=k-B;$=k-F;f[1]=k-(F+$)+($-B);D=j+F;$=D-j;f[2]=j-(D-$)+(F-$);f[3]=D;const O=sum(N,i,4,f,r);return r[O-1]}function orient2d(c,s,t,e,o,a){const l=(s-a)*(t-o);const i=(c-o)*(e-a);const r=l-i;const f=Math.abs(l+i);return Math.abs(r)>=n*f?r:-orient2dadapt(c,s,t,e,o,a,f)}function orient2dfast(c,s,t,n,e,o){return(s-o)*(t-e)-(c-e)*(n-o)}const u=(7+56*c)*c;const d=(3+28*c)*c;const v=(26+288*c)*c*c;const h=vec(4);const m=vec(4);const _=vec(4);const b=vec(4);const M=vec(4);const p=vec(4);const $=vec(4);const x=vec(4);const g=vec(4);const w=vec(8);const y=vec(8);const A=vec(8);const F=vec(4);const j=vec(8);const k=vec(8);const q=vec(8);const z=vec(12);let B=vec(192);let C=vec(192);function finadd$1(c,s,t){c=sum(c,B,s,t,C);const n=B;B=C;C=n;return c}function tailinit(c,t,n,e,o,a,l,i){let r,f,u,d,v,h,m,_,b,M,p,$,x,g,w;if(0===c){if(0===t){l[0]=0;i[0]=0;return 1}w=-t;M=w*n;f=s*w;u=f-(f-w);d=w-u;f=s*n;v=f-(f-n);h=n-v;l[0]=d*h-(M-u*v-d*v-u*h);l[1]=M;M=t*o;f=s*t;u=f-(f-t);d=t-u;f=s*o;v=f-(f-o);h=o-v;i[0]=d*h-(M-u*v-d*v-u*h);i[1]=M;return 2}if(0===t){M=c*e;f=s*c;u=f-(f-c);d=c-u;f=s*e;v=f-(f-e);h=e-v;l[0]=d*h-(M-u*v-d*v-u*h);l[1]=M;w=-c;M=w*a;f=s*w;u=f-(f-w);d=w-u;f=s*a;v=f-(f-a);h=a-v;i[0]=d*h-(M-u*v-d*v-u*h);i[1]=M;return 2}M=c*e;f=s*c;u=f-(f-c);d=c-u;f=s*e;v=f-(f-e);h=e-v;p=d*h-(M-u*v-d*v-u*h);$=t*n;f=s*t;u=f-(f-t);d=t-u;f=s*n;v=f-(f-n);h=n-v;x=d*h-($-u*v-d*v-u*h);m=p-x;r=p-m;l[0]=p-(m+r)+(r-x);_=M+m;r=_-M;b=M-(_-r)+(m-r);m=b-$;r=b-m;l[1]=b-(m+r)+(r-$);g=_+m;r=g-_;l[2]=_-(g-r)+(m-r);l[3]=g;M=t*o;f=s*t;u=f-(f-t);d=t-u;f=s*o;v=f-(f-o);h=o-v;p=d*h-(M-u*v-d*v-u*h);$=c*a;f=s*c;u=f-(f-c);d=c-u;f=s*a;v=f-(f-a);h=a-v;x=d*h-($-u*v-d*v-u*h);m=p-x;r=p-m;i[0]=p-(m+r)+(r-x);_=M+m;r=_-M;b=M-(_-r)+(m-r);m=b-$;r=b-m;i[1]=b-(m+r)+(r-$);g=_+m;r=g-_;i[2]=_-(g-r)+(m-r);i[3]=g;return 4}function tailadd(c,t,n,e,o){let a,l,i,r,f,u,d,v,h,m,_,b,M;_=t*n;l=s*t;i=l-(l-t);r=t-i;l=s*n;f=l-(l-n);u=n-f;b=r*u-(_-i*f-r*f-i*u);l=s*e;f=l-(l-e);u=e-f;d=b*e;l=s*b;i=l-(l-b);r=b-i;F[0]=r*u-(d-i*f-r*f-i*u);v=_*e;l=s*_;i=l-(l-_);r=_-i;m=r*u-(v-i*f-r*f-i*u);h=d+m;a=h-d;F[1]=d-(h-a)+(m-a);M=v+h;F[2]=h-(M-v);F[3]=M;c=finadd$1(c,4,F);if(0!==o){l=s*o;f=l-(l-o);u=o-f;d=b*o;l=s*b;i=l-(l-b);r=b-i;F[0]=r*u-(d-i*f-r*f-i*u);v=_*o;l=s*_;i=l-(l-_);r=_-i;m=r*u-(v-i*f-r*f-i*u);h=d+m;a=h-d;F[1]=d-(h-a)+(m-a);M=v+h;F[2]=h-(M-v);F[3]=M;c=finadd$1(c,4,F)}return c}function orient3dadapt(c,n,e,o,a,l,i,r,f,u,F,C,D){let E;let G,H,I;let J,K,L;let N,O,P;let Q,R,S,T,U,V,W,X,Y,Z,cc,sc,tc,nc;const ec=c-u;const oc=o-u;const ac=i-u;const lc=n-F;const ic=a-F;const rc=r-F;const fc=e-C;const uc=l-C;const dc=f-C;Z=oc*rc;R=s*oc;S=R-(R-oc);T=oc-S;R=s*rc;U=R-(R-rc);V=rc-U;cc=T*V-(Z-S*U-T*U-S*V);sc=ac*ic;R=s*ac;S=R-(R-ac);T=ac-S;R=s*ic;U=R-(R-ic);V=ic-U;tc=T*V-(sc-S*U-T*U-S*V);W=cc-tc;Q=cc-W;h[0]=cc-(W+Q)+(Q-tc);X=Z+W;Q=X-Z;Y=Z-(X-Q)+(W-Q);W=Y-sc;Q=Y-W;h[1]=Y-(W+Q)+(Q-sc);nc=X+W;Q=nc-X;h[2]=X-(nc-Q)+(W-Q);h[3]=nc;Z=ac*lc;R=s*ac;S=R-(R-ac);T=ac-S;R=s*lc;U=R-(R-lc);V=lc-U;cc=T*V-(Z-S*U-T*U-S*V);sc=ec*rc;R=s*ec;S=R-(R-ec);T=ec-S;R=s*rc;U=R-(R-rc);V=rc-U;tc=T*V-(sc-S*U-T*U-S*V);W=cc-tc;Q=cc-W;m[0]=cc-(W+Q)+(Q-tc);X=Z+W;Q=X-Z;Y=Z-(X-Q)+(W-Q);W=Y-sc;Q=Y-W;m[1]=Y-(W+Q)+(Q-sc);nc=X+W;Q=nc-X;m[2]=X-(nc-Q)+(W-Q);m[3]=nc;Z=ec*ic;R=s*ec;S=R-(R-ec);T=ec-S;R=s*ic;U=R-(R-ic);V=ic-U;cc=T*V-(Z-S*U-T*U-S*V);sc=oc*lc;R=s*oc;S=R-(R-oc);T=oc-S;R=s*lc;U=R-(R-lc);V=lc-U;tc=T*V-(sc-S*U-T*U-S*V);W=cc-tc;Q=cc-W;_[0]=cc-(W+Q)+(Q-tc);X=Z+W;Q=X-Z;Y=Z-(X-Q)+(W-Q);W=Y-sc;Q=Y-W;_[1]=Y-(W+Q)+(Q-sc);nc=X+W;Q=nc-X;_[2]=X-(nc-Q)+(W-Q);_[3]=nc;E=sum(sum(scale(4,h,fc,j),j,scale(4,m,uc,k),k,q),q,scale(4,_,dc,j),j,B);let vc=estimate(E,B);let hc=d*D;if(vc>=hc||-vc>=hc)return vc;Q=c-ec;G=c-(ec+Q)+(Q-u);Q=o-oc;H=o-(oc+Q)+(Q-u);Q=i-ac;I=i-(ac+Q)+(Q-u);Q=n-lc;J=n-(lc+Q)+(Q-F);Q=a-ic;K=a-(ic+Q)+(Q-F);Q=r-rc;L=r-(rc+Q)+(Q-F);Q=e-fc;N=e-(fc+Q)+(Q-C);Q=l-uc;O=l-(uc+Q)+(Q-C);Q=f-dc;P=f-(dc+Q)+(Q-C);if(0===G&&0===H&&0===I&&0===J&&0===K&&0===L&&0===N&&0===O&&0===P)return vc;hc=v*D+t*Math.abs(vc);vc+=fc*(oc*L+rc*H-(ic*I+ac*K))+N*(oc*rc-ic*ac)+uc*(ac*J+lc*I-(rc*G+ec*L))+O*(ac*lc-rc*ec)+dc*(ec*K+ic*G-(lc*H+oc*J))+P*(ec*ic-lc*oc);if(vc>=hc||-vc>=hc)return vc;const mc=tailinit(G,J,oc,ic,ac,rc,b,M);const _c=tailinit(H,K,ac,rc,ec,lc,p,$);const bc=tailinit(I,L,ec,lc,oc,ic,x,g);const Mc=sum(_c,p,bc,g,w);E=finadd$1(E,scale(Mc,w,fc,q),q);const pc=sum(bc,x,mc,M,y);E=finadd$1(E,scale(pc,y,uc,q),q);const $c=sum(mc,b,_c,$,A);E=finadd$1(E,scale($c,A,dc,q),q);if(0!==N){E=finadd$1(E,scale(4,h,N,z),z);E=finadd$1(E,scale(Mc,w,N,q),q)}if(0!==O){E=finadd$1(E,scale(4,m,O,z),z);E=finadd$1(E,scale(pc,y,O,q),q)}if(0!==P){E=finadd$1(E,scale(4,_,P,z),z);E=finadd$1(E,scale($c,A,P,q),q)}if(0!==G){0!==K&&(E=tailadd(E,G,K,dc,P));0!==L&&(E=tailadd(E,-G,L,uc,O))}if(0!==H){0!==L&&(E=tailadd(E,H,L,fc,N));0!==J&&(E=tailadd(E,-H,J,dc,P))}if(0!==I){0!==J&&(E=tailadd(E,I,J,uc,O));0!==K&&(E=tailadd(E,-I,K,fc,N))}return B[E-1]}function orient3d(c,s,t,n,e,o,a,l,i,r,f,d){const v=c-r;const h=n-r;const m=a-r;const _=s-f;const b=e-f;const M=l-f;const p=t-d;const $=o-d;const x=i-d;const g=h*M;const w=m*b;const y=m*_;const A=v*M;const F=v*b;const j=h*_;const k=p*(g-w)+$*(y-A)+x*(F-j);const q=(Math.abs(g)+Math.abs(w))*Math.abs(p)+(Math.abs(y)+Math.abs(A))*Math.abs($)+(Math.abs(F)+Math.abs(j))*Math.abs(x);const z=u*q;return k>z||-k>z?k:orient3dadapt(c,s,t,n,e,o,a,l,i,r,f,d,q)}function orient3dfast(c,s,t,n,e,o,a,l,i,r,f,u){const d=c-r;const v=n-r;const h=a-r;const m=s-f;const _=e-f;const b=l-f;const M=t-u;const p=o-u;const $=i-u;return d*(_*$-p*b)+v*(b*M-$*m)+h*(m*p-M*_)}const D=(10+96*c)*c;const E=(4+48*c)*c;const G=(44+576*c)*c*c;const H=vec(4);const I=vec(4);const J=vec(4);const K=vec(4);const L=vec(4);const N=vec(4);const O=vec(4);const P=vec(4);const Q=vec(8);const R=vec(8);const S=vec(8);const T=vec(8);const U=vec(8);const V=vec(8);const W=vec(8);const X=vec(8);const Y=vec(8);const Z=vec(4);const cc=vec(4);const sc=vec(4);const tc=vec(8);const nc=vec(16);const ec=vec(16);const oc=vec(16);const ac=vec(32);const lc=vec(32);const ic=vec(48);const rc=vec(64);let fc=vec(1152);let uc=vec(1152);function finadd(c,s,t){c=sum(c,fc,s,t,uc);const n=fc;fc=uc;uc=n;return c}function incircleadapt(c,n,e,o,a,l,i,r,f){let u;let d,v,h,m,_,b;let M,p,$,x,g,w;let y,A,F;let j,k,q;let z,B;let C,D,uc,dc,vc,hc,mc,_c,bc,Mc,pc,$c,xc,gc;const wc=c-i;const yc=e-i;const Ac=a-i;const Fc=n-r;const jc=o-r;const kc=l-r;Mc=yc*kc;D=s*yc;uc=D-(D-yc);dc=yc-uc;D=s*kc;vc=D-(D-kc);hc=kc-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=Ac*jc;D=s*Ac;uc=D-(D-Ac);dc=Ac-uc;D=s*jc;vc=D-(D-jc);hc=jc-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc-xc;C=pc-mc;H[0]=pc-(mc+C)+(C-xc);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc-$c;C=bc-mc;H[1]=bc-(mc+C)+(C-$c);gc=_c+mc;C=gc-_c;H[2]=_c-(gc-C)+(mc-C);H[3]=gc;Mc=Ac*Fc;D=s*Ac;uc=D-(D-Ac);dc=Ac-uc;D=s*Fc;vc=D-(D-Fc);hc=Fc-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=wc*kc;D=s*wc;uc=D-(D-wc);dc=wc-uc;D=s*kc;vc=D-(D-kc);hc=kc-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc-xc;C=pc-mc;I[0]=pc-(mc+C)+(C-xc);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc-$c;C=bc-mc;I[1]=bc-(mc+C)+(C-$c);gc=_c+mc;C=gc-_c;I[2]=_c-(gc-C)+(mc-C);I[3]=gc;Mc=wc*jc;D=s*wc;uc=D-(D-wc);dc=wc-uc;D=s*jc;vc=D-(D-jc);hc=jc-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=yc*Fc;D=s*yc;uc=D-(D-yc);dc=yc-uc;D=s*Fc;vc=D-(D-Fc);hc=Fc-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc-xc;C=pc-mc;J[0]=pc-(mc+C)+(C-xc);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc-$c;C=bc-mc;J[1]=bc-(mc+C)+(C-$c);gc=_c+mc;C=gc-_c;J[2]=_c-(gc-C)+(mc-C);J[3]=gc;u=sum(sum(sum(scale(scale(4,H,wc,tc),tc,wc,nc),nc,scale(scale(4,H,Fc,tc),tc,Fc,ec),ec,ac),ac,sum(scale(scale(4,I,yc,tc),tc,yc,nc),nc,scale(scale(4,I,jc,tc),tc,jc,ec),ec,lc),lc,rc),rc,sum(scale(scale(4,J,Ac,tc),tc,Ac,nc),nc,scale(scale(4,J,kc,tc),tc,kc,ec),ec,ac),ac,fc);let qc=estimate(u,fc);let zc=E*f;if(qc>=zc||-qc>=zc)return qc;C=c-wc;d=c-(wc+C)+(C-i);C=n-Fc;m=n-(Fc+C)+(C-r);C=e-yc;v=e-(yc+C)+(C-i);C=o-jc;_=o-(jc+C)+(C-r);C=a-Ac;h=a-(Ac+C)+(C-i);C=l-kc;b=l-(kc+C)+(C-r);if(0===d&&0===v&&0===h&&0===m&&0===_&&0===b)return qc;zc=G*f+t*Math.abs(qc);qc+=(wc*wc+Fc*Fc)*(yc*b+kc*v-(jc*h+Ac*_))+2*(wc*d+Fc*m)*(yc*kc-jc*Ac)+((yc*yc+jc*jc)*(Ac*m+Fc*h-(kc*d+wc*b))+2*(yc*v+jc*_)*(Ac*Fc-kc*wc))+((Ac*Ac+kc*kc)*(wc*_+jc*d-(Fc*v+yc*m))+2*(Ac*h+kc*b)*(wc*jc-Fc*yc));if(qc>=zc||-qc>=zc)return qc;if(0!==v||0!==_||0!==h||0!==b){Mc=wc*wc;D=s*wc;uc=D-(D-wc);dc=wc-uc;pc=dc*dc-(Mc-uc*uc-(uc+uc)*dc);$c=Fc*Fc;D=s*Fc;uc=D-(D-Fc);dc=Fc-uc;xc=dc*dc-($c-uc*uc-(uc+uc)*dc);mc=pc+xc;C=mc-pc;K[0]=pc-(mc-C)+(xc-C);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc+$c;C=mc-bc;K[1]=bc-(mc-C)+($c-C);gc=_c+mc;C=gc-_c;K[2]=_c-(gc-C)+(mc-C);K[3]=gc}if(0!==h||0!==b||0!==d||0!==m){Mc=yc*yc;D=s*yc;uc=D-(D-yc);dc=yc-uc;pc=dc*dc-(Mc-uc*uc-(uc+uc)*dc);$c=jc*jc;D=s*jc;uc=D-(D-jc);dc=jc-uc;xc=dc*dc-($c-uc*uc-(uc+uc)*dc);mc=pc+xc;C=mc-pc;L[0]=pc-(mc-C)+(xc-C);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc+$c;C=mc-bc;L[1]=bc-(mc-C)+($c-C);gc=_c+mc;C=gc-_c;L[2]=_c-(gc-C)+(mc-C);L[3]=gc}if(0!==d||0!==m||0!==v||0!==_){Mc=Ac*Ac;D=s*Ac;uc=D-(D-Ac);dc=Ac-uc;pc=dc*dc-(Mc-uc*uc-(uc+uc)*dc);$c=kc*kc;D=s*kc;uc=D-(D-kc);dc=kc-uc;xc=dc*dc-($c-uc*uc-(uc+uc)*dc);mc=pc+xc;C=mc-pc;N[0]=pc-(mc-C)+(xc-C);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc+$c;C=mc-bc;N[1]=bc-(mc-C)+($c-C);gc=_c+mc;C=gc-_c;N[2]=_c-(gc-C)+(mc-C);N[3]=gc}if(0!==d){M=scale(4,H,d,Q);u=finadd(u,sum_three(scale(M,Q,2*wc,nc),nc,scale(scale(4,N,d,tc),tc,jc,ec),ec,scale(scale(4,L,d,tc),tc,-kc,oc),oc,ac,ic),ic)}if(0!==m){p=scale(4,H,m,R);u=finadd(u,sum_three(scale(p,R,2*Fc,nc),nc,scale(scale(4,L,m,tc),tc,Ac,ec),ec,scale(scale(4,N,m,tc),tc,-yc,oc),oc,ac,ic),ic)}if(0!==v){$=scale(4,I,v,S);u=finadd(u,sum_three(scale($,S,2*yc,nc),nc,scale(scale(4,K,v,tc),tc,kc,ec),ec,scale(scale(4,N,v,tc),tc,-Fc,oc),oc,ac,ic),ic)}if(0!==_){x=scale(4,I,_,T);u=finadd(u,sum_three(scale(x,T,2*jc,nc),nc,scale(scale(4,N,_,tc),tc,wc,ec),ec,scale(scale(4,K,_,tc),tc,-Ac,oc),oc,ac,ic),ic)}if(0!==h){g=scale(4,J,h,U);u=finadd(u,sum_three(scale(g,U,2*Ac,nc),nc,scale(scale(4,L,h,tc),tc,Fc,ec),ec,scale(scale(4,K,h,tc),tc,-jc,oc),oc,ac,ic),ic)}if(0!==b){w=scale(4,J,b,V);u=finadd(u,sum_three(scale(w,V,2*kc,nc),nc,scale(scale(4,K,b,tc),tc,yc,ec),ec,scale(scale(4,L,b,tc),tc,-wc,oc),oc,ac,ic),ic)}if(0!==d||0!==m){if(0!==v||0!==_||0!==h||0!==b){Mc=v*kc;D=s*v;uc=D-(D-v);dc=v-uc;D=s*kc;vc=D-(D-kc);hc=kc-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=yc*b;D=s*yc;uc=D-(D-yc);dc=yc-uc;D=s*b;vc=D-(D-b);hc=b-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc+xc;C=mc-pc;O[0]=pc-(mc-C)+(xc-C);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc+$c;C=mc-bc;O[1]=bc-(mc-C)+($c-C);gc=_c+mc;C=gc-_c;O[2]=_c-(gc-C)+(mc-C);O[3]=gc;Mc=h*-jc;D=s*h;uc=D-(D-h);dc=h-uc;D=s*-jc;vc=D-(D- -jc);hc=-jc-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=Ac*-_;D=s*Ac;uc=D-(D-Ac);dc=Ac-uc;D=s*-_;vc=D-(D- -_);hc=-_-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc+xc;C=mc-pc;P[0]=pc-(mc-C)+(xc-C);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc+$c;C=mc-bc;P[1]=bc-(mc-C)+($c-C);gc=_c+mc;C=gc-_c;P[2]=_c-(gc-C)+(mc-C);P[3]=gc;A=sum(4,O,4,P,X);Mc=v*b;D=s*v;uc=D-(D-v);dc=v-uc;D=s*b;vc=D-(D-b);hc=b-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=h*_;D=s*h;uc=D-(D-h);dc=h-uc;D=s*_;vc=D-(D-_);hc=_-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc-xc;C=pc-mc;cc[0]=pc-(mc+C)+(C-xc);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc-$c;C=bc-mc;cc[1]=bc-(mc+C)+(C-$c);gc=_c+mc;C=gc-_c;cc[2]=_c-(gc-C)+(mc-C);cc[3]=gc;k=4}else{X[0]=0;A=1;cc[0]=0;k=1}if(0!==d){const c=scale(A,X,d,oc);u=finadd(u,sum(scale(M,Q,d,nc),nc,scale(c,oc,2*wc,ac),ac,ic),ic);const s=scale(k,cc,d,tc);u=finadd(u,sum_three(scale(s,tc,2*wc,nc),nc,scale(s,tc,d,ec),ec,scale(c,oc,d,ac),ac,lc,rc),rc);0!==_&&(u=finadd(u,scale(scale(4,N,d,tc),tc,_,nc),nc));0!==b&&(u=finadd(u,scale(scale(4,L,-d,tc),tc,b,nc),nc))}if(0!==m){const c=scale(A,X,m,oc);u=finadd(u,sum(scale(p,R,m,nc),nc,scale(c,oc,2*Fc,ac),ac,ic),ic);const s=scale(k,cc,m,tc);u=finadd(u,sum_three(scale(s,tc,2*Fc,nc),nc,scale(s,tc,m,ec),ec,scale(c,oc,m,ac),ac,lc,rc),rc)}}if(0!==v||0!==_){if(0!==h||0!==b||0!==d||0!==m){Mc=h*Fc;D=s*h;uc=D-(D-h);dc=h-uc;D=s*Fc;vc=D-(D-Fc);hc=Fc-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=Ac*m;D=s*Ac;uc=D-(D-Ac);dc=Ac-uc;D=s*m;vc=D-(D-m);hc=m-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc+xc;C=mc-pc;O[0]=pc-(mc-C)+(xc-C);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc+$c;C=mc-bc;O[1]=bc-(mc-C)+($c-C);gc=_c+mc;C=gc-_c;O[2]=_c-(gc-C)+(mc-C);O[3]=gc;z=-kc;B=-b;Mc=d*z;D=s*d;uc=D-(D-d);dc=d-uc;D=s*z;vc=D-(D-z);hc=z-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=wc*B;D=s*wc;uc=D-(D-wc);dc=wc-uc;D=s*B;vc=D-(D-B);hc=B-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc+xc;C=mc-pc;P[0]=pc-(mc-C)+(xc-C);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc+$c;C=mc-bc;P[1]=bc-(mc-C)+($c-C);gc=_c+mc;C=gc-_c;P[2]=_c-(gc-C)+(mc-C);P[3]=gc;F=sum(4,O,4,P,Y);Mc=h*m;D=s*h;uc=D-(D-h);dc=h-uc;D=s*m;vc=D-(D-m);hc=m-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=d*b;D=s*d;uc=D-(D-d);dc=d-uc;D=s*b;vc=D-(D-b);hc=b-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc-xc;C=pc-mc;sc[0]=pc-(mc+C)+(C-xc);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc-$c;C=bc-mc;sc[1]=bc-(mc+C)+(C-$c);gc=_c+mc;C=gc-_c;sc[2]=_c-(gc-C)+(mc-C);sc[3]=gc;q=4}else{Y[0]=0;F=1;sc[0]=0;q=1}if(0!==v){const c=scale(F,Y,v,oc);u=finadd(u,sum(scale($,S,v,nc),nc,scale(c,oc,2*yc,ac),ac,ic),ic);const s=scale(q,sc,v,tc);u=finadd(u,sum_three(scale(s,tc,2*yc,nc),nc,scale(s,tc,v,ec),ec,scale(c,oc,v,ac),ac,lc,rc),rc);0!==b&&(u=finadd(u,scale(scale(4,K,v,tc),tc,b,nc),nc));0!==m&&(u=finadd(u,scale(scale(4,N,-v,tc),tc,m,nc),nc))}if(0!==_){const c=scale(F,Y,_,oc);u=finadd(u,sum(scale(x,T,_,nc),nc,scale(c,oc,2*jc,ac),ac,ic),ic);const s=scale(q,sc,_,tc);u=finadd(u,sum_three(scale(s,tc,2*jc,nc),nc,scale(s,tc,_,ec),ec,scale(c,oc,_,ac),ac,lc,rc),rc)}}if(0!==h||0!==b){if(0!==d||0!==m||0!==v||0!==_){Mc=d*jc;D=s*d;uc=D-(D-d);dc=d-uc;D=s*jc;vc=D-(D-jc);hc=jc-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=wc*_;D=s*wc;uc=D-(D-wc);dc=wc-uc;D=s*_;vc=D-(D-_);hc=_-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc+xc;C=mc-pc;O[0]=pc-(mc-C)+(xc-C);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc+$c;C=mc-bc;O[1]=bc-(mc-C)+($c-C);gc=_c+mc;C=gc-_c;O[2]=_c-(gc-C)+(mc-C);O[3]=gc;z=-Fc;B=-m;Mc=v*z;D=s*v;uc=D-(D-v);dc=v-uc;D=s*z;vc=D-(D-z);hc=z-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=yc*B;D=s*yc;uc=D-(D-yc);dc=yc-uc;D=s*B;vc=D-(D-B);hc=B-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc+xc;C=mc-pc;P[0]=pc-(mc-C)+(xc-C);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc+$c;C=mc-bc;P[1]=bc-(mc-C)+($c-C);gc=_c+mc;C=gc-_c;P[2]=_c-(gc-C)+(mc-C);P[3]=gc;y=sum(4,O,4,P,W);Mc=d*_;D=s*d;uc=D-(D-d);dc=d-uc;D=s*_;vc=D-(D-_);hc=_-vc;pc=dc*hc-(Mc-uc*vc-dc*vc-uc*hc);$c=v*m;D=s*v;uc=D-(D-v);dc=v-uc;D=s*m;vc=D-(D-m);hc=m-vc;xc=dc*hc-($c-uc*vc-dc*vc-uc*hc);mc=pc-xc;C=pc-mc;Z[0]=pc-(mc+C)+(C-xc);_c=Mc+mc;C=_c-Mc;bc=Mc-(_c-C)+(mc-C);mc=bc-$c;C=bc-mc;Z[1]=bc-(mc+C)+(C-$c);gc=_c+mc;C=gc-_c;Z[2]=_c-(gc-C)+(mc-C);Z[3]=gc;j=4}else{W[0]=0;y=1;Z[0]=0;j=1}if(0!==h){const c=scale(y,W,h,oc);u=finadd(u,sum(scale(g,U,h,nc),nc,scale(c,oc,2*Ac,ac),ac,ic),ic);const s=scale(j,Z,h,tc);u=finadd(u,sum_three(scale(s,tc,2*Ac,nc),nc,scale(s,tc,h,ec),ec,scale(c,oc,h,ac),ac,lc,rc),rc);0!==m&&(u=finadd(u,scale(scale(4,L,h,tc),tc,m,nc),nc));0!==_&&(u=finadd(u,scale(scale(4,K,-h,tc),tc,_,nc),nc))}if(0!==b){const c=scale(y,W,b,oc);u=finadd(u,sum(scale(w,V,b,nc),nc,scale(c,oc,2*kc,ac),ac,ic),ic);const s=scale(j,Z,b,tc);u=finadd(u,sum_three(scale(s,tc,2*kc,nc),nc,scale(s,tc,b,ec),ec,scale(c,oc,b,ac),ac,lc,rc),rc)}}return fc[u-1]}function incircle(c,s,t,n,e,o,a,l){const i=c-a;const r=t-a;const f=e-a;const u=s-l;const d=n-l;const v=o-l;const h=r*v;const m=f*d;const _=i*i+u*u;const b=f*u;const M=i*v;const p=r*r+d*d;const $=i*d;const x=r*u;const g=f*f+v*v;const w=_*(h-m)+p*(b-M)+g*($-x);const y=(Math.abs(h)+Math.abs(m))*_+(Math.abs(b)+Math.abs(M))*p+(Math.abs($)+Math.abs(x))*g;const A=D*y;return w>A||-w>A?w:incircleadapt(c,s,t,n,e,o,a,l,y)}function incirclefast(c,s,t,n,e,o,a,l){const i=c-a;const r=s-l;const f=t-a;const u=n-l;const d=e-a;const v=o-l;const h=i*u-f*r;const m=f*v-d*u;const _=d*r-i*v;const b=i*i+r*r;const M=f*f+u*u;const p=d*d+v*v;return b*m+M*_+p*h}const dc=(16+224*c)*c;const vc=(5+72*c)*c;const hc=(71+1408*c)*c*c;const mc=vec(4);const _c=vec(4);const bc=vec(4);const Mc=vec(4);const pc=vec(4);const $c=vec(4);const xc=vec(4);const gc=vec(4);const wc=vec(4);const yc=vec(4);const Ac=vec(24);const Fc=vec(24);const jc=vec(24);const kc=vec(24);const qc=vec(24);const zc=vec(24);const Bc=vec(24);const Cc=vec(24);const Dc=vec(24);const Ec=vec(24);const Gc=vec(1152);const Hc=vec(1152);const Ic=vec(1152);const Jc=vec(1152);const Kc=vec(1152);const Lc=vec(2304);const Nc=vec(2304);const Oc=vec(3456);const Pc=vec(5760);const Qc=vec(8);const Rc=vec(8);const Sc=vec(8);const Tc=vec(16);const Uc=vec(24);const Vc=vec(48);const Wc=vec(48);const Xc=vec(96);const Yc=vec(192);const Zc=vec(384);const cs=vec(384);const ss=vec(384);const ts=vec(768);function sum_three_scale(c,s,t,n,e,o,a){return sum_three(scale(4,c,n,Qc),Qc,scale(4,s,e,Rc),Rc,scale(4,t,o,Sc),Sc,Tc,a)}function liftexact(c,s,t,n,e,o,a,l,i,r,f,u){const d=sum(sum(c,s,t,n,Vc),Vc,negate(sum(e,o,a,l,Wc),Wc),Wc,Xc);return sum_three(scale(scale(d,Xc,i,Yc),Yc,i,Zc),Zc,scale(scale(d,Xc,r,Yc),Yc,r,cs),cs,scale(scale(d,Xc,f,Yc),Yc,f,ss),ss,ts,u)}function insphereexact(c,t,n,e,o,a,l,i,r,f,u,d,v,h,m){let _,b,M,p,$,x,g,w,y,A,F,j,k,q;A=c*o;b=s*c;M=b-(b-c);p=c-M;b=s*o;$=b-(b-o);x=o-$;F=p*x-(A-M*$-p*$-M*x);j=e*t;b=s*e;M=b-(b-e);p=e-M;b=s*t;$=b-(b-t);x=t-$;k=p*x-(j-M*$-p*$-M*x);g=F-k;_=F-g;mc[0]=F-(g+_)+(_-k);w=A+g;_=w-A;y=A-(w-_)+(g-_);g=y-j;_=y-g;mc[1]=y-(g+_)+(_-j);q=w+g;_=q-w;mc[2]=w-(q-_)+(g-_);mc[3]=q;A=e*i;b=s*e;M=b-(b-e);p=e-M;b=s*i;$=b-(b-i);x=i-$;F=p*x-(A-M*$-p*$-M*x);j=l*o;b=s*l;M=b-(b-l);p=l-M;b=s*o;$=b-(b-o);x=o-$;k=p*x-(j-M*$-p*$-M*x);g=F-k;_=F-g;_c[0]=F-(g+_)+(_-k);w=A+g;_=w-A;y=A-(w-_)+(g-_);g=y-j;_=y-g;_c[1]=y-(g+_)+(_-j);q=w+g;_=q-w;_c[2]=w-(q-_)+(g-_);_c[3]=q;A=l*u;b=s*l;M=b-(b-l);p=l-M;b=s*u;$=b-(b-u);x=u-$;F=p*x-(A-M*$-p*$-M*x);j=f*i;b=s*f;M=b-(b-f);p=f-M;b=s*i;$=b-(b-i);x=i-$;k=p*x-(j-M*$-p*$-M*x);g=F-k;_=F-g;bc[0]=F-(g+_)+(_-k);w=A+g;_=w-A;y=A-(w-_)+(g-_);g=y-j;_=y-g;bc[1]=y-(g+_)+(_-j);q=w+g;_=q-w;bc[2]=w-(q-_)+(g-_);bc[3]=q;A=f*h;b=s*f;M=b-(b-f);p=f-M;b=s*h;$=b-(b-h);x=h-$;F=p*x-(A-M*$-p*$-M*x);j=v*u;b=s*v;M=b-(b-v);p=v-M;b=s*u;$=b-(b-u);x=u-$;k=p*x-(j-M*$-p*$-M*x);g=F-k;_=F-g;Mc[0]=F-(g+_)+(_-k);w=A+g;_=w-A;y=A-(w-_)+(g-_);g=y-j;_=y-g;Mc[1]=y-(g+_)+(_-j);q=w+g;_=q-w;Mc[2]=w-(q-_)+(g-_);Mc[3]=q;A=v*t;b=s*v;M=b-(b-v);p=v-M;b=s*t;$=b-(b-t);x=t-$;F=p*x-(A-M*$-p*$-M*x);j=c*h;b=s*c;M=b-(b-c);p=c-M;b=s*h;$=b-(b-h);x=h-$;k=p*x-(j-M*$-p*$-M*x);g=F-k;_=F-g;pc[0]=F-(g+_)+(_-k);w=A+g;_=w-A;y=A-(w-_)+(g-_);g=y-j;_=y-g;pc[1]=y-(g+_)+(_-j);q=w+g;_=q-w;pc[2]=w-(q-_)+(g-_);pc[3]=q;A=c*i;b=s*c;M=b-(b-c);p=c-M;b=s*i;$=b-(b-i);x=i-$;F=p*x-(A-M*$-p*$-M*x);j=l*t;b=s*l;M=b-(b-l);p=l-M;b=s*t;$=b-(b-t);x=t-$;k=p*x-(j-M*$-p*$-M*x);g=F-k;_=F-g;$c[0]=F-(g+_)+(_-k);w=A+g;_=w-A;y=A-(w-_)+(g-_);g=y-j;_=y-g;$c[1]=y-(g+_)+(_-j);q=w+g;_=q-w;$c[2]=w-(q-_)+(g-_);$c[3]=q;A=e*u;b=s*e;M=b-(b-e);p=e-M;b=s*u;$=b-(b-u);x=u-$;F=p*x-(A-M*$-p*$-M*x);j=f*o;b=s*f;M=b-(b-f);p=f-M;b=s*o;$=b-(b-o);x=o-$;k=p*x-(j-M*$-p*$-M*x);g=F-k;_=F-g;xc[0]=F-(g+_)+(_-k);w=A+g;_=w-A;y=A-(w-_)+(g-_);g=y-j;_=y-g;xc[1]=y-(g+_)+(_-j);q=w+g;_=q-w;xc[2]=w-(q-_)+(g-_);xc[3]=q;A=l*h;b=s*l;M=b-(b-l);p=l-M;b=s*h;$=b-(b-h);x=h-$;F=p*x-(A-M*$-p*$-M*x);j=v*i;b=s*v;M=b-(b-v);p=v-M;b=s*i;$=b-(b-i);x=i-$;k=p*x-(j-M*$-p*$-M*x);g=F-k;_=F-g;gc[0]=F-(g+_)+(_-k);w=A+g;_=w-A;y=A-(w-_)+(g-_);g=y-j;_=y-g;gc[1]=y-(g+_)+(_-j);q=w+g;_=q-w;gc[2]=w-(q-_)+(g-_);gc[3]=q;A=f*t;b=s*f;M=b-(b-f);p=f-M;b=s*t;$=b-(b-t);x=t-$;F=p*x-(A-M*$-p*$-M*x);j=c*u;b=s*c;M=b-(b-c);p=c-M;b=s*u;$=b-(b-u);x=u-$;k=p*x-(j-M*$-p*$-M*x);g=F-k;_=F-g;wc[0]=F-(g+_)+(_-k);w=A+g;_=w-A;y=A-(w-_)+(g-_);g=y-j;_=y-g;wc[1]=y-(g+_)+(_-j);q=w+g;_=q-w;wc[2]=w-(q-_)+(g-_);wc[3]=q;A=v*o;b=s*v;M=b-(b-v);p=v-M;b=s*o;$=b-(b-o);x=o-$;F=p*x-(A-M*$-p*$-M*x);j=e*h;b=s*e;M=b-(b-e);p=e-M;b=s*h;$=b-(b-h);x=h-$;k=p*x-(j-M*$-p*$-M*x);g=F-k;_=F-g;yc[0]=F-(g+_)+(_-k);w=A+g;_=w-A;y=A-(w-_)+(g-_);g=y-j;_=y-g;yc[1]=y-(g+_)+(_-j);q=w+g;_=q-w;yc[2]=w-(q-_)+(g-_);yc[3]=q;const z=sum_three_scale(mc,_c,$c,r,n,-a,Ac);const B=sum_three_scale(_c,bc,xc,d,a,-r,Fc);const C=sum_three_scale(bc,Mc,gc,m,r,-d,jc);const D=sum_three_scale(Mc,pc,wc,n,d,-m,kc);const E=sum_three_scale(pc,mc,yc,a,m,-n,qc);const G=sum_three_scale(mc,xc,wc,d,n,a,zc);const H=sum_three_scale(_c,gc,yc,m,a,r,Bc);const I=sum_three_scale(bc,wc,$c,n,r,d,Cc);const J=sum_three_scale(Mc,yc,xc,a,d,m,Dc);const K=sum_three_scale(pc,$c,gc,r,m,n,Ec);const L=sum_three(liftexact(C,jc,H,Bc,J,Dc,B,Fc,c,t,n,Gc),Gc,liftexact(D,kc,I,Cc,K,Ec,C,jc,e,o,a,Hc),Hc,sum_three(liftexact(E,qc,J,Dc,G,zc,D,kc,l,i,r,Ic),Ic,liftexact(z,Ac,K,Ec,H,Bc,E,qc,f,u,d,Jc),Jc,liftexact(B,Fc,G,zc,I,Cc,z,Ac,v,h,m,Kc),Kc,Nc,Oc),Oc,Lc,Pc);return Pc[L-1]}const ns=vec(96);const es=vec(96);const os=vec(96);const as=vec(1152);function liftadapt(c,s,t,n,e,o,a,l,i,r){const f=sum_three_scale(c,s,t,n,e,o,Uc);return sum_three(scale(scale(f,Uc,a,Vc),Vc,a,ns),ns,scale(scale(f,Uc,l,Vc),Vc,l,es),es,scale(scale(f,Uc,i,Vc),Vc,i,os),os,Yc,r)}function insphereadapt(c,n,e,o,a,l,i,r,f,u,d,v,h,m,_,b){let M,p,$,x,g,w;let y,A,F,j;let k,q,z,B;let C,D,E,G;let H,I,J,K,L,N,O,P,Q,R,S,T,U;const V=c-h;const W=o-h;const X=i-h;const Y=u-h;const Z=n-m;const cc=a-m;const sc=r-m;const tc=d-m;const nc=e-_;const ec=l-_;const oc=f-_;const ac=v-_;R=V*cc;I=s*V;J=I-(I-V);K=V-J;I=s*cc;L=I-(I-cc);N=cc-L;S=K*N-(R-J*L-K*L-J*N);T=W*Z;I=s*W;J=I-(I-W);K=W-J;I=s*Z;L=I-(I-Z);N=Z-L;U=K*N-(T-J*L-K*L-J*N);O=S-U;H=S-O;mc[0]=S-(O+H)+(H-U);P=R+O;H=P-R;Q=R-(P-H)+(O-H);O=Q-T;H=Q-O;mc[1]=Q-(O+H)+(H-T);M=P+O;H=M-P;mc[2]=P-(M-H)+(O-H);mc[3]=M;R=W*sc;I=s*W;J=I-(I-W);K=W-J;I=s*sc;L=I-(I-sc);N=sc-L;S=K*N-(R-J*L-K*L-J*N);T=X*cc;I=s*X;J=I-(I-X);K=X-J;I=s*cc;L=I-(I-cc);N=cc-L;U=K*N-(T-J*L-K*L-J*N);O=S-U;H=S-O;_c[0]=S-(O+H)+(H-U);P=R+O;H=P-R;Q=R-(P-H)+(O-H);O=Q-T;H=Q-O;_c[1]=Q-(O+H)+(H-T);p=P+O;H=p-P;_c[2]=P-(p-H)+(O-H);_c[3]=p;R=X*tc;I=s*X;J=I-(I-X);K=X-J;I=s*tc;L=I-(I-tc);N=tc-L;S=K*N-(R-J*L-K*L-J*N);T=Y*sc;I=s*Y;J=I-(I-Y);K=Y-J;I=s*sc;L=I-(I-sc);N=sc-L;U=K*N-(T-J*L-K*L-J*N);O=S-U;H=S-O;bc[0]=S-(O+H)+(H-U);P=R+O;H=P-R;Q=R-(P-H)+(O-H);O=Q-T;H=Q-O;bc[1]=Q-(O+H)+(H-T);$=P+O;H=$-P;bc[2]=P-($-H)+(O-H);bc[3]=$;R=Y*Z;I=s*Y;J=I-(I-Y);K=Y-J;I=s*Z;L=I-(I-Z);N=Z-L;S=K*N-(R-J*L-K*L-J*N);T=V*tc;I=s*V;J=I-(I-V);K=V-J;I=s*tc;L=I-(I-tc);N=tc-L;U=K*N-(T-J*L-K*L-J*N);O=S-U;H=S-O;wc[0]=S-(O+H)+(H-U);P=R+O;H=P-R;Q=R-(P-H)+(O-H);O=Q-T;H=Q-O;wc[1]=Q-(O+H)+(H-T);x=P+O;H=x-P;wc[2]=P-(x-H)+(O-H);wc[3]=x;R=V*sc;I=s*V;J=I-(I-V);K=V-J;I=s*sc;L=I-(I-sc);N=sc-L;S=K*N-(R-J*L-K*L-J*N);T=X*Z;I=s*X;J=I-(I-X);K=X-J;I=s*Z;L=I-(I-Z);N=Z-L;U=K*N-(T-J*L-K*L-J*N);O=S-U;H=S-O;$c[0]=S-(O+H)+(H-U);P=R+O;H=P-R;Q=R-(P-H)+(O-H);O=Q-T;H=Q-O;$c[1]=Q-(O+H)+(H-T);g=P+O;H=g-P;$c[2]=P-(g-H)+(O-H);$c[3]=g;R=W*tc;I=s*W;J=I-(I-W);K=W-J;I=s*tc;L=I-(I-tc);N=tc-L;S=K*N-(R-J*L-K*L-J*N);T=Y*cc;I=s*Y;J=I-(I-Y);K=Y-J;I=s*cc;L=I-(I-cc);N=cc-L;U=K*N-(T-J*L-K*L-J*N);O=S-U;H=S-O;xc[0]=S-(O+H)+(H-U);P=R+O;H=P-R;Q=R-(P-H)+(O-H);O=Q-T;H=Q-O;xc[1]=Q-(O+H)+(H-T);w=P+O;H=w-P;xc[2]=P-(w-H)+(O-H);xc[3]=w;const lc=sum(sum(negate(liftadapt(_c,bc,xc,ac,ec,-oc,V,Z,nc,Gc),Gc),Gc,liftadapt(bc,wc,$c,nc,oc,ac,W,cc,ec,Hc),Hc,Lc),Lc,sum(negate(liftadapt(wc,mc,xc,ec,ac,nc,X,sc,oc,Ic),Ic),Ic,liftadapt(mc,_c,$c,oc,nc,-ec,Y,tc,ac,Jc),Jc,Nc),Nc,as);let ic=estimate(lc,as);let rc=vc*b;if(ic>=rc||-ic>=rc)return ic;H=c-V;y=c-(V+H)+(H-h);H=n-Z;k=n-(Z+H)+(H-m);H=e-nc;C=e-(nc+H)+(H-_);H=o-W;A=o-(W+H)+(H-h);H=a-cc;q=a-(cc+H)+(H-m);H=l-ec;D=l-(ec+H)+(H-_);H=i-X;F=i-(X+H)+(H-h);H=r-sc;z=r-(sc+H)+(H-m);H=f-oc;E=f-(oc+H)+(H-_);H=u-Y;j=u-(Y+H)+(H-h);H=d-tc;B=d-(tc+H)+(H-m);H=v-ac;G=v-(ac+H)+(H-_);if(0===y&&0===k&&0===C&&0===A&&0===q&&0===D&&0===F&&0===z&&0===E&&0===j&&0===B&&0===G)return ic;rc=hc*b+t*Math.abs(ic);const fc=V*q+cc*y-(Z*A+W*k);const uc=W*z+sc*A-(cc*F+X*q);const dc=X*B+tc*F-(sc*j+Y*z);const Mc=Y*k+Z*j-(tc*y+V*B);const pc=V*z+sc*y-(Z*F+X*k);const gc=W*B+tc*A-(cc*j+Y*q);ic+=(W*W+cc*cc+ec*ec)*(oc*Mc+ac*pc+nc*dc+(E*x+G*g+C*$))+(Y*Y+tc*tc+ac*ac)*(nc*uc-ec*pc+oc*fc+(C*p-D*g+E*M))-((V*V+Z*Z+nc*nc)*(ec*dc-oc*gc+ac*uc+(D*$-E*w+G*p))+(X*X+sc*sc+oc*oc)*(ac*fc+nc*gc+ec*Mc+(G*M+C*w+D*x)))+2*((W*A+cc*q+ec*D)*(oc*x+ac*g+nc*$)+(Y*j+tc*B+ac*G)*(nc*p-ec*g+oc*M)-((V*y+Z*k+nc*C)*(ec*$-oc*w+ac*p)+(X*F+sc*z+oc*E)*(ac*M+nc*w+ec*x)));return ic>=rc||-ic>=rc?ic:insphereexact(c,n,e,o,a,l,i,r,f,u,d,v,h,m,_)}function insphere(c,s,t,n,e,o,a,l,i,r,f,u,d,v,h){const m=c-d;const _=n-d;const b=a-d;const M=r-d;const p=s-v;const $=e-v;const x=l-v;const g=f-v;const w=t-h;const y=o-h;const A=i-h;const F=u-h;const j=m*$;const k=_*p;const q=j-k;const z=_*x;const B=b*$;const C=z-B;const D=b*g;const E=M*x;const G=D-E;const H=M*p;const I=m*g;const J=H-I;const K=m*x;const L=b*p;const N=K-L;const O=_*g;const P=M*$;const Q=O-P;const R=m*m+p*p+w*w;const S=_*_+$*$+y*y;const T=b*b+x*x+A*A;const U=M*M+g*g+F*F;const V=T*(F*q+w*Q+y*J)-U*(w*C-y*N+A*q)+(R*(y*G-A*Q+F*C)-S*(A*J+F*N+w*G));const W=Math.abs(w);const X=Math.abs(y);const Y=Math.abs(A);const Z=Math.abs(F);const cc=Math.abs(j)+Math.abs(k);const sc=Math.abs(z)+Math.abs(B);const tc=Math.abs(D)+Math.abs(E);const nc=Math.abs(H)+Math.abs(I);const ec=Math.abs(K)+Math.abs(L);const oc=Math.abs(O)+Math.abs(P);const ac=(tc*X+oc*Y+sc*Z)*R+(nc*Y+ec*Z+tc*W)*S+(cc*Z+oc*W+nc*X)*T+(sc*W+ec*X+cc*Y)*U;const lc=dc*ac;return V>lc||-V>lc?V:-insphereadapt(c,s,t,n,e,o,a,l,i,r,f,u,d,v,h,ac)}function inspherefast(c,s,t,n,e,o,a,l,i,r,f,u,d,v,h){const m=c-d;const _=n-d;const b=a-d;const M=r-d;const p=s-v;const $=e-v;const x=l-v;const g=f-v;const w=t-h;const y=o-h;const A=i-h;const F=u-h;const j=m*$-_*p;const k=_*x-b*$;const q=b*g-M*x;const z=M*p-m*g;const B=m*x-b*p;const C=_*g-M*$;const D=w*k-y*B+A*j;const E=y*q-A*C+F*k;const G=A*z+F*B+w*q;const H=F*j+w*C+y*z;const I=m*m+p*p+w*w;const J=_*_+$*$+y*y;const K=b*b+x*x+A*A;const L=M*M+g*g+F*F;return K*H-L*D+(I*E-J*G)}export{incircle,incirclefast,insphere,inspherefast,orient2d,orient2dfast,orient3d,orient3dfast};