├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── Appraisals ├── Gemfile ├── LICENSE.txt ├── Makefile ├── README.md ├── Rakefile ├── docker-compose.yml ├── examples └── web-ui.png ├── lib ├── sidekiq-benchmark.rb └── sidekiq-benchmark │ ├── testing.rb │ ├── version.rb │ ├── web.rb │ └── worker.rb ├── sidekiq-benchmark.gemspec ├── test ├── lib │ ├── testing.rb │ ├── web_test.rb │ └── worker_test.rb └── test_helper.rb ├── vendor └── .git_keep └── web ├── assets └── javascripts │ └── chartkick.js └── views └── benchmarks.erb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | ruby-latest: 14 | runs-on: ubuntu-latest 15 | container: ruby:latest 16 | services: 17 | redis: 18 | image: redis 19 | options: >- 20 | --health-cmd "redis-cli ping" 21 | --health-interval 10s 22 | --health-timeout 5s 23 | --health-retries 5 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: bundle install 27 | run: bundle install 28 | - name: run tests 29 | run: bundle exec rake test 30 | env: 31 | REDIS_HOST: redis 32 | ruby-latest-sidekiq-6: 33 | runs-on: ubuntu-latest 34 | container: ruby:latest 35 | services: 36 | redis: 37 | image: redis 38 | options: >- 39 | --health-cmd "redis-cli ping" 40 | --health-interval 10s 41 | --health-timeout 5s 42 | --health-retries 5 43 | steps: 44 | - uses: actions/checkout@v3 45 | - name: bundle install 46 | run: bundle install 47 | - name: appraisal install 48 | run: bundle exec appraisal install 49 | - name: run tests 50 | run: bundle exec appraisal sidekiq-6 rake test 51 | env: 52 | REDIS_HOST: redis 53 | ruby-2: 54 | runs-on: ubuntu-latest 55 | container: ruby:2 56 | services: 57 | redis: 58 | image: redis 59 | options: >- 60 | --health-cmd "redis-cli ping" 61 | --health-interval 10s 62 | --health-timeout 5s 63 | --health-retries 5 64 | steps: 65 | - uses: actions/checkout@v3 66 | - name: bundle install 67 | run: bundle install 68 | - name: run tests 69 | run: bundle exec rake test 70 | env: 71 | REDIS_HOST: redis 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | Gemfile.lock 3 | coverage/ 4 | gemfiles 5 | *.gem 6 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'sidekiq-7' do 2 | gem 'sidekiq', '> 7' 3 | end 4 | 5 | appraise 'sidekiq-6' do 6 | gem 'sidekiq', '~> 6' 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in sidekiq-benchmark.gemspec 4 | gemspec 5 | 6 | gem "sidekiq", "> 5" 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Konstantin Kosmatov 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | DOCKER_CONSOLE := docker-compose run -w /app$(APP_PATH) --rm console 4 | PROJECT_NAME ?= $(shell basename $(shell pwd)) 5 | 6 | container: 7 | docker-compose build 8 | $(DOCKER_CONSOLE) bundle install 9 | 10 | bundle bundle_install bundle_update: 11 | $(eval bundle_cmd ?= $(shell echo $@ | tr _ ' ')) 12 | $(DOCKER_CONSOLE) $(bundle_cmd) 13 | ifndef APP_PATH 14 | APP_PATH=/examples/heroku make $@ 15 | endif 16 | 17 | build: 18 | $(DOCKER_CONSOLE) gem build $(PROJECT_NAME).gemspec 19 | 20 | clean: 21 | -$(DOCKER_CONSOLE) rm *.gem 22 | 23 | test: 24 | $(DOCKER_CONSOLE) bundle exec rake test 25 | 26 | push: clean test build 27 | $(eval gem_file ?= $(shell find -name $(PROJECT_NAME)\*.gem -print | sort | tail -1)) 28 | $(DOCKER_CONSOLE) gem push $(gem_file) 29 | 30 | signin: 31 | $(DOCKER_CONSOLE) gem signin 32 | 33 | console: 34 | $(DOCKER_CONSOLE) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sidekiq::Benchmark 2 | [![Gem Version](https://badge.fury.io/rb/sidekiq-benchmark.svg)](https://badge.fury.io/rb/sidekiq-benchmark) 3 | [![Ruby](https://github.com/kosmatov/sidekiq-benchmark/actions/workflows/ruby.yml/badge.svg)](https://github.com/kosmatov/sidekiq-benchmark/actions/workflows/ruby.yml) 4 | 5 | Adds benchmarking methods to 6 | [Sidekiq](https://github.com/mperham/sidekiq) workers, keeps metrics and adds tab to Web UI to let you browse them. 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | gem 'sidekiq-benchmark' 13 | 14 | And then execute: 15 | 16 | ```shell 17 | bundle 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```ruby 23 | class SampleWorker 24 | include Sidekiq::Worker 25 | include Sidekiq::Benchmark::Worker 26 | 27 | def perform(id) 28 | benchmark.first_metric do 29 | 100500.times do something end 30 | end 31 | 32 | benchmark.second_metric do 33 | 42.times do anything end 34 | end 35 | 36 | benchmark.finish 37 | end 38 | end 39 | 40 | class OtherSampleWorker 41 | include Sidekiq::Worker 42 | include Sidekiq::Benchmark::Worker 43 | 44 | def perform(id) 45 | benchmark do |bm| 46 | bm.some_metric do 47 | 100500.times do 48 | end 49 | end 50 | 51 | bm.other_metric do 52 | something_code 53 | end 54 | 55 | bm.some_metric do 56 | # some_metric measure continues 57 | end 58 | end 59 | # if block given, yield and finish 60 | end 61 | 62 | end 63 | ``` 64 | ## Examples 65 | 66 | ### Web UI 67 | 68 | ![Web UI](https://github.com/kosmatov/sidekiq-benchmark/raw/master/examples/web-ui.png) 69 | 70 | ## Testing sidekiq workers 71 | 72 | When you use [Sidekiq::Testing](https://github.com/mperham/sidekiq/wiki/Testing) you 73 | must load `sidekiq-benchmark/testing` to stop saving benchmark data to redis. 74 | Just add next code to your test or spec helper: 75 | 76 | ```ruby 77 | require 'sidekiq-benchmark/testing' 78 | ``` 79 | 80 | ## Contributing 81 | 82 | 1. Fork it 83 | 2. Create your feature branch (`git checkout -b my-new-feature`) 84 | 3. Commit your changes (`git commit -am 'Add some feature'`) 85 | 4. Push to the branch (`git push origin my-new-feature`) 86 | 5. Create new Pull Request 87 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | task default: :test 4 | 5 | Rake::TestTask.new :test do |test| 6 | test.libs << 'test' 7 | test.pattern = 'test/**/*_test.rb' 8 | end 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | x-logging: &logging 4 | options: 5 | max-size: '5k' 6 | max-file: '5' 7 | labels: "{{.Name}}" 8 | driver: json-file 9 | 10 | services: 11 | app: &base 12 | image: ruby:latest 13 | environment: &app_env 14 | - BUNDLE_PATH=/app/vendor 15 | - REDIS_HOST=redis 16 | volumes: 17 | - .:/app:cached 18 | - ./vendor:/app/vendor 19 | - ~/.pry_history:/root/.pry_history 20 | - ~/.bash_history:/root/.bash_history 21 | - ~/.local/share/gem/credentials:/root/.local/share/gem/credentials 22 | links: 23 | - redis:redis 24 | logging: *logging 25 | 26 | redis: 27 | image: redis:alpine 28 | logging: *logging 29 | 30 | console: 31 | <<: *base 32 | command: bash 33 | -------------------------------------------------------------------------------- /examples/web-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosmatov/sidekiq-benchmark/b5c7030a82b3383f4d2ddde81fd91e58a21a9993/examples/web-ui.png -------------------------------------------------------------------------------- /lib/sidekiq-benchmark.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq/web' 2 | require 'sidekiq-benchmark/web' 3 | 4 | Sidekiq::Web.register Sidekiq::Benchmark::Web 5 | Sidekiq::Web.tabs["Benchmarks"] = "benchmarks" 6 | 7 | module Sidekiq 8 | module Benchmark 9 | REDIS_NAMESPACE = :benchmark 10 | TYPES_KEY = "#{REDIS_NAMESPACE}:types".freeze 11 | STAT_KEYS = %i[stats total] 12 | REDIS_KEYS_TTL = 3600 * 24 * 30 13 | 14 | autoload :Worker, 'sidekiq-benchmark/worker' 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /lib/sidekiq-benchmark/testing.rb: -------------------------------------------------------------------------------- 1 | module Sidekiq 2 | module Benchmark 3 | module Worker 4 | class Benchmark 5 | def save; end 6 | def set_redis_key(key); end 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/sidekiq-benchmark/version.rb: -------------------------------------------------------------------------------- 1 | module Sidekiq 2 | module Benchmark 3 | VERSION = "0.7.3" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/sidekiq-benchmark/web.rb: -------------------------------------------------------------------------------- 1 | require 'chartkick' 2 | 3 | module Sidekiq 4 | module Benchmark 5 | module Web 6 | WEB_DIR = File.expand_path("../../../web", __FILE__).freeze 7 | JS_DIR = File.join(WEB_DIR, "assets", "javascripts").freeze 8 | VIEW_PATH = File.join(WEB_DIR, "views", "benchmarks.erb").freeze 9 | 10 | def self.registered(app) 11 | app.helpers Chartkick::Helper 12 | 13 | app.get '/benchmarks/javascripts/chartkick.js' do 14 | body = File.read File.join(JS_DIR, 'chartkick.js') 15 | headers = { 16 | 'Content-Type' => 'application/javascript', 17 | 'Cache-Control' => 'public, max-age=84600' 18 | } 19 | [200, headers, [body]] 20 | end 21 | 22 | app.get "/benchmarks" do 23 | @charts = {} 24 | 25 | Sidekiq.redis do |conn| 26 | @types = conn.smembers TYPES_KEY 27 | @types.each do |type| 28 | @charts[type] = STAT_KEYS.reduce({}) { |a, e| a[e] = []; a } 29 | 30 | total_key = "#{REDIS_NAMESPACE}:#{type}:total" 31 | total_keys = conn.hkeys(total_key) - %w(start_time job_time finish_time) 32 | 33 | total_time = conn.hget total_key, :job_time 34 | total_time = total_time.to_f 35 | total_keys.each do |key| 36 | value = conn.hget total_key, key 37 | @charts[type][:total] << [key, value.to_f.round(2)] 38 | end 39 | 40 | stats = conn.hgetall "#{REDIS_NAMESPACE}:#{type}:stats" 41 | stats.each do |key, value| 42 | @charts[type][:stats] << [key.to_f, value.to_i] 43 | end 44 | 45 | @charts[type][:stats].sort! { |a, b| a[0] <=> b[0] } 46 | @charts[type][:stats].map! { |a| [a[0].to_s, a[1]] } 47 | end 48 | end 49 | 50 | erb File.read(VIEW_PATH) 51 | end 52 | 53 | app.post "/benchmarks/remove" do 54 | Sidekiq.redis do |conn| 55 | keys = STAT_KEYS.map { |key| "#{REDIS_NAMESPACE}:#{params[:type]}:#{key}" } 56 | conn.srem TYPES_KEY, params[:type] 57 | conn.del keys 58 | end 59 | 60 | redirect "#{root_path}benchmarks" 61 | end 62 | app.post "/benchmarks/remove_all" do 63 | Sidekiq.redis do |conn| 64 | types = conn.smembers TYPES_KEY 65 | keys = STAT_KEYS.map do |key| 66 | types.map { |type| "#{REDIS_NAMESPACE}:#{type}:#{key}" } 67 | end.flatten 68 | keys << TYPES_KEY 69 | conn.del keys 70 | end 71 | 72 | redirect "#{root_path}benchmarks" 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/sidekiq-benchmark/worker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Sidekiq 4 | module Benchmark 5 | module Worker 6 | def benchmark(options = {}) 7 | @benchmark ||= Benchmark.new self, benchmark_redis_type_key, options 8 | 9 | if block_given? 10 | yield @benchmark 11 | @benchmark.finish 12 | end 13 | 14 | @benchmark 15 | end 16 | 17 | def benchmark_redis_type_key 18 | @benchmark_redis_type_key ||= self.class.name.gsub('::', '_').downcase 19 | end 20 | 21 | class Benchmark 22 | attr_reader :metrics, :start_time, :finish_time, :redis_keys 23 | 24 | def initialize(worker, redis_key, options) 25 | @metrics = {} 26 | @worker = worker 27 | @options = options 28 | @start_time = Time.now 29 | 30 | @redis_keys = 31 | %i[total stats].reduce({}) do |m, e| 32 | m[e] = "#{REDIS_NAMESPACE}:#{redis_key}:#{e}" 33 | m 34 | end 35 | 36 | set_redis_key redis_key 37 | end 38 | 39 | def finish 40 | @finish_time = Time.now 41 | self[:job_time] = finish_time - start_time 42 | save 43 | end 44 | 45 | def measure(name) 46 | t0 = Time.now 47 | ret = yield 48 | t1 = Time.now 49 | 50 | self[name] ||= 0.0 51 | self[name] += t1 - t0 52 | 53 | Sidekiq.logger.info "Benchmark #{name}: #{t1 - t0} sec." if @options[:log] 54 | 55 | ret 56 | end 57 | alias_method :bm, :measure 58 | 59 | def call(name, *args) 60 | measure(name) { @worker.send(name, *args) } 61 | end 62 | 63 | def []=(name, value) 64 | @metrics[name] = value.to_f 65 | end 66 | 67 | def [](name) 68 | @metrics[name] 69 | end 70 | 71 | def set_redis_key(key) 72 | Sidekiq.redis do |conn| 73 | conn.sadd TYPES_KEY, key 74 | conn.expire TYPES_KEY, REDIS_KEYS_TTL 75 | end 76 | end 77 | 78 | def save 79 | job_time_key = @metrics[:job_time].round(1) 80 | 81 | Sidekiq.redis do |conn| 82 | conn.multi do |transaction| 83 | @metrics.each do |key, value| 84 | transaction.hincrbyfloat redis_keys[:total], key, value 85 | end 86 | 87 | transaction.hincrby redis_keys[:stats], job_time_key, 1 88 | 89 | transaction.hsetnx redis_keys[:total], "start_time", start_time.to_s 90 | transaction.hset redis_keys[:total], "finish_time", finish_time.to_s 91 | 92 | transaction.expire redis_keys[:stats], REDIS_KEYS_TTL 93 | transaction.expire redis_keys[:total], REDIS_KEYS_TTL 94 | end 95 | end 96 | end 97 | 98 | def method_missing(name, *args, &block) 99 | if block_given? 100 | measure(name, &block) 101 | self[name] 102 | else 103 | self[name] = args[0] 104 | end 105 | end 106 | 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /sidekiq-benchmark.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'sidekiq-benchmark/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "sidekiq-benchmark" 8 | gem.version = Sidekiq::Benchmark::VERSION 9 | gem.authors = ["Konstantin Kosmatov"] 10 | gem.email = ["key@kosmatov.ru"] 11 | gem.description = %q{Benchmarks for Sidekiq} 12 | gem.summary = %q{Adds benchmarking methods to Sidekiq workers, keeps metrics and adds tab to Web UI to let you browse them.} 13 | gem.homepage = "https://github.com/kosmatov/sidekiq-benchmark/" 14 | gem.license = 'MIT' 15 | 16 | gem.files = `git ls-files | grep -Ev '^(examples|vendor|docker|.travis|Makefile)'`.split("\n") 17 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 18 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 19 | gem.require_paths = ["lib"] 20 | 21 | gem.add_dependency "chartkick", ">= 1.1.1" 22 | gem.add_dependency "sidekiq", "> 5" 23 | 24 | gem.add_development_dependency "rake" 25 | gem.add_development_dependency "rack-test" 26 | gem.add_development_dependency "minitest", "~> 5" 27 | gem.add_development_dependency "coveralls" 28 | gem.add_development_dependency "pry" 29 | gem.add_development_dependency 'delorean', '~> 2.1' 30 | gem.add_development_dependency 'appraisal' 31 | end 32 | -------------------------------------------------------------------------------- /test/lib/testing.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Sidekiq::Benchmark::TestingTest < Minitest::Spec 4 | require 'sidekiq-benchmark/testing' 5 | 6 | describe 'Testing' do 7 | before do 8 | Sidekiq::Benchmark::Test.flush_db 9 | @worker = Sidekiq::Benchmark::Test::WorkerMock.new 10 | end 11 | 12 | it "save nothing to redis" do 13 | Sidekiq.redis do |conn| 14 | total_time = conn.hget(@worker.benchmark.redis_keys[:total], :job_time) 15 | _(total_time).must_be_nil 16 | end 17 | end 18 | 19 | it "run code in bm blocks" do 20 | _(@worker.counter).wont_equal 0 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/lib/web_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Sidekiq 4 | module Benchmark 5 | module Test 6 | TOKEN = SecureRandom.base64(32).freeze 7 | 8 | describe "Web extention" do 9 | include Rack::Test::Methods 10 | 11 | def app 12 | @app ||= Sidekiq::Web 13 | end 14 | 15 | before do 16 | env 'rack.session', { csrf: TOKEN } 17 | env 'HTTP_X_CSRF_TOKEN', TOKEN 18 | Test.flush_db 19 | end 20 | 21 | it "display index without stats" do 22 | get '/benchmarks' 23 | _(last_response.status).must_equal 200 24 | end 25 | 26 | it "display index with stats" do 27 | WorkerMock.new 28 | 29 | get '/benchmarks' 30 | _(last_response.status).must_equal 200 31 | end 32 | 33 | it "remove all benchmarks data" do 34 | WorkerMock.new 35 | 36 | Sidekiq.redis { |conn| _(conn.keys("benchmark:*")).wont_be_empty } 37 | 38 | post '/benchmarks/remove_all' 39 | _(last_response.status).must_equal 302 40 | 41 | Sidekiq.redis { |conn| _(conn.keys("benchmark:*")).must_be_empty } 42 | end 43 | 44 | it "remove benchmark data" do 45 | WorkerMock.new 46 | 47 | Sidekiq.redis { |conn| _(conn.keys("benchmark:sidekiq_benchmark_test_workermock:*")).wont_be_empty } 48 | 49 | post '/benchmarks/remove', type: :sidekiq_benchmark_test_workermock 50 | _(last_response.status).must_equal 302 51 | 52 | Sidekiq.redis { |conn| _(conn.keys("benchmark:sidekiq_benchmark_test_workermock:*")).must_be_empty } 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/lib/worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Sidekiq 4 | module Benchmark 5 | module Test 6 | 7 | class WorkerTest < Minitest::Spec 8 | include Sidekiq::Benchmark::Worker 9 | 10 | before do 11 | Test.flush_db 12 | @worker = WorkerMock.new 13 | end 14 | 15 | it "should collect metrics" do 16 | metrics = @worker.benchmark.metrics 17 | 18 | @worker.metric_names.each do |metric_name| 19 | _(metrics[metric_name]).wont_be_nil 20 | end 21 | 22 | _(@worker.benchmark.start_time).wont_be_nil 23 | _(@worker.benchmark.finish_time).wont_be_nil 24 | _(metrics[:assigned_metric]).must_equal @worker.assigned_metric 25 | end 26 | 27 | it 'should add up metrics' do 28 | worker = ContinuingWorkerMock.new 29 | metrics = worker.benchmark.metrics 30 | 31 | assert_in_delta metrics[:continued_metric], 2, 0.2 32 | end 33 | 34 | it "should save metrics to redis" do 35 | Sidekiq.redis do |conn| 36 | total_time = conn.hget(@worker.benchmark.redis_keys[:total], :job_time) 37 | _(total_time).wont_be_nil 38 | 39 | metrics = conn.hkeys(@worker.benchmark.redis_keys[:stats]) 40 | _(metrics).wont_be_empty 41 | end 42 | end 43 | 44 | it "should collect metrics with alter syntax" do 45 | worker = AlterWorkerMock.new 46 | metrics = worker.benchmark.metrics 47 | 48 | Sidekiq.redis do |conn| 49 | metric_set = conn.hkeys(worker.benchmark.redis_keys[:stats]) 50 | _(metric_set).must_be_empty 51 | end 52 | 53 | worker.metric_names.each do |metric_name| 54 | _(metrics[metric_name]).wont_be_nil 55 | end 56 | 57 | _(worker.benchmark.finish_time).must_be_nil 58 | worker.finish 59 | _(worker.benchmark.finish_time).wont_be_nil 60 | 61 | Sidekiq.redis do |conn| 62 | metric_set = conn.hkeys(worker.benchmark.redis_keys[:stats]) 63 | _(metric_set).wont_be_empty 64 | end 65 | end 66 | 67 | it "should allow benchmark methods" do 68 | worker = AlterWorkerMock.new 69 | value = worker.benchmark.call(:multiply, 4, 4) 70 | 71 | _(value).must_equal 16 72 | _(worker.benchmark.metrics[:multiply]).wont_be_nil 73 | end 74 | 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'minitest/pride' 3 | 4 | ENV['RACK_ENV'] = 'test' 5 | $TESTING = true 6 | 7 | require 'bundler/setup' 8 | require 'rack/test' 9 | 10 | require 'sidekiq' 11 | require 'sidekiq-benchmark' 12 | 13 | require 'delorean' 14 | require 'pry' 15 | 16 | Bundler.require 17 | 18 | Sidekiq.configure_server do |cfg| 19 | cfg.redis = {url: "redis://#{ENV['REDIS_HOST'] || 'localhost'}/15"} 20 | end 21 | 22 | Sidekiq.configure_client do |cfg| 23 | cfg.redis = {url: "redis://#{ENV['REDIS_HOST'] || 'localhost'}/15"} 24 | end 25 | 26 | module Sidekiq 27 | module Benchmark 28 | module Test 29 | 30 | class WorkerMock 31 | include Sidekiq::Worker 32 | include Sidekiq::Benchmark::Worker 33 | 34 | attr_reader :bm_obj, :metric_names, :assigned_metric, :counter 35 | 36 | def initialize 37 | @assigned_metric = 0.1 38 | @counter = 0 39 | 40 | benchmark do |bm| 41 | bm.test_metric do 42 | 2.times do |i| 43 | bm.send("nested_test_metric_#{i}") do 44 | Delorean.jump 1 45 | @counter += 100500 46 | end 47 | end 48 | end 49 | 50 | bm.assigned_metric @assigned_metric 51 | end 52 | 53 | @metric_names = [:test_metric, :nested_test_metric_1, :job_time] 54 | end 55 | end 56 | 57 | class AlterWorkerMock < WorkerMock 58 | def initialize 59 | benchmark.test_metric do 60 | Delorean.jump 1 61 | end 62 | 63 | benchmark.other_metric do 64 | Delorean.jump 1 65 | end 66 | 67 | @metric_names = [:test_metric, :other_metric] 68 | end 69 | 70 | def multiply(a, b) 71 | a * b 72 | end 73 | 74 | def finish 75 | benchmark.finish 76 | end 77 | end 78 | 79 | class ContinuingWorkerMock < WorkerMock 80 | def initialize 81 | benchmark do |bm| 82 | bm.continued_metric do 83 | Delorean.jump 1 84 | end 85 | 86 | bm.continued_metric do 87 | Delorean.jump 1 88 | end 89 | end 90 | end 91 | end 92 | 93 | def self.flush_db 94 | Sidekiq.redis do |conn| 95 | conn.flushdb 96 | end 97 | end 98 | 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /vendor/.git_keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosmatov/sidekiq-benchmark/b5c7030a82b3383f4d2ddde81fd91e58a21a9993/vendor/.git_keep -------------------------------------------------------------------------------- /web/assets/javascripts/chartkick.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Chartkick.js 3 | * Create beautiful charts with one line of JavaScript 4 | * https://github.com/ankane/chartkick.js 5 | * v3.2.1 6 | * MIT License 7 | */ 8 | 9 | (function (global, factory) { 10 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 11 | typeof define === 'function' && define.amd ? define(factory) : 12 | (global = global || self, global.Chartkick = factory()); 13 | }(this, (function () { 'use strict'; 14 | 15 | function isArray(variable) { 16 | return Object.prototype.toString.call(variable) === "[object Array]"; 17 | } 18 | 19 | function isFunction(variable) { 20 | return variable instanceof Function; 21 | } 22 | 23 | function isPlainObject(variable) { 24 | // protect against prototype pollution, defense 2 25 | return Object.prototype.toString.call(variable) === "[object Object]" && !isFunction(variable) && variable instanceof Object; 26 | } 27 | 28 | // https://github.com/madrobby/zepto/blob/master/src/zepto.js 29 | function extend(target, source) { 30 | var key; 31 | for (key in source) { 32 | // protect against prototype pollution, defense 1 33 | if (key === "__proto__") { continue; } 34 | 35 | if (isPlainObject(source[key]) || isArray(source[key])) { 36 | if (isPlainObject(source[key]) && !isPlainObject(target[key])) { 37 | target[key] = {}; 38 | } 39 | if (isArray(source[key]) && !isArray(target[key])) { 40 | target[key] = []; 41 | } 42 | extend(target[key], source[key]); 43 | } else if (source[key] !== undefined) { 44 | target[key] = source[key]; 45 | } 46 | } 47 | } 48 | 49 | function merge(obj1, obj2) { 50 | var target = {}; 51 | extend(target, obj1); 52 | extend(target, obj2); 53 | return target; 54 | } 55 | 56 | var DATE_PATTERN = /^(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)$/i; 57 | 58 | // https://github.com/Do/iso8601.js 59 | var ISO8601_PATTERN = /(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)(T)?(\d\d)(:)?(\d\d)?(:)?(\d\d)?([.,]\d+)?($|Z|([+-])(\d\d)(:)?(\d\d)?)/i; 60 | var DECIMAL_SEPARATOR = String(1.5).charAt(1); 61 | 62 | function parseISO8601(input) { 63 | var day, hour, matches, milliseconds, minutes, month, offset, result, seconds, type, year; 64 | type = Object.prototype.toString.call(input); 65 | if (type === "[object Date]") { 66 | return input; 67 | } 68 | if (type !== "[object String]") { 69 | return; 70 | } 71 | matches = input.match(ISO8601_PATTERN); 72 | if (matches) { 73 | year = parseInt(matches[1], 10); 74 | month = parseInt(matches[3], 10) - 1; 75 | day = parseInt(matches[5], 10); 76 | hour = parseInt(matches[7], 10); 77 | minutes = matches[9] ? parseInt(matches[9], 10) : 0; 78 | seconds = matches[11] ? parseInt(matches[11], 10) : 0; 79 | milliseconds = matches[12] ? parseFloat(DECIMAL_SEPARATOR + matches[12].slice(1)) * 1000 : 0; 80 | result = Date.UTC(year, month, day, hour, minutes, seconds, milliseconds); 81 | if (matches[13] && matches[14]) { 82 | offset = matches[15] * 60; 83 | if (matches[17]) { 84 | offset += parseInt(matches[17], 10); 85 | } 86 | offset *= matches[14] === "-" ? -1 : 1; 87 | result -= offset * 60 * 1000; 88 | } 89 | return new Date(result); 90 | } 91 | } 92 | // end iso8601.js 93 | 94 | function negativeValues(series) { 95 | var i, j, data; 96 | for (i = 0; i < series.length; i++) { 97 | data = series[i].data; 98 | for (j = 0; j < data.length; j++) { 99 | if (data[j][1] < 0) { 100 | return true; 101 | } 102 | } 103 | } 104 | return false; 105 | } 106 | 107 | function toStr(n) { 108 | return "" + n; 109 | } 110 | 111 | function toFloat(n) { 112 | return parseFloat(n); 113 | } 114 | 115 | function toDate(n) { 116 | var matches, year, month, day; 117 | if (typeof n !== "object") { 118 | if (typeof n === "number") { 119 | n = new Date(n * 1000); // ms 120 | } else { 121 | n = toStr(n); 122 | if ((matches = n.match(DATE_PATTERN))) { 123 | year = parseInt(matches[1], 10); 124 | month = parseInt(matches[3], 10) - 1; 125 | day = parseInt(matches[5], 10); 126 | return new Date(year, month, day); 127 | } else { // str 128 | // try our best to get the str into iso8601 129 | // TODO be smarter about this 130 | var str = n.replace(/ /, "T").replace(" ", "").replace("UTC", "Z"); 131 | n = parseISO8601(str) || new Date(n); 132 | } 133 | } 134 | } 135 | return n; 136 | } 137 | 138 | function toArr(n) { 139 | if (!isArray(n)) { 140 | var arr = [], i; 141 | for (i in n) { 142 | if (n.hasOwnProperty(i)) { 143 | arr.push([i, n[i]]); 144 | } 145 | } 146 | n = arr; 147 | } 148 | return n; 149 | } 150 | 151 | function jsOptionsFunc(defaultOptions, hideLegend, setTitle, setMin, setMax, setStacked, setXtitle, setYtitle) { 152 | return function (chart, opts, chartOptions) { 153 | var series = chart.data; 154 | var options = merge({}, defaultOptions); 155 | options = merge(options, chartOptions || {}); 156 | 157 | if (chart.hideLegend || "legend" in opts) { 158 | hideLegend(options, opts.legend, chart.hideLegend); 159 | } 160 | 161 | if (opts.title) { 162 | setTitle(options, opts.title); 163 | } 164 | 165 | // min 166 | if ("min" in opts) { 167 | setMin(options, opts.min); 168 | } else if (!negativeValues(series)) { 169 | setMin(options, 0); 170 | } 171 | 172 | // max 173 | if (opts.max) { 174 | setMax(options, opts.max); 175 | } 176 | 177 | if ("stacked" in opts) { 178 | setStacked(options, opts.stacked); 179 | } 180 | 181 | if (opts.colors) { 182 | options.colors = opts.colors; 183 | } 184 | 185 | if (opts.xtitle) { 186 | setXtitle(options, opts.xtitle); 187 | } 188 | 189 | if (opts.ytitle) { 190 | setYtitle(options, opts.ytitle); 191 | } 192 | 193 | // merge library last 194 | options = merge(options, opts.library || {}); 195 | 196 | return options; 197 | }; 198 | } 199 | 200 | function sortByTime(a, b) { 201 | return a[0].getTime() - b[0].getTime(); 202 | } 203 | 204 | function sortByNumberSeries(a, b) { 205 | return a[0] - b[0]; 206 | } 207 | 208 | function sortByNumber(a, b) { 209 | return a - b; 210 | } 211 | 212 | function isMinute(d) { 213 | return d.getMilliseconds() === 0 && d.getSeconds() === 0; 214 | } 215 | 216 | function isHour(d) { 217 | return isMinute(d) && d.getMinutes() === 0; 218 | } 219 | 220 | function isDay(d) { 221 | return isHour(d) && d.getHours() === 0; 222 | } 223 | 224 | function isWeek(d, dayOfWeek) { 225 | return isDay(d) && d.getDay() === dayOfWeek; 226 | } 227 | 228 | function isMonth(d) { 229 | return isDay(d) && d.getDate() === 1; 230 | } 231 | 232 | function isYear(d) { 233 | return isMonth(d) && d.getMonth() === 0; 234 | } 235 | 236 | function isDate(obj) { 237 | return !isNaN(toDate(obj)) && toStr(obj).length >= 6; 238 | } 239 | 240 | function isNumber(obj) { 241 | return typeof obj === "number"; 242 | } 243 | 244 | var byteSuffixes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB"]; 245 | 246 | function formatValue(pre, value, options, axis) { 247 | pre = pre || ""; 248 | if (options.prefix) { 249 | if (value < 0) { 250 | value = value * -1; 251 | pre += "-"; 252 | } 253 | pre += options.prefix; 254 | } 255 | 256 | var suffix = options.suffix || ""; 257 | var precision = options.precision; 258 | var round = options.round; 259 | 260 | if (options.byteScale) { 261 | var suffixIdx; 262 | var baseValue = axis ? options.byteScale : value; 263 | 264 | if (baseValue >= 1152921504606846976) { 265 | value /= 1152921504606846976; 266 | suffixIdx = 6; 267 | } else if (baseValue >= 1125899906842624) { 268 | value /= 1125899906842624; 269 | suffixIdx = 5; 270 | } else if (baseValue >= 1099511627776) { 271 | value /= 1099511627776; 272 | suffixIdx = 4; 273 | } else if (baseValue >= 1073741824) { 274 | value /= 1073741824; 275 | suffixIdx = 3; 276 | } else if (baseValue >= 1048576) { 277 | value /= 1048576; 278 | suffixIdx = 2; 279 | } else if (baseValue >= 1024) { 280 | value /= 1024; 281 | suffixIdx = 1; 282 | } else { 283 | suffixIdx = 0; 284 | } 285 | 286 | // TODO handle manual precision case 287 | if (precision === undefined && round === undefined) { 288 | if (value >= 1023.5) { 289 | if (suffixIdx < byteSuffixes.length - 1) { 290 | value = 1.0; 291 | suffixIdx += 1; 292 | } 293 | } 294 | precision = value >= 1000 ? 4 : 3; 295 | } 296 | suffix = " " + byteSuffixes[suffixIdx]; 297 | } 298 | 299 | if (precision !== undefined && round !== undefined) { 300 | throw Error("Use either round or precision, not both"); 301 | } 302 | 303 | if (!axis) { 304 | if (precision !== undefined) { 305 | value = value.toPrecision(precision); 306 | if (!options.zeros) { 307 | value = parseFloat(value); 308 | } 309 | } 310 | 311 | if (round !== undefined) { 312 | if (round < 0) { 313 | var num = Math.pow(10, -1 * round); 314 | value = parseInt((1.0 * value / num).toFixed(0)) * num; 315 | } else { 316 | value = value.toFixed(round); 317 | if (!options.zeros) { 318 | value = parseFloat(value); 319 | } 320 | } 321 | } 322 | } 323 | 324 | if (options.thousands || options.decimal) { 325 | value = toStr(value); 326 | var parts = value.split("."); 327 | value = parts[0]; 328 | if (options.thousands) { 329 | value = value.replace(/\B(?=(\d{3})+(?!\d))/g, options.thousands); 330 | } 331 | if (parts.length > 1) { 332 | value += (options.decimal || ".") + parts[1]; 333 | } 334 | } 335 | 336 | return pre + value + suffix; 337 | } 338 | 339 | function seriesOption(chart, series, option) { 340 | if (option in series) { 341 | return series[option]; 342 | } else if (option in chart.options) { 343 | return chart.options[option]; 344 | } 345 | return null; 346 | } 347 | 348 | function allZeros(data) { 349 | var i, j, d; 350 | for (i = 0; i < data.length; i++) { 351 | d = data[i].data; 352 | for (j = 0; j < d.length; j++) { 353 | if (d[j][1] != 0) { 354 | return false; 355 | } 356 | } 357 | } 358 | return true; 359 | } 360 | 361 | var baseOptions = { 362 | maintainAspectRatio: false, 363 | animation: false, 364 | tooltips: { 365 | displayColors: false, 366 | callbacks: {} 367 | }, 368 | legend: {}, 369 | title: {fontSize: 20, fontColor: "#333"} 370 | }; 371 | 372 | var defaultOptions = { 373 | scales: { 374 | yAxes: [ 375 | { 376 | ticks: { 377 | maxTicksLimit: 4 378 | }, 379 | scaleLabel: { 380 | fontSize: 16, 381 | // fontStyle: "bold", 382 | fontColor: "#333" 383 | } 384 | } 385 | ], 386 | xAxes: [ 387 | { 388 | gridLines: { 389 | drawOnChartArea: false 390 | }, 391 | scaleLabel: { 392 | fontSize: 16, 393 | // fontStyle: "bold", 394 | fontColor: "#333" 395 | }, 396 | time: {}, 397 | ticks: {} 398 | } 399 | ] 400 | } 401 | }; 402 | 403 | // http://there4.io/2012/05/02/google-chart-color-list/ 404 | var defaultColors = [ 405 | "#3366CC", "#DC3912", "#FF9900", "#109618", "#990099", "#3B3EAC", "#0099C6", 406 | "#DD4477", "#66AA00", "#B82E2E", "#316395", "#994499", "#22AA99", "#AAAA11", 407 | "#6633CC", "#E67300", "#8B0707", "#329262", "#5574A6", "#651067" 408 | ]; 409 | 410 | var hideLegend = function (options, legend, hideLegend) { 411 | if (legend !== undefined) { 412 | options.legend.display = !!legend; 413 | if (legend && legend !== true) { 414 | options.legend.position = legend; 415 | } 416 | } else if (hideLegend) { 417 | options.legend.display = false; 418 | } 419 | }; 420 | 421 | var setTitle = function (options, title) { 422 | options.title.display = true; 423 | options.title.text = title; 424 | }; 425 | 426 | var setMin = function (options, min) { 427 | if (min !== null) { 428 | options.scales.yAxes[0].ticks.min = toFloat(min); 429 | } 430 | }; 431 | 432 | var setMax = function (options, max) { 433 | options.scales.yAxes[0].ticks.max = toFloat(max); 434 | }; 435 | 436 | var setBarMin = function (options, min) { 437 | if (min !== null) { 438 | options.scales.xAxes[0].ticks.min = toFloat(min); 439 | } 440 | }; 441 | 442 | var setBarMax = function (options, max) { 443 | options.scales.xAxes[0].ticks.max = toFloat(max); 444 | }; 445 | 446 | var setStacked = function (options, stacked) { 447 | options.scales.xAxes[0].stacked = !!stacked; 448 | options.scales.yAxes[0].stacked = !!stacked; 449 | }; 450 | 451 | var setXtitle = function (options, title) { 452 | options.scales.xAxes[0].scaleLabel.display = true; 453 | options.scales.xAxes[0].scaleLabel.labelString = title; 454 | }; 455 | 456 | var setYtitle = function (options, title) { 457 | options.scales.yAxes[0].scaleLabel.display = true; 458 | options.scales.yAxes[0].scaleLabel.labelString = title; 459 | }; 460 | 461 | // https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb 462 | var addOpacity = function(hex, opacity) { 463 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 464 | return result ? "rgba(" + parseInt(result[1], 16) + ", " + parseInt(result[2], 16) + ", " + parseInt(result[3], 16) + ", " + opacity + ")" : hex; 465 | }; 466 | 467 | // check if not null or undefined 468 | // https://stackoverflow.com/a/27757708/1177228 469 | var notnull = function(x) { 470 | return x != null; 471 | }; 472 | 473 | var setLabelSize = function (chart, data, options) { 474 | var maxLabelSize = Math.ceil(chart.element.offsetWidth / 4.0 / data.labels.length); 475 | if (maxLabelSize > 25) { 476 | maxLabelSize = 25; 477 | } else if (maxLabelSize < 10) { 478 | maxLabelSize = 10; 479 | } 480 | if (!options.scales.xAxes[0].ticks.callback) { 481 | options.scales.xAxes[0].ticks.callback = function (value) { 482 | value = toStr(value); 483 | if (value.length > maxLabelSize) { 484 | return value.substring(0, maxLabelSize - 2) + "..."; 485 | } else { 486 | return value; 487 | } 488 | }; 489 | } 490 | }; 491 | 492 | var setFormatOptions = function(chart, options, chartType) { 493 | var formatOptions = { 494 | prefix: chart.options.prefix, 495 | suffix: chart.options.suffix, 496 | thousands: chart.options.thousands, 497 | decimal: chart.options.decimal, 498 | precision: chart.options.precision, 499 | round: chart.options.round, 500 | zeros: chart.options.zeros 501 | }; 502 | 503 | if (chart.options.bytes) { 504 | var series = chart.data; 505 | if (chartType === "pie") { 506 | series = [{data: series}]; 507 | } 508 | 509 | // calculate max 510 | var max = 0; 511 | for (var i = 0; i < series.length; i++) { 512 | var s = series[i]; 513 | for (var j = 0; j < s.data.length; j++) { 514 | if (s.data[j][1] > max) { 515 | max = s.data[j][1]; 516 | } 517 | } 518 | } 519 | 520 | // calculate scale 521 | var scale = 1; 522 | while (max >= 1024) { 523 | scale *= 1024; 524 | max /= 1024; 525 | } 526 | 527 | // set step size 528 | formatOptions.byteScale = scale; 529 | } 530 | 531 | if (chartType !== "pie") { 532 | var myAxes = options.scales.yAxes; 533 | if (chartType === "bar") { 534 | myAxes = options.scales.xAxes; 535 | } 536 | 537 | if (formatOptions.byteScale) { 538 | if (!myAxes[0].ticks.stepSize) { 539 | myAxes[0].ticks.stepSize = formatOptions.byteScale / 2; 540 | } 541 | if (!myAxes[0].ticks.maxTicksLimit) { 542 | myAxes[0].ticks.maxTicksLimit = 4; 543 | } 544 | } 545 | 546 | if (!myAxes[0].ticks.callback) { 547 | myAxes[0].ticks.callback = function (value) { 548 | return formatValue("", value, formatOptions, true); 549 | }; 550 | } 551 | } 552 | 553 | if (!options.tooltips.callbacks.label) { 554 | if (chartType === "scatter") { 555 | options.tooltips.callbacks.label = function (item, data) { 556 | var label = data.datasets[item.datasetIndex].label || ''; 557 | if (label) { 558 | label += ': '; 559 | } 560 | return label + '(' + item.xLabel + ', ' + item.yLabel + ')'; 561 | }; 562 | } else if (chartType === "bubble") { 563 | options.tooltips.callbacks.label = function (item, data) { 564 | var label = data.datasets[item.datasetIndex].label || ''; 565 | if (label) { 566 | label += ': '; 567 | } 568 | var dataPoint = data.datasets[item.datasetIndex].data[item.index]; 569 | return label + '(' + item.xLabel + ', ' + item.yLabel + ', ' + dataPoint.v + ')'; 570 | }; 571 | } else if (chartType === "pie") { 572 | // need to use separate label for pie charts 573 | options.tooltips.callbacks.label = function (tooltipItem, data) { 574 | var dataLabel = data.labels[tooltipItem.index]; 575 | var value = ': '; 576 | 577 | if (isArray(dataLabel)) { 578 | // show value on first line of multiline label 579 | // need to clone because we are changing the value 580 | dataLabel = dataLabel.slice(); 581 | dataLabel[0] += value; 582 | } else { 583 | dataLabel += value; 584 | } 585 | 586 | return formatValue(dataLabel, data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index], formatOptions); 587 | }; 588 | } else { 589 | var valueLabel = chartType === "bar" ? "xLabel" : "yLabel"; 590 | options.tooltips.callbacks.label = function (tooltipItem, data) { 591 | var label = data.datasets[tooltipItem.datasetIndex].label || ''; 592 | if (label) { 593 | label += ': '; 594 | } 595 | return formatValue(label, tooltipItem[valueLabel], formatOptions); 596 | }; 597 | } 598 | } 599 | }; 600 | 601 | var jsOptions = jsOptionsFunc(merge(baseOptions, defaultOptions), hideLegend, setTitle, setMin, setMax, setStacked, setXtitle, setYtitle); 602 | 603 | var createDataTable = function (chart, options, chartType, library) { 604 | var datasets = []; 605 | var labels = []; 606 | 607 | var colors = chart.options.colors || defaultColors; 608 | 609 | var day = true; 610 | var week = true; 611 | var dayOfWeek; 612 | var month = true; 613 | var year = true; 614 | var hour = true; 615 | var minute = true; 616 | 617 | var series = chart.data; 618 | 619 | var max = 0; 620 | if (chartType === "bubble") { 621 | for (var i$1 = 0; i$1 < series.length; i$1++) { 622 | var s$1 = series[i$1]; 623 | for (var j$1 = 0; j$1 < s$1.data.length; j$1++) { 624 | if (s$1.data[j$1][2] > max) { 625 | max = s$1.data[j$1][2]; 626 | } 627 | } 628 | } 629 | } 630 | 631 | var i, j, s, d, key, rows = [], rows2 = []; 632 | 633 | if (chartType === "bar" || chartType === "column" || (chart.xtype !== "number" && chart.xtype !== "bubble")) { 634 | var sortedLabels = []; 635 | 636 | for (i = 0; i < series.length; i++) { 637 | s = series[i]; 638 | 639 | for (j = 0; j < s.data.length; j++) { 640 | d = s.data[j]; 641 | key = chart.xtype == "datetime" ? d[0].getTime() : d[0]; 642 | if (!rows[key]) { 643 | rows[key] = new Array(series.length); 644 | } 645 | rows[key][i] = toFloat(d[1]); 646 | if (sortedLabels.indexOf(key) === -1) { 647 | sortedLabels.push(key); 648 | } 649 | } 650 | } 651 | 652 | if (chart.xtype === "datetime" || chart.xtype === "number") { 653 | sortedLabels.sort(sortByNumber); 654 | } 655 | 656 | for (j = 0; j < series.length; j++) { 657 | rows2.push([]); 658 | } 659 | 660 | var value; 661 | var k; 662 | for (k = 0; k < sortedLabels.length; k++) { 663 | i = sortedLabels[k]; 664 | if (chart.xtype === "datetime") { 665 | value = new Date(toFloat(i)); 666 | // TODO make this efficient 667 | day = day && isDay(value); 668 | if (!dayOfWeek) { 669 | dayOfWeek = value.getDay(); 670 | } 671 | week = week && isWeek(value, dayOfWeek); 672 | month = month && isMonth(value); 673 | year = year && isYear(value); 674 | hour = hour && isHour(value); 675 | minute = minute && isMinute(value); 676 | } else { 677 | value = i; 678 | } 679 | labels.push(value); 680 | for (j = 0; j < series.length; j++) { 681 | // Chart.js doesn't like undefined 682 | rows2[j].push(rows[i][j] === undefined ? null : rows[i][j]); 683 | } 684 | } 685 | } else { 686 | for (var i$2 = 0; i$2 < series.length; i$2++) { 687 | var s$2 = series[i$2]; 688 | var d$1 = []; 689 | for (var j$2 = 0; j$2 < s$2.data.length; j$2++) { 690 | var point = { 691 | x: toFloat(s$2.data[j$2][0]), 692 | y: toFloat(s$2.data[j$2][1]) 693 | }; 694 | if (chartType === "bubble") { 695 | point.r = toFloat(s$2.data[j$2][2]) * 20 / max; 696 | // custom attribute, for tooltip 697 | point.v = s$2.data[j$2][2]; 698 | } 699 | d$1.push(point); 700 | } 701 | rows2.push(d$1); 702 | } 703 | } 704 | 705 | for (i = 0; i < series.length; i++) { 706 | s = series[i]; 707 | 708 | var color = s.color || colors[i]; 709 | var backgroundColor = chartType !== "line" ? addOpacity(color, 0.5) : color; 710 | 711 | var dataset = { 712 | label: s.name || "", 713 | data: rows2[i], 714 | fill: chartType === "area", 715 | borderColor: color, 716 | backgroundColor: backgroundColor, 717 | pointBackgroundColor: color, 718 | borderWidth: 2, 719 | pointHoverBackgroundColor: color 720 | }; 721 | 722 | if (s.stack) { 723 | dataset.stack = s.stack; 724 | } 725 | 726 | var curve = seriesOption(chart, s, "curve"); 727 | if (curve === false) { 728 | dataset.lineTension = 0; 729 | } 730 | 731 | var points = seriesOption(chart, s, "points"); 732 | if (points === false) { 733 | dataset.pointRadius = 0; 734 | dataset.pointHitRadius = 5; 735 | } 736 | 737 | dataset = merge(dataset, chart.options.dataset || {}); 738 | dataset = merge(dataset, s.library || {}); 739 | dataset = merge(dataset, s.dataset || {}); 740 | 741 | datasets.push(dataset); 742 | } 743 | 744 | var xmin = chart.options.xmin; 745 | var xmax = chart.options.xmax; 746 | 747 | if (chart.xtype === "datetime") { 748 | // hacky check for Chart.js >= 2.9.0 749 | // https://github.com/chartjs/Chart.js/compare/v2.8.0...v2.9.0 750 | var gte29 = "math" in library.helpers; 751 | var ticksKey = gte29 ? "ticks" : "time"; 752 | if (notnull(xmin)) { 753 | options.scales.xAxes[0][ticksKey].min = toDate(xmin).getTime(); 754 | } 755 | if (notnull(xmax)) { 756 | options.scales.xAxes[0][ticksKey].max = toDate(xmax).getTime(); 757 | } 758 | } else if (chart.xtype === "number") { 759 | if (notnull(xmin)) { 760 | options.scales.xAxes[0].ticks.min = xmin; 761 | } 762 | if (notnull(xmax)) { 763 | options.scales.xAxes[0].ticks.max = xmax; 764 | } 765 | } 766 | 767 | // for empty datetime chart 768 | if (chart.xtype === "datetime" && labels.length === 0) { 769 | if (notnull(xmin)) { 770 | labels.push(toDate(xmin)); 771 | } 772 | if (notnull(xmax)) { 773 | labels.push(toDate(xmax)); 774 | } 775 | day = false; 776 | week = false; 777 | month = false; 778 | year = false; 779 | hour = false; 780 | minute = false; 781 | } 782 | 783 | if (chart.xtype === "datetime" && labels.length > 0) { 784 | var minTime = (notnull(xmin) ? toDate(xmin) : labels[0]).getTime(); 785 | var maxTime = (notnull(xmax) ? toDate(xmax) : labels[0]).getTime(); 786 | 787 | for (i = 1; i < labels.length; i++) { 788 | var value$1 = labels[i].getTime(); 789 | if (value$1 < minTime) { 790 | minTime = value$1; 791 | } 792 | if (value$1 > maxTime) { 793 | maxTime = value$1; 794 | } 795 | } 796 | 797 | var timeDiff = (maxTime - minTime) / (86400 * 1000.0); 798 | 799 | if (!options.scales.xAxes[0].time.unit) { 800 | var step; 801 | if (year || timeDiff > 365 * 10) { 802 | options.scales.xAxes[0].time.unit = "year"; 803 | step = 365; 804 | } else if (month || timeDiff > 30 * 10) { 805 | options.scales.xAxes[0].time.unit = "month"; 806 | step = 30; 807 | } else if (day || timeDiff > 10) { 808 | options.scales.xAxes[0].time.unit = "day"; 809 | step = 1; 810 | } else if (hour || timeDiff > 0.5) { 811 | options.scales.xAxes[0].time.displayFormats = {hour: "MMM D, h a"}; 812 | options.scales.xAxes[0].time.unit = "hour"; 813 | step = 1 / 24.0; 814 | } else if (minute) { 815 | options.scales.xAxes[0].time.displayFormats = {minute: "h:mm a"}; 816 | options.scales.xAxes[0].time.unit = "minute"; 817 | step = 1 / 24.0 / 60.0; 818 | } 819 | 820 | if (step && timeDiff > 0) { 821 | var unitStepSize = Math.ceil(timeDiff / step / (chart.element.offsetWidth / 100.0)); 822 | if (week && step === 1) { 823 | unitStepSize = Math.ceil(unitStepSize / 7.0) * 7; 824 | } 825 | options.scales.xAxes[0].time.unitStepSize = unitStepSize; 826 | } 827 | } 828 | 829 | if (!options.scales.xAxes[0].time.tooltipFormat) { 830 | if (day) { 831 | options.scales.xAxes[0].time.tooltipFormat = "ll"; 832 | } else if (hour) { 833 | options.scales.xAxes[0].time.tooltipFormat = "MMM D, h a"; 834 | } else if (minute) { 835 | options.scales.xAxes[0].time.tooltipFormat = "h:mm a"; 836 | } 837 | } 838 | } 839 | 840 | var data = { 841 | labels: labels, 842 | datasets: datasets 843 | }; 844 | 845 | return data; 846 | }; 847 | 848 | var defaultExport = function defaultExport(library) { 849 | this.name = "chartjs"; 850 | this.library = library; 851 | }; 852 | 853 | defaultExport.prototype.renderLineChart = function renderLineChart (chart, chartType) { 854 | var chartOptions = {}; 855 | // fix for https://github.com/chartjs/Chart.js/issues/2441 856 | if (!chart.options.max && allZeros(chart.data)) { 857 | chartOptions.max = 1; 858 | } 859 | 860 | var options = jsOptions(chart, merge(chartOptions, chart.options)); 861 | setFormatOptions(chart, options, chartType); 862 | 863 | var data = createDataTable(chart, options, chartType || "line", this.library); 864 | 865 | if (chart.xtype === "number") { 866 | options.scales.xAxes[0].type = "linear"; 867 | options.scales.xAxes[0].position = "bottom"; 868 | } else { 869 | options.scales.xAxes[0].type = chart.xtype === "string" ? "category" : "time"; 870 | } 871 | 872 | this.drawChart(chart, "line", data, options); 873 | }; 874 | 875 | defaultExport.prototype.renderPieChart = function renderPieChart (chart) { 876 | var options = merge({}, baseOptions); 877 | if (chart.options.donut) { 878 | options.cutoutPercentage = 50; 879 | } 880 | 881 | if ("legend" in chart.options) { 882 | hideLegend(options, chart.options.legend); 883 | } 884 | 885 | if (chart.options.title) { 886 | setTitle(options, chart.options.title); 887 | } 888 | 889 | options = merge(options, chart.options.library || {}); 890 | setFormatOptions(chart, options, "pie"); 891 | 892 | var labels = []; 893 | var values = []; 894 | for (var i = 0; i < chart.data.length; i++) { 895 | var point = chart.data[i]; 896 | labels.push(point[0]); 897 | values.push(point[1]); 898 | } 899 | 900 | var dataset = { 901 | data: values, 902 | backgroundColor: chart.options.colors || defaultColors 903 | }; 904 | dataset = merge(dataset, chart.options.dataset || {}); 905 | 906 | var data = { 907 | labels: labels, 908 | datasets: [dataset] 909 | }; 910 | 911 | this.drawChart(chart, "pie", data, options); 912 | }; 913 | 914 | defaultExport.prototype.renderColumnChart = function renderColumnChart (chart, chartType) { 915 | var options; 916 | if (chartType === "bar") { 917 | var barOptions = merge(baseOptions, defaultOptions); 918 | delete barOptions.scales.yAxes[0].ticks.maxTicksLimit; 919 | options = jsOptionsFunc(barOptions, hideLegend, setTitle, setBarMin, setBarMax, setStacked, setXtitle, setYtitle)(chart, chart.options); 920 | } else { 921 | options = jsOptions(chart, chart.options); 922 | } 923 | setFormatOptions(chart, options, chartType); 924 | var data = createDataTable(chart, options, "column", this.library); 925 | if (chartType !== "bar") { 926 | setLabelSize(chart, data, options); 927 | } 928 | this.drawChart(chart, (chartType === "bar" ? "horizontalBar" : "bar"), data, options); 929 | }; 930 | 931 | defaultExport.prototype.renderAreaChart = function renderAreaChart (chart) { 932 | this.renderLineChart(chart, "area"); 933 | }; 934 | 935 | defaultExport.prototype.renderBarChart = function renderBarChart (chart) { 936 | this.renderColumnChart(chart, "bar"); 937 | }; 938 | 939 | defaultExport.prototype.renderScatterChart = function renderScatterChart (chart, chartType) { 940 | chartType = chartType || "scatter"; 941 | 942 | var options = jsOptions(chart, chart.options); 943 | setFormatOptions(chart, options, chartType); 944 | 945 | if (!("showLines" in options)) { 946 | options.showLines = false; 947 | } 948 | 949 | var data = createDataTable(chart, options, chartType, this.library); 950 | 951 | options.scales.xAxes[0].type = "linear"; 952 | options.scales.xAxes[0].position = "bottom"; 953 | 954 | this.drawChart(chart, chartType, data, options); 955 | }; 956 | 957 | defaultExport.prototype.renderBubbleChart = function renderBubbleChart (chart) { 958 | this.renderScatterChart(chart, "bubble"); 959 | }; 960 | 961 | defaultExport.prototype.destroy = function destroy (chart) { 962 | if (chart.chart) { 963 | chart.chart.destroy(); 964 | } 965 | }; 966 | 967 | defaultExport.prototype.drawChart = function drawChart (chart, type, data, options) { 968 | this.destroy(chart); 969 | 970 | var chartOptions = { 971 | type: type, 972 | data: data, 973 | options: options 974 | }; 975 | 976 | if (chart.options.code) { 977 | window.console.log("new Chart(ctx, " + JSON.stringify(chartOptions) + ");"); 978 | } 979 | 980 | chart.element.innerHTML = ""; 981 | var ctx = chart.element.getElementsByTagName("CANVAS")[0]; 982 | chart.chart = new this.library(ctx, chartOptions); 983 | }; 984 | 985 | var defaultOptions$1 = { 986 | chart: {}, 987 | xAxis: { 988 | title: { 989 | text: null 990 | }, 991 | labels: { 992 | style: { 993 | fontSize: "12px" 994 | } 995 | } 996 | }, 997 | yAxis: { 998 | title: { 999 | text: null 1000 | }, 1001 | labels: { 1002 | style: { 1003 | fontSize: "12px" 1004 | } 1005 | } 1006 | }, 1007 | title: { 1008 | text: null 1009 | }, 1010 | credits: { 1011 | enabled: false 1012 | }, 1013 | legend: { 1014 | borderWidth: 0 1015 | }, 1016 | tooltip: { 1017 | style: { 1018 | fontSize: "12px" 1019 | } 1020 | }, 1021 | plotOptions: { 1022 | areaspline: {}, 1023 | area: {}, 1024 | series: { 1025 | marker: {} 1026 | } 1027 | } 1028 | }; 1029 | 1030 | var hideLegend$1 = function (options, legend, hideLegend) { 1031 | if (legend !== undefined) { 1032 | options.legend.enabled = !!legend; 1033 | if (legend && legend !== true) { 1034 | if (legend === "top" || legend === "bottom") { 1035 | options.legend.verticalAlign = legend; 1036 | } else { 1037 | options.legend.layout = "vertical"; 1038 | options.legend.verticalAlign = "middle"; 1039 | options.legend.align = legend; 1040 | } 1041 | } 1042 | } else if (hideLegend) { 1043 | options.legend.enabled = false; 1044 | } 1045 | }; 1046 | 1047 | var setTitle$1 = function (options, title) { 1048 | options.title.text = title; 1049 | }; 1050 | 1051 | var setMin$1 = function (options, min) { 1052 | options.yAxis.min = min; 1053 | }; 1054 | 1055 | var setMax$1 = function (options, max) { 1056 | options.yAxis.max = max; 1057 | }; 1058 | 1059 | var setStacked$1 = function (options, stacked) { 1060 | var stackedValue = stacked ? (stacked === true ? "normal" : stacked) : null; 1061 | options.plotOptions.series.stacking = stackedValue; 1062 | options.plotOptions.area.stacking = stackedValue; 1063 | options.plotOptions.areaspline.stacking = stackedValue; 1064 | }; 1065 | 1066 | var setXtitle$1 = function (options, title) { 1067 | options.xAxis.title.text = title; 1068 | }; 1069 | 1070 | var setYtitle$1 = function (options, title) { 1071 | options.yAxis.title.text = title; 1072 | }; 1073 | 1074 | var jsOptions$1 = jsOptionsFunc(defaultOptions$1, hideLegend$1, setTitle$1, setMin$1, setMax$1, setStacked$1, setXtitle$1, setYtitle$1); 1075 | 1076 | var setFormatOptions$1 = function(chart, options, chartType) { 1077 | var formatOptions = { 1078 | prefix: chart.options.prefix, 1079 | suffix: chart.options.suffix, 1080 | thousands: chart.options.thousands, 1081 | decimal: chart.options.decimal, 1082 | precision: chart.options.precision, 1083 | round: chart.options.round, 1084 | zeros: chart.options.zeros 1085 | }; 1086 | 1087 | if (chartType !== "pie" && !options.yAxis.labels.formatter) { 1088 | options.yAxis.labels.formatter = function () { 1089 | return formatValue("", this.value, formatOptions); 1090 | }; 1091 | } 1092 | 1093 | if (!options.tooltip.pointFormatter) { 1094 | options.tooltip.pointFormatter = function () { 1095 | return '\u25CF ' + formatValue(this.series.name + ': ', this.y, formatOptions) + '
'; 1096 | }; 1097 | } 1098 | }; 1099 | 1100 | var defaultExport$1 = function defaultExport(library) { 1101 | this.name = "highcharts"; 1102 | this.library = library; 1103 | }; 1104 | 1105 | defaultExport$1.prototype.renderLineChart = function renderLineChart (chart, chartType) { 1106 | chartType = chartType || "spline"; 1107 | var chartOptions = {}; 1108 | if (chartType === "areaspline") { 1109 | chartOptions = { 1110 | plotOptions: { 1111 | areaspline: { 1112 | stacking: "normal" 1113 | }, 1114 | area: { 1115 | stacking: "normal" 1116 | }, 1117 | series: { 1118 | marker: { 1119 | enabled: false 1120 | } 1121 | } 1122 | } 1123 | }; 1124 | } 1125 | 1126 | if (chart.options.curve === false) { 1127 | if (chartType === "areaspline") { 1128 | chartType = "area"; 1129 | } else if (chartType === "spline") { 1130 | chartType = "line"; 1131 | } 1132 | } 1133 | 1134 | var options = jsOptions$1(chart, chart.options, chartOptions), data, i, j; 1135 | options.xAxis.type = chart.xtype === "string" ? "category" : (chart.xtype === "number" ? "linear" : "datetime"); 1136 | if (!options.chart.type) { 1137 | options.chart.type = chartType; 1138 | } 1139 | setFormatOptions$1(chart, options, chartType); 1140 | 1141 | var series = chart.data; 1142 | for (i = 0; i < series.length; i++) { 1143 | series[i].name = series[i].name || "Value"; 1144 | data = series[i].data; 1145 | if (chart.xtype === "datetime") { 1146 | for (j = 0; j < data.length; j++) { 1147 | data[j][0] = data[j][0].getTime(); 1148 | } 1149 | } 1150 | series[i].marker = {symbol: "circle"}; 1151 | if (chart.options.points === false) { 1152 | series[i].marker.enabled = false; 1153 | } 1154 | } 1155 | 1156 | this.drawChart(chart, series, options); 1157 | }; 1158 | 1159 | defaultExport$1.prototype.renderScatterChart = function renderScatterChart (chart) { 1160 | var options = jsOptions$1(chart, chart.options, {}); 1161 | options.chart.type = "scatter"; 1162 | this.drawChart(chart, chart.data, options); 1163 | }; 1164 | 1165 | defaultExport$1.prototype.renderPieChart = function renderPieChart (chart) { 1166 | var chartOptions = merge(defaultOptions$1, {}); 1167 | 1168 | if (chart.options.colors) { 1169 | chartOptions.colors = chart.options.colors; 1170 | } 1171 | if (chart.options.donut) { 1172 | chartOptions.plotOptions = {pie: {innerSize: "50%"}}; 1173 | } 1174 | 1175 | if ("legend" in chart.options) { 1176 | hideLegend$1(chartOptions, chart.options.legend); 1177 | } 1178 | 1179 | if (chart.options.title) { 1180 | setTitle$1(chartOptions, chart.options.title); 1181 | } 1182 | 1183 | var options = merge(chartOptions, chart.options.library || {}); 1184 | setFormatOptions$1(chart, options, "pie"); 1185 | var series = [{ 1186 | type: "pie", 1187 | name: chart.options.label || "Value", 1188 | data: chart.data 1189 | }]; 1190 | 1191 | this.drawChart(chart, series, options); 1192 | }; 1193 | 1194 | defaultExport$1.prototype.renderColumnChart = function renderColumnChart (chart, chartType) { 1195 | chartType = chartType || "column"; 1196 | var series = chart.data; 1197 | var options = jsOptions$1(chart, chart.options), i, j, s, d, rows = [], categories = []; 1198 | options.chart.type = chartType; 1199 | setFormatOptions$1(chart, options, chartType); 1200 | 1201 | for (i = 0; i < series.length; i++) { 1202 | s = series[i]; 1203 | 1204 | for (j = 0; j < s.data.length; j++) { 1205 | d = s.data[j]; 1206 | if (!rows[d[0]]) { 1207 | rows[d[0]] = new Array(series.length); 1208 | categories.push(d[0]); 1209 | } 1210 | rows[d[0]][i] = d[1]; 1211 | } 1212 | } 1213 | 1214 | if (chart.xtype === "number") { 1215 | categories.sort(sortByNumber); 1216 | } 1217 | 1218 | options.xAxis.categories = categories; 1219 | 1220 | var newSeries = [], d2; 1221 | for (i = 0; i < series.length; i++) { 1222 | d = []; 1223 | for (j = 0; j < categories.length; j++) { 1224 | d.push(rows[categories[j]][i] || 0); 1225 | } 1226 | 1227 | d2 = { 1228 | name: series[i].name || "Value", 1229 | data: d 1230 | }; 1231 | if (series[i].stack) { 1232 | d2.stack = series[i].stack; 1233 | } 1234 | 1235 | newSeries.push(d2); 1236 | } 1237 | 1238 | this.drawChart(chart, newSeries, options); 1239 | }; 1240 | 1241 | defaultExport$1.prototype.renderBarChart = function renderBarChart (chart) { 1242 | this.renderColumnChart(chart, "bar"); 1243 | }; 1244 | 1245 | defaultExport$1.prototype.renderAreaChart = function renderAreaChart (chart) { 1246 | this.renderLineChart(chart, "areaspline"); 1247 | }; 1248 | 1249 | defaultExport$1.prototype.destroy = function destroy (chart) { 1250 | if (chart.chart) { 1251 | chart.chart.destroy(); 1252 | } 1253 | }; 1254 | 1255 | defaultExport$1.prototype.drawChart = function drawChart (chart, data, options) { 1256 | this.destroy(chart); 1257 | 1258 | options.chart.renderTo = chart.element.id; 1259 | options.series = data; 1260 | 1261 | if (chart.options.code) { 1262 | window.console.log("new Highcharts.Chart(" + JSON.stringify(options) + ");"); 1263 | } 1264 | 1265 | chart.chart = new this.library.Chart(options); 1266 | }; 1267 | 1268 | var loaded = {}; 1269 | var callbacks = []; 1270 | 1271 | // Set chart options 1272 | var defaultOptions$2 = { 1273 | chartArea: {}, 1274 | fontName: "'Lucida Grande', 'Lucida Sans Unicode', Verdana, Arial, Helvetica, sans-serif", 1275 | pointSize: 6, 1276 | legend: { 1277 | textStyle: { 1278 | fontSize: 12, 1279 | color: "#444" 1280 | }, 1281 | alignment: "center", 1282 | position: "right" 1283 | }, 1284 | curveType: "function", 1285 | hAxis: { 1286 | textStyle: { 1287 | color: "#666", 1288 | fontSize: 12 1289 | }, 1290 | titleTextStyle: {}, 1291 | gridlines: { 1292 | color: "transparent" 1293 | }, 1294 | baselineColor: "#ccc", 1295 | viewWindow: {} 1296 | }, 1297 | vAxis: { 1298 | textStyle: { 1299 | color: "#666", 1300 | fontSize: 12 1301 | }, 1302 | titleTextStyle: {}, 1303 | baselineColor: "#ccc", 1304 | viewWindow: {} 1305 | }, 1306 | tooltip: { 1307 | textStyle: { 1308 | color: "#666", 1309 | fontSize: 12 1310 | } 1311 | } 1312 | }; 1313 | 1314 | var hideLegend$2 = function (options, legend, hideLegend) { 1315 | if (legend !== undefined) { 1316 | var position; 1317 | if (!legend) { 1318 | position = "none"; 1319 | } else if (legend === true) { 1320 | position = "right"; 1321 | } else { 1322 | position = legend; 1323 | } 1324 | options.legend.position = position; 1325 | } else if (hideLegend) { 1326 | options.legend.position = "none"; 1327 | } 1328 | }; 1329 | 1330 | var setTitle$2 = function (options, title) { 1331 | options.title = title; 1332 | options.titleTextStyle = {color: "#333", fontSize: "20px"}; 1333 | }; 1334 | 1335 | var setMin$2 = function (options, min) { 1336 | options.vAxis.viewWindow.min = min; 1337 | }; 1338 | 1339 | var setMax$2 = function (options, max) { 1340 | options.vAxis.viewWindow.max = max; 1341 | }; 1342 | 1343 | var setBarMin$1 = function (options, min) { 1344 | options.hAxis.viewWindow.min = min; 1345 | }; 1346 | 1347 | var setBarMax$1 = function (options, max) { 1348 | options.hAxis.viewWindow.max = max; 1349 | }; 1350 | 1351 | var setStacked$2 = function (options, stacked) { 1352 | options.isStacked = stacked ? stacked : false; 1353 | }; 1354 | 1355 | var setXtitle$2 = function (options, title) { 1356 | options.hAxis.title = title; 1357 | options.hAxis.titleTextStyle.italic = false; 1358 | }; 1359 | 1360 | var setYtitle$2 = function (options, title) { 1361 | options.vAxis.title = title; 1362 | options.vAxis.titleTextStyle.italic = false; 1363 | }; 1364 | 1365 | var jsOptions$2 = jsOptionsFunc(defaultOptions$2, hideLegend$2, setTitle$2, setMin$2, setMax$2, setStacked$2, setXtitle$2, setYtitle$2); 1366 | 1367 | var resize = function (callback) { 1368 | if (window.attachEvent) { 1369 | window.attachEvent("onresize", callback); 1370 | } else if (window.addEventListener) { 1371 | window.addEventListener("resize", callback, true); 1372 | } 1373 | callback(); 1374 | }; 1375 | 1376 | var defaultExport$2 = function defaultExport(library) { 1377 | this.name = "google"; 1378 | this.library = library; 1379 | }; 1380 | 1381 | defaultExport$2.prototype.renderLineChart = function renderLineChart (chart) { 1382 | var this$1 = this; 1383 | 1384 | this.waitForLoaded(chart, function () { 1385 | var chartOptions = {}; 1386 | 1387 | if (chart.options.curve === false) { 1388 | chartOptions.curveType = "none"; 1389 | } 1390 | 1391 | if (chart.options.points === false) { 1392 | chartOptions.pointSize = 0; 1393 | } 1394 | 1395 | var options = jsOptions$2(chart, chart.options, chartOptions); 1396 | var data = this$1.createDataTable(chart.data, chart.xtype); 1397 | 1398 | this$1.drawChart(chart, "LineChart", data, options); 1399 | }); 1400 | }; 1401 | 1402 | defaultExport$2.prototype.renderPieChart = function renderPieChart (chart) { 1403 | var this$1 = this; 1404 | 1405 | this.waitForLoaded(chart, function () { 1406 | var chartOptions = { 1407 | chartArea: { 1408 | top: "10%", 1409 | height: "80%" 1410 | }, 1411 | legend: {} 1412 | }; 1413 | if (chart.options.colors) { 1414 | chartOptions.colors = chart.options.colors; 1415 | } 1416 | if (chart.options.donut) { 1417 | chartOptions.pieHole = 0.5; 1418 | } 1419 | if ("legend" in chart.options) { 1420 | hideLegend$2(chartOptions, chart.options.legend); 1421 | } 1422 | if (chart.options.title) { 1423 | setTitle$2(chartOptions, chart.options.title); 1424 | } 1425 | var options = merge(merge(defaultOptions$2, chartOptions), chart.options.library || {}); 1426 | 1427 | var data = new this$1.library.visualization.DataTable(); 1428 | data.addColumn("string", ""); 1429 | data.addColumn("number", "Value"); 1430 | data.addRows(chart.data); 1431 | 1432 | this$1.drawChart(chart, "PieChart", data, options); 1433 | }); 1434 | }; 1435 | 1436 | defaultExport$2.prototype.renderColumnChart = function renderColumnChart (chart) { 1437 | var this$1 = this; 1438 | 1439 | this.waitForLoaded(chart, function () { 1440 | var options = jsOptions$2(chart, chart.options); 1441 | var data = this$1.createDataTable(chart.data, chart.xtype); 1442 | 1443 | this$1.drawChart(chart, "ColumnChart", data, options); 1444 | }); 1445 | }; 1446 | 1447 | defaultExport$2.prototype.renderBarChart = function renderBarChart (chart) { 1448 | var this$1 = this; 1449 | 1450 | this.waitForLoaded(chart, function () { 1451 | var chartOptions = { 1452 | hAxis: { 1453 | gridlines: { 1454 | color: "#ccc" 1455 | } 1456 | } 1457 | }; 1458 | var options = jsOptionsFunc(defaultOptions$2, hideLegend$2, setTitle$2, setBarMin$1, setBarMax$1, setStacked$2, setXtitle$2, setYtitle$2)(chart, chart.options, chartOptions); 1459 | var data = this$1.createDataTable(chart.data, chart.xtype); 1460 | 1461 | this$1.drawChart(chart, "BarChart", data, options); 1462 | }); 1463 | }; 1464 | 1465 | defaultExport$2.prototype.renderAreaChart = function renderAreaChart (chart) { 1466 | var this$1 = this; 1467 | 1468 | this.waitForLoaded(chart, function () { 1469 | var chartOptions = { 1470 | isStacked: true, 1471 | pointSize: 0, 1472 | areaOpacity: 0.5 1473 | }; 1474 | 1475 | var options = jsOptions$2(chart, chart.options, chartOptions); 1476 | var data = this$1.createDataTable(chart.data, chart.xtype); 1477 | 1478 | this$1.drawChart(chart, "AreaChart", data, options); 1479 | }); 1480 | }; 1481 | 1482 | defaultExport$2.prototype.renderGeoChart = function renderGeoChart (chart) { 1483 | var this$1 = this; 1484 | 1485 | this.waitForLoaded(chart, "geochart", function () { 1486 | var chartOptions = { 1487 | legend: "none", 1488 | colorAxis: { 1489 | colors: chart.options.colors || ["#f6c7b6", "#ce502d"] 1490 | } 1491 | }; 1492 | var options = merge(merge(defaultOptions$2, chartOptions), chart.options.library || {}); 1493 | 1494 | var data = new this$1.library.visualization.DataTable(); 1495 | data.addColumn("string", ""); 1496 | data.addColumn("number", chart.options.label || "Value"); 1497 | data.addRows(chart.data); 1498 | 1499 | this$1.drawChart(chart, "GeoChart", data, options); 1500 | }); 1501 | }; 1502 | 1503 | defaultExport$2.prototype.renderScatterChart = function renderScatterChart (chart) { 1504 | var this$1 = this; 1505 | 1506 | this.waitForLoaded(chart, function () { 1507 | var chartOptions = {}; 1508 | var options = jsOptions$2(chart, chart.options, chartOptions); 1509 | 1510 | var series = chart.data, rows2 = [], i, j, data, d; 1511 | for (i = 0; i < series.length; i++) { 1512 | series[i].name = series[i].name || "Value"; 1513 | d = series[i].data; 1514 | for (j = 0; j < d.length; j++) { 1515 | var row = new Array(series.length + 1); 1516 | row[0] = d[j][0]; 1517 | row[i + 1] = d[j][1]; 1518 | rows2.push(row); 1519 | } 1520 | } 1521 | 1522 | data = new this$1.library.visualization.DataTable(); 1523 | data.addColumn("number", ""); 1524 | for (i = 0; i < series.length; i++) { 1525 | data.addColumn("number", series[i].name); 1526 | } 1527 | data.addRows(rows2); 1528 | 1529 | this$1.drawChart(chart, "ScatterChart", data, options); 1530 | }); 1531 | }; 1532 | 1533 | defaultExport$2.prototype.renderTimeline = function renderTimeline (chart) { 1534 | var this$1 = this; 1535 | 1536 | this.waitForLoaded(chart, "timeline", function () { 1537 | var chartOptions = { 1538 | legend: "none" 1539 | }; 1540 | 1541 | if (chart.options.colors) { 1542 | chartOptions.colors = chart.options.colors; 1543 | } 1544 | var options = merge(merge(defaultOptions$2, chartOptions), chart.options.library || {}); 1545 | 1546 | var data = new this$1.library.visualization.DataTable(); 1547 | data.addColumn({type: "string", id: "Name"}); 1548 | data.addColumn({type: "date", id: "Start"}); 1549 | data.addColumn({type: "date", id: "End"}); 1550 | data.addRows(chart.data); 1551 | 1552 | chart.element.style.lineHeight = "normal"; 1553 | 1554 | this$1.drawChart(chart, "Timeline", data, options); 1555 | }); 1556 | }; 1557 | 1558 | defaultExport$2.prototype.destroy = function destroy (chart) { 1559 | if (chart.chart) { 1560 | chart.chart.clearChart(); 1561 | } 1562 | }; 1563 | 1564 | defaultExport$2.prototype.drawChart = function drawChart (chart, type, data, options) { 1565 | this.destroy(chart); 1566 | 1567 | if (chart.options.code) { 1568 | window.console.log("var data = new google.visualization.DataTable(" + data.toJSON() + ");\nvar chart = new google.visualization." + type + "(element);\nchart.draw(data, " + JSON.stringify(options) + ");"); 1569 | } 1570 | 1571 | chart.chart = new this.library.visualization[type](chart.element); 1572 | resize(function () { 1573 | chart.chart.draw(data, options); 1574 | }); 1575 | }; 1576 | 1577 | defaultExport$2.prototype.waitForLoaded = function waitForLoaded (chart, pack, callback) { 1578 | var this$1 = this; 1579 | 1580 | if (!callback) { 1581 | callback = pack; 1582 | pack = "corechart"; 1583 | } 1584 | 1585 | callbacks.push({pack: pack, callback: callback}); 1586 | 1587 | if (loaded[pack]) { 1588 | this.runCallbacks(); 1589 | } else { 1590 | loaded[pack] = true; 1591 | 1592 | // https://groups.google.com/forum/#!topic/google-visualization-api/fMKJcyA2yyI 1593 | var loadOptions = { 1594 | packages: [pack], 1595 | callback: function () { this$1.runCallbacks(); } 1596 | }; 1597 | var config = chart.__config(); 1598 | if (config.language) { 1599 | loadOptions.language = config.language; 1600 | } 1601 | if (pack === "geochart" && config.mapsApiKey) { 1602 | loadOptions.mapsApiKey = config.mapsApiKey; 1603 | } 1604 | 1605 | this.library.charts.load("current", loadOptions); 1606 | } 1607 | }; 1608 | 1609 | defaultExport$2.prototype.runCallbacks = function runCallbacks () { 1610 | var cb, call; 1611 | for (var i = 0; i < callbacks.length; i++) { 1612 | cb = callbacks[i]; 1613 | call = this.library.visualization && ((cb.pack === "corechart" && this.library.visualization.LineChart) || (cb.pack === "timeline" && this.library.visualization.Timeline) || (cb.pack === "geochart" && this.library.visualization.GeoChart)); 1614 | if (call) { 1615 | cb.callback(); 1616 | callbacks.splice(i, 1); 1617 | i--; 1618 | } 1619 | } 1620 | }; 1621 | 1622 | // cant use object as key 1623 | defaultExport$2.prototype.createDataTable = function createDataTable (series, columnType) { 1624 | var i, j, s, d, key, rows = [], sortedLabels = []; 1625 | for (i = 0; i < series.length; i++) { 1626 | s = series[i]; 1627 | series[i].name = series[i].name || "Value"; 1628 | 1629 | for (j = 0; j < s.data.length; j++) { 1630 | d = s.data[j]; 1631 | key = (columnType === "datetime") ? d[0].getTime() : d[0]; 1632 | if (!rows[key]) { 1633 | rows[key] = new Array(series.length); 1634 | sortedLabels.push(key); 1635 | } 1636 | rows[key][i] = toFloat(d[1]); 1637 | } 1638 | } 1639 | 1640 | var rows2 = []; 1641 | var day = true; 1642 | var value; 1643 | for (j = 0; j < sortedLabels.length; j++) { 1644 | i = sortedLabels[j]; 1645 | if (columnType === "datetime") { 1646 | value = new Date(toFloat(i)); 1647 | day = day && isDay(value); 1648 | } else if (columnType === "number") { 1649 | value = toFloat(i); 1650 | } else { 1651 | value = i; 1652 | } 1653 | rows2.push([value].concat(rows[i])); 1654 | } 1655 | if (columnType === "datetime") { 1656 | rows2.sort(sortByTime); 1657 | } else if (columnType === "number") { 1658 | rows2.sort(sortByNumberSeries); 1659 | 1660 | for (i = 0; i < rows2.length; i++) { 1661 | rows2[i][0] = toStr(rows2[i][0]); 1662 | } 1663 | 1664 | columnType = "string"; 1665 | } 1666 | 1667 | // create datatable 1668 | var data = new this.library.visualization.DataTable(); 1669 | columnType = columnType === "datetime" && day ? "date" : columnType; 1670 | data.addColumn(columnType, ""); 1671 | for (i = 0; i < series.length; i++) { 1672 | data.addColumn("number", series[i].name); 1673 | } 1674 | data.addRows(rows2); 1675 | 1676 | return data; 1677 | }; 1678 | 1679 | var pendingRequests = [], runningRequests = 0, maxRequests = 4; 1680 | 1681 | function pushRequest(url, success, error) { 1682 | pendingRequests.push([url, success, error]); 1683 | runNext(); 1684 | } 1685 | 1686 | function runNext() { 1687 | if (runningRequests < maxRequests) { 1688 | var request = pendingRequests.shift(); 1689 | if (request) { 1690 | runningRequests++; 1691 | getJSON(request[0], request[1], request[2]); 1692 | runNext(); 1693 | } 1694 | } 1695 | } 1696 | 1697 | function requestComplete() { 1698 | runningRequests--; 1699 | runNext(); 1700 | } 1701 | 1702 | function getJSON(url, success, error) { 1703 | ajaxCall(url, success, function (jqXHR, textStatus, errorThrown) { 1704 | var message = (typeof errorThrown === "string") ? errorThrown : errorThrown.message; 1705 | error(message); 1706 | }); 1707 | } 1708 | 1709 | function ajaxCall(url, success, error) { 1710 | var $ = window.jQuery || window.Zepto || window.$; 1711 | 1712 | if ($ && $.ajax) { 1713 | $.ajax({ 1714 | dataType: "json", 1715 | url: url, 1716 | success: success, 1717 | error: error, 1718 | complete: requestComplete 1719 | }); 1720 | } else { 1721 | var xhr = new XMLHttpRequest(); 1722 | xhr.open("GET", url, true); 1723 | xhr.setRequestHeader("Content-Type", "application/json"); 1724 | xhr.onload = function () { 1725 | requestComplete(); 1726 | if (xhr.status === 200) { 1727 | success(JSON.parse(xhr.responseText), xhr.statusText, xhr); 1728 | } else { 1729 | error(xhr, "error", xhr.statusText); 1730 | } 1731 | }; 1732 | xhr.send(); 1733 | } 1734 | } 1735 | 1736 | var config = {}; 1737 | var adapters = []; 1738 | 1739 | // helpers 1740 | 1741 | function setText(element, text) { 1742 | if (document.body.innerText) { 1743 | element.innerText = text; 1744 | } else { 1745 | element.textContent = text; 1746 | } 1747 | } 1748 | 1749 | // TODO remove prefix for all messages 1750 | function chartError(element, message, noPrefix) { 1751 | if (!noPrefix) { 1752 | message = "Error Loading Chart: " + message; 1753 | } 1754 | setText(element, message); 1755 | element.style.color = "#ff0000"; 1756 | } 1757 | 1758 | function errorCatcher(chart) { 1759 | try { 1760 | chart.__render(); 1761 | } catch (err) { 1762 | chartError(chart.element, err.message); 1763 | throw err; 1764 | } 1765 | } 1766 | 1767 | function fetchDataSource(chart, dataSource) { 1768 | if (typeof dataSource === "string") { 1769 | pushRequest(dataSource, function (data) { 1770 | chart.rawData = data; 1771 | errorCatcher(chart); 1772 | }, function (message) { 1773 | chartError(chart.element, message); 1774 | }); 1775 | } else if (typeof dataSource === "function") { 1776 | try { 1777 | dataSource(function (data) { 1778 | chart.rawData = data; 1779 | errorCatcher(chart); 1780 | }, function (message) { 1781 | chartError(chart.element, message, true); 1782 | }); 1783 | } catch (err) { 1784 | chartError(chart.element, err, true); 1785 | } 1786 | } else { 1787 | chart.rawData = dataSource; 1788 | errorCatcher(chart); 1789 | } 1790 | } 1791 | 1792 | function addDownloadButton(chart) { 1793 | var element = chart.element; 1794 | var link = document.createElement("a"); 1795 | 1796 | var download = chart.options.download; 1797 | if (download === true) { 1798 | download = {}; 1799 | } else if (typeof download === "string") { 1800 | download = {filename: download}; 1801 | } 1802 | link.download = download.filename || "chart.png"; // https://caniuse.com/download 1803 | 1804 | link.style.position = "absolute"; 1805 | link.style.top = "20px"; 1806 | link.style.right = "20px"; 1807 | link.style.zIndex = 1000; 1808 | link.style.lineHeight = "20px"; 1809 | link.target = "_blank"; // for safari 1810 | var image = document.createElement("img"); 1811 | image.alt = "Download"; 1812 | image.style.border = "none"; 1813 | // icon from font-awesome 1814 | // http://fa2png.io/ 1815 | image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAABCFBMVEUAAADMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMywEsqxAAAAV3RSTlMAAQIDBggJCgsMDQ4PERQaHB0eISIjJCouLzE0OTo/QUJHSUpLTU5PUllhYmltcHh5foWLjI+SlaCio6atr7S1t7m6vsHHyM7R2tze5Obo7fHz9ff5+/1hlxK2AAAA30lEQVQYGUXBhVYCQQBA0TdYWAt2d3d3YWAHyur7/z9xgD16Lw0DW+XKx+1GgX+FRzM3HWQWrHl5N/oapW5RPe0PkBu+UYeICvozTWZVK23Ao04B79oJrOsJDOoxkZoQPWgX29pHpCZEk7rEvQYiNSFq1UMqvlCjJkRBS1R8hb00Vb/TajtBL7nTHE1X1vyMQF732dQhyF2o6SAwrzP06iUQzvwsArlnzcOdrgBhJyHa1QOgO9U1GsKuvjUTjavliZYQ8nNPapG6sap/3nrIdJ6bOWzmX/fy0XVpfzZP3S8OJT3g9EEiJwAAAABJRU5ErkJggg=="; 1816 | link.appendChild(image); 1817 | element.style.position = "relative"; 1818 | 1819 | chart.__downloadAttached = true; 1820 | 1821 | // mouseenter 1822 | chart.__enterEvent = addEvent(element, "mouseover", function(e) { 1823 | var related = e.relatedTarget; 1824 | // check download option again to ensure it wasn't changed 1825 | if ((!related || (related !== this && !childOf(this, related))) && chart.options.download) { 1826 | link.href = chart.toImage(download); 1827 | element.appendChild(link); 1828 | } 1829 | }); 1830 | 1831 | // mouseleave 1832 | chart.__leaveEvent = addEvent(element, "mouseout", function(e) { 1833 | var related = e.relatedTarget; 1834 | if (!related || (related !== this && !childOf(this, related))) { 1835 | if (link.parentNode) { 1836 | link.parentNode.removeChild(link); 1837 | } 1838 | } 1839 | }); 1840 | } 1841 | 1842 | // https://stackoverflow.com/questions/10149963/adding-event-listener-cross-browser 1843 | function addEvent(elem, event, fn) { 1844 | if (elem.addEventListener) { 1845 | elem.addEventListener(event, fn, false); 1846 | return fn; 1847 | } else { 1848 | var fn2 = function() { 1849 | // set the this pointer same as addEventListener when fn is called 1850 | return(fn.call(elem, window.event)); 1851 | }; 1852 | elem.attachEvent("on" + event, fn2); 1853 | return fn2; 1854 | } 1855 | } 1856 | 1857 | function removeEvent(elem, event, fn) { 1858 | if (elem.removeEventListener) { 1859 | elem.removeEventListener(event, fn, false); 1860 | } else { 1861 | elem.detachEvent("on" + event, fn); 1862 | } 1863 | } 1864 | 1865 | // https://gist.github.com/shawnbot/4166283 1866 | function childOf(p, c) { 1867 | if (p === c) { return false; } 1868 | while (c && c !== p) { c = c.parentNode; } 1869 | return c === p; 1870 | } 1871 | 1872 | function getAdapterType(library) { 1873 | if (library) { 1874 | if (library.product === "Highcharts") { 1875 | return defaultExport$1; 1876 | } else if (library.charts) { 1877 | return defaultExport$2; 1878 | } else if (isFunction(library)) { 1879 | return defaultExport; 1880 | } 1881 | } 1882 | throw new Error("Unknown adapter"); 1883 | } 1884 | 1885 | function addAdapter(library) { 1886 | var adapterType = getAdapterType(library); 1887 | var adapter = new adapterType(library); 1888 | 1889 | if (adapters.indexOf(adapter) === -1) { 1890 | adapters.push(adapter); 1891 | } 1892 | } 1893 | 1894 | function loadAdapters() { 1895 | if ("Chart" in window) { 1896 | addAdapter(window.Chart); 1897 | } 1898 | 1899 | if ("Highcharts" in window) { 1900 | addAdapter(window.Highcharts); 1901 | } 1902 | 1903 | if (window.google && window.google.charts) { 1904 | addAdapter(window.google); 1905 | } 1906 | } 1907 | 1908 | function dataEmpty(data, chartType) { 1909 | if (chartType === "PieChart" || chartType === "GeoChart" || chartType === "Timeline") { 1910 | return data.length === 0; 1911 | } else { 1912 | for (var i = 0; i < data.length; i++) { 1913 | if (data[i].data.length > 0) { 1914 | return false; 1915 | } 1916 | } 1917 | return true; 1918 | } 1919 | } 1920 | 1921 | function renderChart(chartType, chart) { 1922 | if (chart.options.messages && chart.options.messages.empty && dataEmpty(chart.data, chartType)) { 1923 | setText(chart.element, chart.options.messages.empty); 1924 | } else { 1925 | callAdapter(chartType, chart); 1926 | if (chart.options.download && !chart.__downloadAttached && chart.adapter === "chartjs") { 1927 | addDownloadButton(chart); 1928 | } 1929 | } 1930 | } 1931 | 1932 | // TODO remove chartType if cross-browser way 1933 | // to get the name of the chart class 1934 | function callAdapter(chartType, chart) { 1935 | var i, adapter, fnName, adapterName; 1936 | fnName = "render" + chartType; 1937 | adapterName = chart.options.adapter; 1938 | 1939 | loadAdapters(); 1940 | 1941 | for (i = 0; i < adapters.length; i++) { 1942 | adapter = adapters[i]; 1943 | if ((!adapterName || adapterName === adapter.name) && isFunction(adapter[fnName])) { 1944 | chart.adapter = adapter.name; 1945 | chart.__adapterObject = adapter; 1946 | return adapter[fnName](chart); 1947 | } 1948 | } 1949 | 1950 | if (adapters.length > 0) { 1951 | throw new Error("No charting library found for " + chartType); 1952 | } else { 1953 | throw new Error("No charting libraries found - be sure to include one before your charts"); 1954 | } 1955 | } 1956 | 1957 | // process data 1958 | 1959 | var toFormattedKey = function (key, keyType) { 1960 | if (keyType === "number") { 1961 | key = toFloat(key); 1962 | } else if (keyType === "datetime") { 1963 | key = toDate(key); 1964 | } else { 1965 | key = toStr(key); 1966 | } 1967 | return key; 1968 | }; 1969 | 1970 | var formatSeriesData = function (data, keyType) { 1971 | var r = [], key, j; 1972 | for (j = 0; j < data.length; j++) { 1973 | if (keyType === "bubble") { 1974 | r.push([toFloat(data[j][0]), toFloat(data[j][1]), toFloat(data[j][2])]); 1975 | } else { 1976 | key = toFormattedKey(data[j][0], keyType); 1977 | r.push([key, toFloat(data[j][1])]); 1978 | } 1979 | } 1980 | if (keyType === "datetime") { 1981 | r.sort(sortByTime); 1982 | } else if (keyType === "number") { 1983 | r.sort(sortByNumberSeries); 1984 | } 1985 | return r; 1986 | }; 1987 | 1988 | function detectXType(series, noDatetime, options) { 1989 | if (dataEmpty(series)) { 1990 | if ((options.xmin || options.xmax) && (!options.xmin || isDate(options.xmin)) && (!options.xmax || isDate(options.xmax))) { 1991 | return "datetime"; 1992 | } else { 1993 | return "number"; 1994 | } 1995 | } else if (detectXTypeWithFunction(series, isNumber)) { 1996 | return "number"; 1997 | } else if (!noDatetime && detectXTypeWithFunction(series, isDate)) { 1998 | return "datetime"; 1999 | } else { 2000 | return "string"; 2001 | } 2002 | } 2003 | 2004 | function detectXTypeWithFunction(series, func) { 2005 | var i, j, data; 2006 | for (i = 0; i < series.length; i++) { 2007 | data = toArr(series[i].data); 2008 | for (j = 0; j < data.length; j++) { 2009 | if (!func(data[j][0])) { 2010 | return false; 2011 | } 2012 | } 2013 | } 2014 | return true; 2015 | } 2016 | 2017 | // creates a shallow copy of each element of the array 2018 | // elements are expected to be objects 2019 | function copySeries(series) { 2020 | var newSeries = [], i, j; 2021 | for (i = 0; i < series.length; i++) { 2022 | var copy = {}; 2023 | for (j in series[i]) { 2024 | if (series[i].hasOwnProperty(j)) { 2025 | copy[j] = series[i][j]; 2026 | } 2027 | } 2028 | newSeries.push(copy); 2029 | } 2030 | return newSeries; 2031 | } 2032 | 2033 | function processSeries(chart, keyType, noDatetime) { 2034 | var i; 2035 | 2036 | var opts = chart.options; 2037 | var series = chart.rawData; 2038 | 2039 | // see if one series or multiple 2040 | if (!isArray(series) || typeof series[0] !== "object" || isArray(series[0])) { 2041 | series = [{name: opts.label, data: series}]; 2042 | chart.hideLegend = true; 2043 | } else { 2044 | chart.hideLegend = false; 2045 | } 2046 | 2047 | // convert to array 2048 | // must come before dataEmpty check 2049 | series = copySeries(series); 2050 | for (i = 0; i < series.length; i++) { 2051 | series[i].data = toArr(series[i].data); 2052 | } 2053 | 2054 | chart.xtype = keyType ? keyType : (opts.discrete ? "string" : detectXType(series, noDatetime, opts)); 2055 | 2056 | // right format 2057 | for (i = 0; i < series.length; i++) { 2058 | series[i].data = formatSeriesData(series[i].data, chart.xtype); 2059 | } 2060 | 2061 | return series; 2062 | } 2063 | 2064 | function processSimple(chart) { 2065 | var perfectData = toArr(chart.rawData), i; 2066 | for (i = 0; i < perfectData.length; i++) { 2067 | perfectData[i] = [toStr(perfectData[i][0]), toFloat(perfectData[i][1])]; 2068 | } 2069 | return perfectData; 2070 | } 2071 | 2072 | // define classes 2073 | 2074 | var Chart = function Chart(element, dataSource, options) { 2075 | var elementId; 2076 | if (typeof element === "string") { 2077 | elementId = element; 2078 | element = document.getElementById(element); 2079 | if (!element) { 2080 | throw new Error("No element with id " + elementId); 2081 | } 2082 | } 2083 | this.element = element; 2084 | this.options = merge(Chartkick.options, options || {}); 2085 | this.dataSource = dataSource; 2086 | 2087 | Chartkick.charts[element.id] = this; 2088 | 2089 | fetchDataSource(this, dataSource); 2090 | 2091 | if (this.options.refresh) { 2092 | this.startRefresh(); 2093 | } 2094 | }; 2095 | 2096 | Chart.prototype.getElement = function getElement () { 2097 | return this.element; 2098 | }; 2099 | 2100 | Chart.prototype.getDataSource = function getDataSource () { 2101 | return this.dataSource; 2102 | }; 2103 | 2104 | Chart.prototype.getData = function getData () { 2105 | return this.data; 2106 | }; 2107 | 2108 | Chart.prototype.getOptions = function getOptions () { 2109 | return this.options; 2110 | }; 2111 | 2112 | Chart.prototype.getChartObject = function getChartObject () { 2113 | return this.chart; 2114 | }; 2115 | 2116 | Chart.prototype.getAdapter = function getAdapter () { 2117 | return this.adapter; 2118 | }; 2119 | 2120 | Chart.prototype.updateData = function updateData (dataSource, options) { 2121 | this.dataSource = dataSource; 2122 | if (options) { 2123 | this.__updateOptions(options); 2124 | } 2125 | fetchDataSource(this, dataSource); 2126 | }; 2127 | 2128 | Chart.prototype.setOptions = function setOptions (options) { 2129 | this.__updateOptions(options); 2130 | this.redraw(); 2131 | }; 2132 | 2133 | Chart.prototype.redraw = function redraw () { 2134 | fetchDataSource(this, this.rawData); 2135 | }; 2136 | 2137 | Chart.prototype.refreshData = function refreshData () { 2138 | if (typeof this.dataSource === "string") { 2139 | // prevent browser from caching 2140 | var sep = this.dataSource.indexOf("?") === -1 ? "?" : "&"; 2141 | var url = this.dataSource + sep + "_=" + (new Date()).getTime(); 2142 | fetchDataSource(this, url); 2143 | } else if (typeof this.dataSource === "function") { 2144 | fetchDataSource(this, this.dataSource); 2145 | } 2146 | }; 2147 | 2148 | Chart.prototype.startRefresh = function startRefresh () { 2149 | var this$1 = this; 2150 | 2151 | var refresh = this.options.refresh; 2152 | 2153 | if (refresh && typeof this.dataSource !== "string" && typeof this.dataSource !== "function") { 2154 | throw new Error("Data source must be a URL or callback for refresh"); 2155 | } 2156 | 2157 | if (!this.intervalId) { 2158 | if (refresh) { 2159 | this.intervalId = setInterval( function () { 2160 | this$1.refreshData(); 2161 | }, refresh * 1000); 2162 | } else { 2163 | throw new Error("No refresh interval"); 2164 | } 2165 | } 2166 | }; 2167 | 2168 | Chart.prototype.stopRefresh = function stopRefresh () { 2169 | if (this.intervalId) { 2170 | clearInterval(this.intervalId); 2171 | this.intervalId = null; 2172 | } 2173 | }; 2174 | 2175 | Chart.prototype.toImage = function toImage (download) { 2176 | if (this.adapter === "chartjs") { 2177 | if (download && download.background && download.background !== "transparent") { 2178 | // https://stackoverflow.com/questions/30464750/chartjs-line-chart-set-background-color 2179 | var canvas = this.chart.chart.canvas; 2180 | var ctx = this.chart.chart.ctx; 2181 | var tmpCanvas = document.createElement("canvas"); 2182 | var tmpCtx = tmpCanvas.getContext("2d"); 2183 | tmpCanvas.width = ctx.canvas.width; 2184 | tmpCanvas.height = ctx.canvas.height; 2185 | tmpCtx.fillStyle = download.background; 2186 | tmpCtx.fillRect(0, 0, tmpCanvas.width, tmpCanvas.height); 2187 | tmpCtx.drawImage(canvas, 0, 0); 2188 | return tmpCanvas.toDataURL("image/png"); 2189 | } else { 2190 | return this.chart.toBase64Image(); 2191 | } 2192 | } else { 2193 | // TODO throw error in next major version 2194 | // throw new Error("Feature only available for Chart.js"); 2195 | return null; 2196 | } 2197 | }; 2198 | 2199 | Chart.prototype.destroy = function destroy () { 2200 | if (this.__adapterObject) { 2201 | this.__adapterObject.destroy(this); 2202 | } 2203 | 2204 | if (this.__enterEvent) { 2205 | removeEvent(this.element, "mouseover", this.__enterEvent); 2206 | } 2207 | 2208 | if (this.__leaveEvent) { 2209 | removeEvent(this.element, "mouseout", this.__leaveEvent); 2210 | } 2211 | }; 2212 | 2213 | Chart.prototype.__updateOptions = function __updateOptions (options) { 2214 | var updateRefresh = options.refresh && options.refresh !== this.options.refresh; 2215 | this.options = merge(Chartkick.options, options); 2216 | if (updateRefresh) { 2217 | this.stopRefresh(); 2218 | this.startRefresh(); 2219 | } 2220 | }; 2221 | 2222 | Chart.prototype.__render = function __render () { 2223 | this.data = this.__processData(); 2224 | renderChart(this.__chartName(), this); 2225 | }; 2226 | 2227 | Chart.prototype.__config = function __config () { 2228 | return config; 2229 | }; 2230 | 2231 | var LineChart = /*@__PURE__*/(function (Chart) { 2232 | function LineChart () { 2233 | Chart.apply(this, arguments); 2234 | } 2235 | 2236 | if ( Chart ) LineChart.__proto__ = Chart; 2237 | LineChart.prototype = Object.create( Chart && Chart.prototype ); 2238 | LineChart.prototype.constructor = LineChart; 2239 | 2240 | LineChart.prototype.__processData = function __processData () { 2241 | return processSeries(this); 2242 | }; 2243 | 2244 | LineChart.prototype.__chartName = function __chartName () { 2245 | return "LineChart"; 2246 | }; 2247 | 2248 | return LineChart; 2249 | }(Chart)); 2250 | 2251 | var PieChart = /*@__PURE__*/(function (Chart) { 2252 | function PieChart () { 2253 | Chart.apply(this, arguments); 2254 | } 2255 | 2256 | if ( Chart ) PieChart.__proto__ = Chart; 2257 | PieChart.prototype = Object.create( Chart && Chart.prototype ); 2258 | PieChart.prototype.constructor = PieChart; 2259 | 2260 | PieChart.prototype.__processData = function __processData () { 2261 | return processSimple(this); 2262 | }; 2263 | 2264 | PieChart.prototype.__chartName = function __chartName () { 2265 | return "PieChart"; 2266 | }; 2267 | 2268 | return PieChart; 2269 | }(Chart)); 2270 | 2271 | var ColumnChart = /*@__PURE__*/(function (Chart) { 2272 | function ColumnChart () { 2273 | Chart.apply(this, arguments); 2274 | } 2275 | 2276 | if ( Chart ) ColumnChart.__proto__ = Chart; 2277 | ColumnChart.prototype = Object.create( Chart && Chart.prototype ); 2278 | ColumnChart.prototype.constructor = ColumnChart; 2279 | 2280 | ColumnChart.prototype.__processData = function __processData () { 2281 | return processSeries(this, null, true); 2282 | }; 2283 | 2284 | ColumnChart.prototype.__chartName = function __chartName () { 2285 | return "ColumnChart"; 2286 | }; 2287 | 2288 | return ColumnChart; 2289 | }(Chart)); 2290 | 2291 | var BarChart = /*@__PURE__*/(function (Chart) { 2292 | function BarChart () { 2293 | Chart.apply(this, arguments); 2294 | } 2295 | 2296 | if ( Chart ) BarChart.__proto__ = Chart; 2297 | BarChart.prototype = Object.create( Chart && Chart.prototype ); 2298 | BarChart.prototype.constructor = BarChart; 2299 | 2300 | BarChart.prototype.__processData = function __processData () { 2301 | return processSeries(this, null, true); 2302 | }; 2303 | 2304 | BarChart.prototype.__chartName = function __chartName () { 2305 | return "BarChart"; 2306 | }; 2307 | 2308 | return BarChart; 2309 | }(Chart)); 2310 | 2311 | var AreaChart = /*@__PURE__*/(function (Chart) { 2312 | function AreaChart () { 2313 | Chart.apply(this, arguments); 2314 | } 2315 | 2316 | if ( Chart ) AreaChart.__proto__ = Chart; 2317 | AreaChart.prototype = Object.create( Chart && Chart.prototype ); 2318 | AreaChart.prototype.constructor = AreaChart; 2319 | 2320 | AreaChart.prototype.__processData = function __processData () { 2321 | return processSeries(this); 2322 | }; 2323 | 2324 | AreaChart.prototype.__chartName = function __chartName () { 2325 | return "AreaChart"; 2326 | }; 2327 | 2328 | return AreaChart; 2329 | }(Chart)); 2330 | 2331 | var GeoChart = /*@__PURE__*/(function (Chart) { 2332 | function GeoChart () { 2333 | Chart.apply(this, arguments); 2334 | } 2335 | 2336 | if ( Chart ) GeoChart.__proto__ = Chart; 2337 | GeoChart.prototype = Object.create( Chart && Chart.prototype ); 2338 | GeoChart.prototype.constructor = GeoChart; 2339 | 2340 | GeoChart.prototype.__processData = function __processData () { 2341 | return processSimple(this); 2342 | }; 2343 | 2344 | GeoChart.prototype.__chartName = function __chartName () { 2345 | return "GeoChart"; 2346 | }; 2347 | 2348 | return GeoChart; 2349 | }(Chart)); 2350 | 2351 | var ScatterChart = /*@__PURE__*/(function (Chart) { 2352 | function ScatterChart () { 2353 | Chart.apply(this, arguments); 2354 | } 2355 | 2356 | if ( Chart ) ScatterChart.__proto__ = Chart; 2357 | ScatterChart.prototype = Object.create( Chart && Chart.prototype ); 2358 | ScatterChart.prototype.constructor = ScatterChart; 2359 | 2360 | ScatterChart.prototype.__processData = function __processData () { 2361 | return processSeries(this, "number"); 2362 | }; 2363 | 2364 | ScatterChart.prototype.__chartName = function __chartName () { 2365 | return "ScatterChart"; 2366 | }; 2367 | 2368 | return ScatterChart; 2369 | }(Chart)); 2370 | 2371 | var BubbleChart = /*@__PURE__*/(function (Chart) { 2372 | function BubbleChart () { 2373 | Chart.apply(this, arguments); 2374 | } 2375 | 2376 | if ( Chart ) BubbleChart.__proto__ = Chart; 2377 | BubbleChart.prototype = Object.create( Chart && Chart.prototype ); 2378 | BubbleChart.prototype.constructor = BubbleChart; 2379 | 2380 | BubbleChart.prototype.__processData = function __processData () { 2381 | return processSeries(this, "bubble"); 2382 | }; 2383 | 2384 | BubbleChart.prototype.__chartName = function __chartName () { 2385 | return "BubbleChart"; 2386 | }; 2387 | 2388 | return BubbleChart; 2389 | }(Chart)); 2390 | 2391 | var Timeline = /*@__PURE__*/(function (Chart) { 2392 | function Timeline () { 2393 | Chart.apply(this, arguments); 2394 | } 2395 | 2396 | if ( Chart ) Timeline.__proto__ = Chart; 2397 | Timeline.prototype = Object.create( Chart && Chart.prototype ); 2398 | Timeline.prototype.constructor = Timeline; 2399 | 2400 | Timeline.prototype.__processData = function __processData () { 2401 | var i, data = this.rawData; 2402 | for (i = 0; i < data.length; i++) { 2403 | data[i][1] = toDate(data[i][1]); 2404 | data[i][2] = toDate(data[i][2]); 2405 | } 2406 | return data; 2407 | }; 2408 | 2409 | Timeline.prototype.__chartName = function __chartName () { 2410 | return "Timeline"; 2411 | }; 2412 | 2413 | return Timeline; 2414 | }(Chart)); 2415 | 2416 | var Chartkick = { 2417 | LineChart: LineChart, 2418 | PieChart: PieChart, 2419 | ColumnChart: ColumnChart, 2420 | BarChart: BarChart, 2421 | AreaChart: AreaChart, 2422 | GeoChart: GeoChart, 2423 | ScatterChart: ScatterChart, 2424 | BubbleChart: BubbleChart, 2425 | Timeline: Timeline, 2426 | charts: {}, 2427 | configure: function (options) { 2428 | for (var key in options) { 2429 | if (options.hasOwnProperty(key)) { 2430 | config[key] = options[key]; 2431 | } 2432 | } 2433 | }, 2434 | setDefaultOptions: function (opts) { 2435 | Chartkick.options = opts; 2436 | }, 2437 | eachChart: function (callback) { 2438 | for (var chartId in Chartkick.charts) { 2439 | if (Chartkick.charts.hasOwnProperty(chartId)) { 2440 | callback(Chartkick.charts[chartId]); 2441 | } 2442 | } 2443 | }, 2444 | config: config, 2445 | options: {}, 2446 | adapters: adapters, 2447 | addAdapter: addAdapter, 2448 | use: function(adapter) { 2449 | addAdapter(adapter); 2450 | return Chartkick; 2451 | } 2452 | }; 2453 | 2454 | // not ideal, but allows for simpler integration 2455 | if (typeof window !== "undefined" && !window.Chartkick) { 2456 | window.Chartkick = Chartkick; 2457 | } 2458 | 2459 | // backwards compatibility for esm require 2460 | Chartkick.default = Chartkick; 2461 | 2462 | return Chartkick; 2463 | 2464 | }))); 2465 | -------------------------------------------------------------------------------- /web/views/benchmarks.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |

Benchmarks

7 |
8 | 9 |
10 |
11 | <%= csrf_tag %> 12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 | <% @types.each do |type| %> 20 |
21 |
22 |
23 |

<%= type %>

24 |
25 |
26 |
27 | <%= csrf_tag %> 28 |
29 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
Amount of tasks by execution time
38 | <%= column_chart @charts[type][:stats], library: { hAxis: { title: "Execution time (s)" }, vAxis: { title: "Tasks" } } %> 39 |
40 |
41 |
Average execution time
42 | <%= pie_chart @charts[type][:total] %> 43 |
44 |
45 |
46 | <% end %> 47 | --------------------------------------------------------------------------------