├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── pycall.yml ├── .gitignore ├── Dockerfile.dev ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── binder └── Dockerfile ├── charty.gemspec ├── examples ├── Gemfile ├── active_record.ipynb ├── bar_plot.rb ├── box_plot.rb ├── daru.ipynb ├── iris_dataset.ipynb ├── nmatrix.ipynb ├── numo_narray.ipynb ├── palette.rb ├── sample.png ├── sample_bokeh.ipynb ├── sample_google_chart.ipynb ├── sample_gruff.ipynb ├── sample_images │ ├── bar_bokeh.html │ ├── bar_gruff.png │ ├── bar_pyplot.png │ ├── bar_rubyplot.png │ ├── barh_bokeh.html │ ├── barh_gruff.png │ ├── barh_pyplot.png │ ├── box_plot_bokeh.html │ ├── box_plot_pyplot.png │ ├── bubble_pyplot.png │ ├── bubble_rubyplot.png │ ├── curve_bokeh.html │ ├── curve_gruff.png │ ├── curve_pyplot.png │ ├── curve_rubyplot.png │ ├── curve_with_function_bokeh.html │ ├── curve_with_function_pyplot.png │ ├── curve_with_function_rubyplot.png │ ├── error_bar_pyplot.png │ ├── hist_gruff.png │ ├── hist_pyplot.png │ ├── scatter_bokeh.html │ ├── scatter_gruff.png │ ├── scatter_pyplot.png │ ├── scatter_rubyplot.png │ ├── subplot2_pyplot.png │ └── subplot_pyplot.png ├── sample_pyplot.ipynb ├── sample_rubyplot.ipynb └── scatter_plot.rb ├── images ├── design_concept.png ├── penguins_body_mass_g_flipper_length_mm_scatter_plot.png ├── penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png ├── penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png ├── penguins_species_body_mass_g_bar_plot_h.png ├── penguins_species_body_mass_g_bar_plot_v.png ├── penguins_species_body_mass_g_box_plot_h.png ├── penguins_species_body_mass_g_box_plot_v.png ├── penguins_species_body_mass_g_sex_bar_plot_v.png └── penguins_species_body_mass_g_sex_box_plot_v.png ├── lib ├── charty.rb └── charty │ ├── backend_methods.rb │ ├── backends.rb │ ├── backends │ ├── bokeh.rb │ ├── google_charts.rb │ ├── gruff.rb │ ├── plotly.rb │ ├── plotly_helpers │ │ ├── html_renderer.rb │ │ ├── notebook_renderer.rb │ │ └── plotly_renderer.rb │ ├── pyplot.rb │ ├── rubyplot.rb │ └── unicode_plot.rb │ ├── cache_dir.rb │ ├── dash_pattern_generator.rb │ ├── index.rb │ ├── iruby_helper.rb │ ├── layout.rb │ ├── linspace.rb │ ├── plot_methods.rb │ ├── plotter.rb │ ├── plotters.rb │ ├── plotters │ ├── abstract_plotter.rb │ ├── bar_plotter.rb │ ├── box_plotter.rb │ ├── categorical_plotter.rb │ ├── count_plotter.rb │ ├── distribution_plotter.rb │ ├── estimation_support.rb │ ├── histogram_plotter.rb │ ├── line_plotter.rb │ ├── random_support.rb │ ├── relational_plotter.rb │ ├── scatter_plotter.rb │ └── vector_plotter.rb │ ├── statistics.rb │ ├── table.rb │ ├── table_adapters.rb │ ├── table_adapters │ ├── active_record_adapter.rb │ ├── arrow_adapter.rb │ ├── base_adapter.rb │ ├── daru_adapter.rb │ ├── datasets_adapter.rb │ ├── hash_adapter.rb │ ├── narray_adapter.rb │ └── pandas_adapter.rb │ ├── util.rb │ ├── vector.rb │ ├── vector_adapters.rb │ ├── vector_adapters │ ├── array_adapter.rb │ ├── arrow_adapter.rb │ ├── daru_adapter.rb │ ├── narray_adapter.rb │ ├── numpy_adapter.rb │ ├── pandas_adapter.rb │ └── vector_adapter.rb │ └── version.rb ├── requirements.txt └── test ├── backends └── plotly_test.rb ├── backends_test.rb ├── dash_pattern_generator_test.rb ├── helper.rb ├── index_test.rb ├── lib └── load_error_backend.rb ├── plot_methods ├── bar_plot_test.rb ├── box_plot_test.rb ├── count_plot_test.rb ├── hist_plot_test.rb ├── line_plot_test.rb └── scatter_plot_test.rb ├── plotter_test.rb ├── run.rb ├── table ├── activerecord_test.rb ├── array_test.rb ├── arrow_test.rb ├── csv_test.rb ├── daru_test.rb ├── hash_test.rb ├── narray_test.rb ├── nmatrix_test.rb ├── pandas_test.rb ├── red_datasets_test.rb ├── table_aref_test.rb ├── table_aset_test.rb ├── table_equality_test.rb ├── table_group_by_test.rb ├── table_melt_test.rb ├── table_reset_index_test.rb └── table_sort_values_test.rb ├── table_adapters_test.rb ├── util_test.rb └── vector ├── array_test.rb ├── arrow_test.rb ├── daru_test.rb ├── iloc_test.rb ├── narray_test.rb ├── nmatrix_test.rb ├── numpy_test.rb ├── pandas_test.rb ├── vector_equality_test.rb ├── vector_scale_test.rb └── vector_values_at_test.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | ruby-versions: 9 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 10 | with: 11 | engine: cruby 12 | min_version: 2.7 13 | versions: '["debug"]' 14 | 15 | test: 16 | needs: ruby-versions 17 | name: ${{ matrix.os }}/${{ matrix.ruby }} 18 | runs-on: ${{ matrix.os }} 19 | timeout-minutes: 10 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: 24 | - ubuntu-latest 25 | - ubuntu-22.04 26 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 27 | 28 | env: 29 | BUNDLE_WITHOUT: "python" 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - run: npm install playwright@latest 35 | - run: ./node_modules/.bin/playwright install 36 | 37 | - run: sudo apt install build-essential libsqlite3-dev 38 | 39 | - name: Set up Ruby 40 | uses: ruby/setup-ruby@v1 41 | with: 42 | bundler-cache: true 43 | ruby-version: ${{ matrix.ruby }} 44 | 45 | - uses: actions/cache@v4 46 | with: 47 | path: ~/.cache/red-datasets 48 | key: ${{ runner.os }}-${{ hashFiles('charty.gemspec') }} 49 | restore-keys: ${{ runner.os }}- 50 | 51 | - run: bundle exec rake 52 | 53 | - run: bundle exec rake build 54 | 55 | - run: gem install --user pkg/*.gem 56 | -------------------------------------------------------------------------------- /.github/workflows/pycall.yml: -------------------------------------------------------------------------------- 1 | name: CI with matplotlib and pandas 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | ruby-versions: 9 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 10 | with: 11 | engine: cruby 12 | min_version: 3.1 13 | versions: '["debug"]' 14 | 15 | test: 16 | needs: ruby-versions 17 | name: ${{ matrix.os }}/${{ matrix.ruby }}/${{ matrix.python }}-${{ matrix.python_architecture }} 18 | runs-on: ${{ matrix.os }} 19 | timeout-minutes: 10 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: 25 | - ubuntu-latest 26 | - ubuntu-22.04 27 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 28 | python: 29 | - 3.x 30 | python_architecture: 31 | - x64 32 | 33 | env: 34 | BUNDLE_WITHOUT: "numo" 35 | PYTHON: python 36 | 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - name: Setup Python 41 | uses: actions/setup-python@v5 42 | with: 43 | architecture: ${{ matrix.python_architecture }} 44 | cache: "pip" 45 | python-version: ${{ matrix.python }} 46 | 47 | - run: pip install -r requirements.txt 48 | 49 | - run: npm install playwright@latest 50 | - run: ./node_modules/.bin/playwright install 51 | 52 | - run: sudo apt install build-essential libsqlite3-dev 53 | 54 | - name: Setup Ruby 55 | uses: ruby/setup-ruby@v1 56 | with: 57 | bundler-cache: true 58 | ruby-version: ${{ matrix.ruby }} 59 | 60 | - uses: actions/cache@v4 61 | with: 62 | path: ~/.cache/red-datasets 63 | key: ${{ runner.os }}-${{ hashFiles('charty.gemspec') }} 64 | restore-keys: ${{ runner.os }}- 65 | 66 | - run: bundle exec rake 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | Gemfile.lock 46 | .ruby-version 47 | .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | 52 | # Jupyter notebook 53 | .ipynb_checkpoints/ 54 | 55 | # Python 56 | __pycache__/ 57 | 58 | # Object files 59 | *.bundle 60 | *.so 61 | *.o 62 | *.pyc 63 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE_TAG=24a7f04dfc46 2 | FROM rubydata/minimal-notebook:$BASE_IMAGE_TAG 3 | 4 | USER root 5 | RUN mkdir -p /charty && \ 6 | chown ${NB_USER}:users /charty 7 | 8 | USER ${NB_USER} 9 | 10 | WORKDIR /charty 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in charty.gemspec 6 | gemspec 7 | 8 | group :development, :test do 9 | gem "bundler", ">= 1.16" 10 | gem "csv" 11 | gem "daru" 12 | gem "fiddle" 13 | gem "iruby", ">= 0.7.0" 14 | gem "matrix" # need for daru on Ruby > 3.0 15 | gem "rake" 16 | end 17 | 18 | group :test do 19 | gem "test-unit" 20 | end 21 | 22 | group :activerecord do 23 | gem "activerecord" 24 | # We may need to specify version explicitly to align with `gem 25 | # "sqlite", "..."` in 26 | # lib/active_record/connection_adapters/sqlite3_adapter.rb. 27 | gem "sqlite3" 28 | end 29 | 30 | group :cruby do 31 | gem "enumerable-statistics" 32 | end 33 | 34 | group :numo do 35 | gem "numo-narray" 36 | end 37 | 38 | group :python do 39 | gem "matplotlib" 40 | gem "numpy" 41 | gem "pandas" 42 | end 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Red Data Tools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | desc "Run tests" 5 | task :test do 6 | ruby("test/run.rb") 7 | end 8 | 9 | task default: :test 10 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "charty" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /binder/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE_TAG=c9ca70040856 2 | FROM rubydata/minimal-notebook:$BASE_IMAGE_TAG 3 | ADD examples/*.ipynb ./ 4 | -------------------------------------------------------------------------------- /charty.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib", __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require "charty/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "charty" 7 | version_components = [ 8 | Charty::Version::MAJOR.to_s, 9 | Charty::Version::MINOR.to_s, 10 | Charty::Version::MICRO.to_s, 11 | Charty::Version::TAG, 12 | ] 13 | spec.version = version_components.compact.join(".") 14 | spec.authors = ["youchan", "mrkn", "284km"] 15 | spec.email = ["youchan01@gmail.com", "mrkn@mrkn.jp", "k.furuhashi10@gmail.com"] 16 | 17 | spec.summary = %q{Visualizing your data in a simple way.} 18 | spec.description = %q{Visualizing your data in a simple way.} 19 | spec.homepage = "https://github.com/red-data-tools/charty" 20 | spec.license = "MIT" 21 | 22 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 23 | f.match(%r{^(test|spec|features)/}) 24 | end 25 | spec.bindir = "exe" 26 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 27 | spec.require_paths = ["lib"] 28 | 29 | spec.add_dependency "red-colors", ">= 0.3.0" 30 | spec.add_dependency "red-datasets", ">= 0.1.2" 31 | spec.add_dependency "red-palette", ">= 0.5.0" 32 | 33 | spec.add_dependency "matplotlib", ">= 1.2.0" 34 | spec.add_dependency "pandas", ">= 0.3.5" 35 | spec.add_dependency "playwright-ruby-client" 36 | spec.add_dependency "pycall", ">= 1.5.2" 37 | end 38 | -------------------------------------------------------------------------------- /examples/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gem 'mimemagic' 8 | 9 | install_if -> { RUBY_PLATFORM !~ /darwin|mswin|mingw/ } do 10 | gem 'rbczmq' 11 | end 12 | 13 | gem 'cztop' 14 | gem 'iruby' 15 | gem 'matplotlib' 16 | gem 'charty', path: ".." 17 | 18 | gem 'gruff' 19 | gem 'rubyplot' 20 | 21 | gem 'red-datasets' 22 | gem 'red-datasets-daru' 23 | gem 'numo-narray' 24 | gem 'nmatrix' 25 | 26 | gem 'rails' 27 | gem 'sqlite3', '~> 1.3.6' 28 | gem 'red-colors' 29 | -------------------------------------------------------------------------------- /examples/bar_plot.rb: -------------------------------------------------------------------------------- 1 | # This example generates box_plot results in README.md 2 | 3 | require "charty" 4 | require "datasets" 5 | require "matplotlib" 6 | 7 | Charty::Backends.use(:pyplot) 8 | Matplotlib.use(:agg) 9 | 10 | penguins = Datasets::Penguins.new 11 | 12 | Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g) 13 | .save("penguins_species_body_mass_g_bar_plot_v.png") 14 | 15 | Charty.bar_plot(data: penguins, x: :body_mass_g, y: :species) 16 | .save("penguins_species_body_mass_g_bar_plot_h.png") 17 | 18 | Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex) 19 | .save("penguins_species_body_mass_g_sex_bar_plot_v.png") 20 | -------------------------------------------------------------------------------- /examples/box_plot.rb: -------------------------------------------------------------------------------- 1 | require "charty" 2 | require "datasets" 3 | require "matplotlib" 4 | 5 | Charty::Backends.use(:pyplot) 6 | Matplotlib.use(:agg) 7 | 8 | penguins = Datasets::Penguins.new 9 | 10 | Charty.box_plot(data: penguins, x: :species, y: :body_mass_g) 11 | .save("penguins_species_body_mass_g_box_plot_v.png") 12 | 13 | Charty.box_plot(data: penguins, x: :body_mass_g, y: :species) 14 | .save("penguins_species_body_mass_g_box_plot_h.png") 15 | 16 | Charty.box_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex) 17 | .save("penguins_species_body_mass_g_sex_box_plot_v.png") 18 | -------------------------------------------------------------------------------- /examples/palette.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | require "charty" 4 | 5 | Palette.default = ARGV[0] if ARGV[0] 6 | 7 | charty = Charty::Plotter.new(:pyplot) 8 | figure = charty.bar do 9 | series [1, 2, 3, 4, 5], [10, 20, 25, 30, 40], label: "a" 10 | series [1, 2, 3, 4, 5], [20, 10, 15, 20, 50], label: "b" 11 | series [1, 2, 3, 4, 5], [30, 25, 20, 10, 5], label: "cd" 12 | end 13 | figure.save("bar_sample.png") 14 | 15 | figure = charty.barh do 16 | series [1, 2, 3, 4, 5], [10, 20, 25, 30, 40], label: "a" 17 | series [1, 2, 3, 4, 5], [20, 10, 15, 20, 50], label: "b" 18 | series [1, 2, 3, 4, 5], [30, 25, 20, 10, 5], label: "cd" 19 | end 20 | figure.save("barh_sample.png") 21 | 22 | figure = charty.curve do 23 | series [1, 2, 3, 4, 5], [10, 20, 25, 30, 40], label: "a" 24 | series [1, 2, 3, 4, 5], [20, 10, 15, 20, 50], label: "b" 25 | series [1, 2, 3, 4, 5], [30, 25, 20, 10, 5], label: "cd" 26 | end 27 | figure.save("curve_sample.png") 28 | 29 | figure = charty.box_plot do 30 | data [ 31 | [1, 3, 7, *Array.new(20) { rand(40..70) }, 100, 110, 120], 32 | [1, 4, 7, *Array.new(80) { rand(35..80) }, 130, 135, 145], 33 | [0, 2, 8, *Array.new(20) { rand(60..90) }, 150, 160, 165] 34 | ] 35 | xlabel "foo" 36 | ylabel "bar" 37 | title "box plot" 38 | end 39 | figure.save("box_plot_sample.png") 40 | 41 | figure = charty.scatter do 42 | series 0..10, (0..1).step(0.1), label: 'sample1' 43 | series 0..5, (0..1).step(0.2), label: 'sample2' 44 | series [0, 1, 2, 3, 4], [0, -0.1, -0.5, -0.5, 0.1] 45 | end 46 | figure.save("scatter_sample.png") 47 | 48 | figure = charty.bubble do 49 | series 0..10, (0..1).step(0.1), [10, 100, 1000, 20, 200, 2000, 5, 50, 500, 4, 40], label: 'sample1' 50 | series 0..5, (0..1).step(0.2), [1, 10, 100, 1000, 500, 100], label: 'sample2' 51 | series [0, 1, 2, 3, 4], [0, -0.1, -0.5, -0.5, 0.1], [40, 30, 200, 10, 5] 52 | range x: 0..10, y: -1..1 53 | xlabel 'x label' 54 | ylabel 'y label' 55 | title 'bubble sample' 56 | end 57 | figure.save("bubble_sample.png") 58 | 59 | def randn(n, mu=0.0, sigma=1.0) 60 | Array.new(n) do 61 | x, y = rand, rand 62 | sigma * Math.sqrt(-2 * Math.log(x)) * Math.cos(2 * Math::PI * y) + mu 63 | end 64 | end 65 | 66 | figure = charty.hist do 67 | data [ randn(1000, 0.0, 1.0), 68 | randn(100, 2.0, 2.0) ] 69 | title "histogram sample" 70 | end 71 | figure.save("hist_sample.png") 72 | -------------------------------------------------------------------------------- /examples/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample.png -------------------------------------------------------------------------------- /examples/sample_bokeh.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 43, 6 | "metadata": { 7 | "scrolled": false 8 | }, 9 | "outputs": [ 10 | { 11 | "data": { 12 | "text/plain": [ 13 | "#>>" 14 | ] 15 | }, 16 | "execution_count": 43, 17 | "metadata": {}, 18 | "output_type": "execute_result" 19 | } 20 | ], 21 | "source": [ 22 | "require 'charty'\n", 23 | "\n", 24 | "charty = Charty::Plotter.new(:bokeh)" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 44, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "curve = charty.curve do\n", 34 | " function {|x| Math.sin(x) }\n", 35 | " range x: 0..10, y: -1..1\n", 36 | " xlabel 'foo'\n", 37 | " ylabel 'bar'\n", 38 | "end\n", 39 | "curve.render(\"sample_images/curve_with_function_bokeh.html\")" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 45, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "curve2 = charty.curve do\n", 49 | " series [0,1,2,3,4], [10,40,20,90,70]\n", 50 | " series [0,1,2,3,4], [90,80,70,60,50]\n", 51 | " series [0,1,2,3,4,5,6,7,8], [50,60,20,30,10, 90, 0, 100, 50]\n", 52 | " range x: 0..10, y: 1..100\n", 53 | " xlabel 'foo'\n", 54 | " ylabel 'bar'\n", 55 | "end\n", 56 | "curve2.render(\"sample_images/curve_bokeh.html\")" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 46, 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "bar = charty.bar do\n", 66 | " series [0,1,2,3,4], [10,40,20,90,70]\n", 67 | " series [0,1,2,3,4], [90,80,70,60,50]\n", 68 | " series [0,1,2,3,4,5,6,7,8], [50,60,20,30,10, 90, 0, 100, 50]\n", 69 | " range x: 0..10, y: 1..100\n", 70 | " xlabel 'foo'\n", 71 | " ylabel 'bar'\n", 72 | " title 'bar plot'\n", 73 | "end\n", 74 | "bar.render(\"sample_images/bar_bokeh.html\")" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 47, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "barh = charty.barh do\n", 84 | " series [0,1,2,3,4], [10,40,20,90,70]\n", 85 | " series [0,1,2,3,4], [90,80,70,60,50]\n", 86 | " series [0,1,2,3,4,5,6,7,8], [50,60,20,30,10, 90, 0, 100, 50]\n", 87 | " range x: 0..10, y: 1..100\n", 88 | " xlabel 'foo'\n", 89 | " ylabel 'bar'\n", 90 | " title 'bar plot'\n", 91 | "end\n", 92 | "barh.render(\"sample_images/barh_bokeh.html\")" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 48, 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "box_plot = charty.box_plot do\n", 102 | " data [[60,70,80,70,50], [100,40,20,80,70], [30, 10]]\n", 103 | " range x: 0..10, y: 1..100\n", 104 | " xlabel 'foo'\n", 105 | " ylabel 'bar'\n", 106 | " title 'box plot'\n", 107 | "end\n", 108 | "box_plot.render(\"sample_images/box_plot_bokeh.html\")" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 49, 114 | "metadata": {}, 115 | "outputs": [], 116 | "source": [ 117 | "scatter = charty.scatter do\n", 118 | " series 0..10, (0..1).step(0.1), label: 'sample1'\n", 119 | " series 0..5, (0..1).step(0.2), label: 'sample2'\n", 120 | " series [0, 1, 2, 3, 4], [0, -0.1, -0.5, -0.5, 0.1]\n", 121 | " range x: 0..10, y: -1..1\n", 122 | " # xlabel 'x label'\n", 123 | " # xlabel ''\n", 124 | " ylabel 'y label'\n", 125 | " title 'scatter sample'\n", 126 | "end\n", 127 | "scatter.render(\"sample_images/scatter_bokeh.html\")" 128 | ] 129 | } 130 | ], 131 | "metadata": { 132 | "kernelspec": { 133 | "display_name": "Ruby 2.6.2", 134 | "language": "ruby", 135 | "name": "ruby" 136 | }, 137 | "language_info": { 138 | "file_extension": ".rb", 139 | "mimetype": "application/x-ruby", 140 | "name": "ruby", 141 | "version": "2.6.2" 142 | }, 143 | "toc": { 144 | "nav_menu": {}, 145 | "number_sections": true, 146 | "sideBar": true, 147 | "skip_h1_title": false, 148 | "toc_cell": false, 149 | "toc_position": {}, 150 | "toc_section_display": "block", 151 | "toc_window_display": false 152 | } 153 | }, 154 | "nbformat": 4, 155 | "nbformat_minor": 2 156 | } 157 | -------------------------------------------------------------------------------- /examples/sample_images/bar_gruff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/bar_gruff.png -------------------------------------------------------------------------------- /examples/sample_images/bar_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/bar_pyplot.png -------------------------------------------------------------------------------- /examples/sample_images/bar_rubyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/bar_rubyplot.png -------------------------------------------------------------------------------- /examples/sample_images/barh_gruff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/barh_gruff.png -------------------------------------------------------------------------------- /examples/sample_images/barh_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/barh_pyplot.png -------------------------------------------------------------------------------- /examples/sample_images/box_plot_bokeh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Bokeh Plot 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 47 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /examples/sample_images/box_plot_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/box_plot_pyplot.png -------------------------------------------------------------------------------- /examples/sample_images/bubble_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/bubble_pyplot.png -------------------------------------------------------------------------------- /examples/sample_images/bubble_rubyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/bubble_rubyplot.png -------------------------------------------------------------------------------- /examples/sample_images/curve_gruff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/curve_gruff.png -------------------------------------------------------------------------------- /examples/sample_images/curve_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/curve_pyplot.png -------------------------------------------------------------------------------- /examples/sample_images/curve_rubyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/curve_rubyplot.png -------------------------------------------------------------------------------- /examples/sample_images/curve_with_function_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/curve_with_function_pyplot.png -------------------------------------------------------------------------------- /examples/sample_images/curve_with_function_rubyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/curve_with_function_rubyplot.png -------------------------------------------------------------------------------- /examples/sample_images/error_bar_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/error_bar_pyplot.png -------------------------------------------------------------------------------- /examples/sample_images/hist_gruff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/hist_gruff.png -------------------------------------------------------------------------------- /examples/sample_images/hist_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/hist_pyplot.png -------------------------------------------------------------------------------- /examples/sample_images/scatter_gruff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/scatter_gruff.png -------------------------------------------------------------------------------- /examples/sample_images/scatter_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/scatter_pyplot.png -------------------------------------------------------------------------------- /examples/sample_images/scatter_rubyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/scatter_rubyplot.png -------------------------------------------------------------------------------- /examples/sample_images/subplot2_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/subplot2_pyplot.png -------------------------------------------------------------------------------- /examples/sample_images/subplot_pyplot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/examples/sample_images/subplot_pyplot.png -------------------------------------------------------------------------------- /examples/scatter_plot.rb: -------------------------------------------------------------------------------- 1 | require "charty" 2 | require "datasets" 3 | require "matplotlib" 4 | 5 | Charty::Backends.use(:pyplot) 6 | Matplotlib.use(:agg) 7 | 8 | penguins = Datasets::Penguins.new 9 | 10 | Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm) 11 | .save("penguins_body_mass_g_flipper_length_mm_scatter_plot.png") 12 | 13 | Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm, color: :species) 14 | .save("penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png") 15 | 16 | Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm, color: :species, style: :sex) 17 | .save("penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png") 18 | -------------------------------------------------------------------------------- /images/design_concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/images/design_concept.png -------------------------------------------------------------------------------- /images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png -------------------------------------------------------------------------------- /images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png -------------------------------------------------------------------------------- /images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png -------------------------------------------------------------------------------- /images/penguins_species_body_mass_g_bar_plot_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/images/penguins_species_body_mass_g_bar_plot_h.png -------------------------------------------------------------------------------- /images/penguins_species_body_mass_g_bar_plot_v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/images/penguins_species_body_mass_g_bar_plot_v.png -------------------------------------------------------------------------------- /images/penguins_species_body_mass_g_box_plot_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/images/penguins_species_body_mass_g_box_plot_h.png -------------------------------------------------------------------------------- /images/penguins_species_body_mass_g_box_plot_v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/images/penguins_species_body_mass_g_box_plot_v.png -------------------------------------------------------------------------------- /images/penguins_species_body_mass_g_sex_bar_plot_v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/images/penguins_species_body_mass_g_sex_bar_plot_v.png -------------------------------------------------------------------------------- /images/penguins_species_body_mass_g_sex_box_plot_v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-data-tools/charty/78c52f5ca31bf5d5230e15d2d352bd13345785ee/images/penguins_species_body_mass_g_sex_box_plot_v.png -------------------------------------------------------------------------------- /lib/charty.rb: -------------------------------------------------------------------------------- 1 | require_relative "charty/version" 2 | 3 | require "colors" 4 | require "palette" 5 | 6 | require_relative "charty/cache_dir" 7 | require_relative "charty/util" 8 | require_relative "charty/iruby_helper" 9 | require_relative "charty/dash_pattern_generator" 10 | require_relative "charty/backends" 11 | require_relative "charty/backend_methods" 12 | require_relative "charty/plotter" 13 | require_relative "charty/index" 14 | require_relative "charty/layout" 15 | require_relative "charty/linspace" 16 | require_relative "charty/plotters" 17 | require_relative "charty/plot_methods" 18 | require_relative "charty/table" 19 | require_relative "charty/table_adapters" 20 | require_relative "charty/statistics" 21 | require_relative "charty/vector_adapters" 22 | require_relative "charty/vector" 23 | -------------------------------------------------------------------------------- /lib/charty/backend_methods.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module BackendMethods 3 | def use_backend(backend) 4 | end 5 | end 6 | 7 | extend BackendMethods 8 | end 9 | -------------------------------------------------------------------------------- /lib/charty/backends.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | class BackendError < RuntimeError; end 3 | class BackendNotFoundError < BackendError; end 4 | class BackendLoadError < BackendError; end 5 | 6 | module Backends 7 | @backends = {} 8 | 9 | @current = nil 10 | 11 | def self.current 12 | @current 13 | end 14 | 15 | def self.current=(backend_name) 16 | backend_class = Backends.find_backend_class(backend_name) 17 | @current = backend_class.new 18 | end 19 | 20 | def self.use(backend) 21 | if block_given? 22 | begin 23 | saved, self.current = self.current, backend 24 | yield 25 | ensure 26 | self.current = saved 27 | end 28 | else 29 | self.current = backend 30 | end 31 | end 32 | 33 | def self.names 34 | @backends.keys 35 | end 36 | 37 | def self.register(name, backend_class) 38 | @backends[normalize_name(name)] = { 39 | class: backend_class, 40 | prepared: false, 41 | } 42 | end 43 | 44 | def self.find_backend_class(name) 45 | backend = @backends[normalize_name(name)] 46 | unless backend 47 | raise BackendNotFoundError, "Backend is not found: #{name.inspect}" 48 | end 49 | backend_class = backend[:class] 50 | unless backend[:prepared] 51 | if backend_class.respond_to?(:prepare) 52 | begin 53 | backend_class.prepare 54 | rescue LoadError 55 | raise BackendLoadError, "Backend load error: #{name.inspect}" 56 | end 57 | end 58 | backend[:prepared] = true 59 | end 60 | backend_class 61 | end 62 | 63 | private_class_method def self.normalize_name(name) 64 | case name 65 | when Symbol 66 | name.to_s 67 | else 68 | name.to_str 69 | end 70 | end 71 | end 72 | end 73 | 74 | require "charty/backends/bokeh" 75 | require "charty/backends/google_charts" 76 | require "charty/backends/gruff" 77 | require "charty/backends/plotly" 78 | require "charty/backends/pyplot" 79 | require "charty/backends/rubyplot" 80 | require "charty/backends/unicode_plot" 81 | -------------------------------------------------------------------------------- /lib/charty/backends/bokeh.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Backends 3 | class Bokeh 4 | Backends.register(:bokeh, self) 5 | 6 | class << self 7 | def prepare 8 | require 'pycall' 9 | end 10 | end 11 | 12 | def initialize 13 | @plot = PyCall.import_module('bokeh.plotting') 14 | end 15 | 16 | def series=(series) 17 | @series = series 18 | end 19 | 20 | def old_style_render(context, filename) 21 | plot = plot(context) 22 | save(plot, context, filename) 23 | PyCall.import_module('bokeh.io').show(plot) 24 | end 25 | 26 | def old_style_save(plot, context, filename) 27 | if filename 28 | PyCall.import_module('bokeh.io').save(plot, filename) 29 | end 30 | end 31 | 32 | def plot(context) 33 | #TODO To implement boxplot, bublle, error_bar, hist. 34 | 35 | plot = @plot.figure(title: context&.title) 36 | plot.xaxis[0].axis_label = context&.xlabel 37 | plot.yaxis[0].axis_label = context&.ylabel 38 | 39 | case context.method 40 | when :bar 41 | context.series.each do |data| 42 | diffs = data.xs.to_a.each_cons(2).map {|n, m| (n - m).abs } 43 | width = diffs.min * 0.8 44 | plot.vbar(data.xs.to_a, width, data.ys.to_a) 45 | end 46 | 47 | when :barh 48 | context.series.each do |data| 49 | diffs = data.xs.to_a.each_cons(2).map {|n, m| (n - m).abs } 50 | height = diffs.min * 0.8 51 | plot.hbar(data.xs.to_a, height, data.ys.to_a) 52 | end 53 | 54 | when :boxplot 55 | raise NotImplementedError 56 | 57 | when :bubble 58 | raise NotImplementedError 59 | 60 | when :curve 61 | context.series.each do |data| 62 | plot.line(data.xs.to_a, data.ys.to_a) 63 | end 64 | 65 | when :scatter 66 | context.series.each do |data| 67 | plot.scatter(data.xs.to_a, data.ys.to_a) 68 | end 69 | 70 | when :error_bar 71 | raise NotImplementedError 72 | 73 | when :hist 74 | raise NotImplementedError 75 | end 76 | plot 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/charty/backends/gruff.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Charty 4 | module Backends 5 | class Gruff 6 | Backends.register(:gruff, self) 7 | 8 | class << self 9 | def prepare 10 | require 'gruff' 11 | end 12 | end 13 | 14 | def initialize 15 | @plot = ::Gruff 16 | end 17 | 18 | def label(x, y) 19 | end 20 | 21 | def series=(series) 22 | @series = series 23 | end 24 | 25 | def render_layout(layout) 26 | raise NotImplementedError 27 | end 28 | 29 | def old_style_render(context, filename="") 30 | FileUtils.mkdir_p(File.dirname(filename)) 31 | plot(@plot, context).write(filename) 32 | end 33 | 34 | def plot(plot, context) 35 | # case 36 | # when plot.respond_to?(:xlim) 37 | # plot.xlim(context.range_x.begin, context.range_x.end) 38 | # plot.ylim(context.range_y.begin, context.range_y.end) 39 | # when plot.respond_to?(:set_xlim) 40 | # plot.set_xlim(context.range_x.begin, context.range_x.end) 41 | # plot.set_ylim(context.range_y.begin, context.range_y.end) 42 | # end 43 | 44 | case context.method 45 | when :bar 46 | p = plot::Bar.new 47 | p.title = context.title if context.title 48 | p.x_axis_label = context.xlabel if context.xlabel 49 | p.y_axis_label = context.ylabel if context.ylabel 50 | context.series.each do |data| 51 | p.data(data.label, data.xs.to_a) 52 | end 53 | p 54 | when :barh 55 | p = plot::SideBar.new 56 | p.title = context.title if context.title 57 | p.x_axis_label = context.xlabel if context.xlabel 58 | p.y_axis_label = context.ylabel if context.ylabel 59 | labels = context.series.map {|data| data.xs.to_a}.flatten.uniq 60 | labels.each do |label| 61 | data_ys = context.series.map do |data| 62 | if data.xs.to_a.index(label) 63 | data.ys.to_a[data.xs.to_a.index(label)] 64 | else 65 | 0 66 | end 67 | end 68 | p.data(label, data_ys) 69 | end 70 | p.labels = context.series.each_with_index.inject({}) do |attr, (data, i)| 71 | attr[i] = data.label 72 | attr 73 | end 74 | p 75 | when :box_plot 76 | # refs. https://github.com/topfunky/gruff/issues/155 77 | raise NotImplementedError 78 | when :bubble 79 | raise NotImplementedError 80 | when :curve 81 | p = plot::Line.new 82 | p.title = context.title if context.title 83 | p.x_axis_label = context.xlabel if context.xlabel 84 | p.y_axis_label = context.ylabel if context.ylabel 85 | context.series.each do |data| 86 | p.dataxy(data.label, data.xs.to_a, data.ys.to_a) 87 | end 88 | p 89 | when :scatter 90 | p = plot::Scatter.new 91 | p.title = context.title if context.title 92 | p.x_axis_label = context.xlabel if context.xlabel 93 | p.y_axis_label = context.ylabel if context.ylabel 94 | context.series.each do |data| 95 | p.data(data.label, data.xs.to_a, data.ys.to_a) 96 | end 97 | p 98 | when :error_bar 99 | # refs. https://github.com/topfunky/gruff/issues/163 100 | raise NotImplementedError 101 | when :hist 102 | p = plot::Histogram.new 103 | p.title = context.title if context.title 104 | p.x_axis_label = context.xlabel if context.xlabel 105 | p.y_axis_label = context.ylabel if context.ylabel 106 | if context.range_x 107 | p.minimum_bin = context.range_x.first 108 | p.maximum_bin = context.range_x.last 109 | end 110 | context.data.each do |data| 111 | p.data('', data.to_a) 112 | end 113 | p 114 | end 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/charty/backends/plotly_helpers/notebook_renderer.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Backends 3 | module PlotlyHelpers 4 | class NotebookRenderer < HtmlRenderer 5 | def initialize(use_cdn: false) 6 | super(use_cdn: use_cdn, full_html: false, requirejs: true) 7 | @initialized = false 8 | end 9 | 10 | def activate 11 | return if @initialized 12 | 13 | unless IRubyHelper.iruby_notebook? 14 | raise "IRuby is unavailable" 15 | end 16 | 17 | if @use_cdn 18 | script = <<~END_SCRIPT % {win_config: window_plotly_config, mathjax_config: mathjax_config} 19 | 34 | END_SCRIPT 35 | else 36 | script = <<~END_SCRIPT % {script: get_plotlyjs, win_config: window_plotly_config, mathjax_config: mathjax_config} 37 | 50 | END_SCRIPT 51 | end 52 | IRuby.display(script, mime: "text/html") 53 | @initialized = true 54 | nil 55 | end 56 | 57 | def render(figure, element_id: nil, post_script: nil) 58 | ary = Array.try_convert(post_script) 59 | post_script = ary || [post_script] 60 | post_script.unshift(<<~END_POST_SCRIPT) 61 | var gd = document.getElementById('%{plot_id}'); 62 | var x = new MutationObserver(function (mutations, observer) { 63 | var display = window.getComputedStyle(gd).display; 64 | if (!display || display === 'none') { 65 | console.log([gd, 'removed']); 66 | Plotly.purge(gd); 67 | observer.disconnect(); 68 | } 69 | }); 70 | 71 | // Listen for the removal of the full notebook cell 72 | var notebookContainer = gd.closest('#notebook-container'); 73 | if (notebookContainer) { 74 | x.observe(notebookContainer, {childList: true}); 75 | } 76 | 77 | // Listen for the clearing of the current output cell 78 | var outputEl = gd.closest('.output'); 79 | if (outputEl) { 80 | x.observe(outputEl, {childList: true}); 81 | } 82 | END_POST_SCRIPT 83 | 84 | super(figure, element_id: element_id, post_script: post_script) 85 | end 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/charty/backends/plotly_helpers/plotly_renderer.rb: -------------------------------------------------------------------------------- 1 | require "date" 2 | require "json" 3 | require "time" 4 | 5 | module Charty 6 | module Backends 7 | module PlotlyHelpers 8 | class PlotlyRenderer 9 | def render(figure) 10 | json = JSON.generate(figure, allow_nan: true) 11 | case json 12 | when /\b(?:Infinity|NaN)\b/ 13 | visit(figure) 14 | else 15 | JSON.load(json) 16 | end 17 | end 18 | 19 | private def visit(obj) 20 | case obj 21 | when Integer, String, Symbol, true, false, nil 22 | obj 23 | 24 | when Numeric 25 | visit_float(obj) 26 | 27 | when Time 28 | visit_time(obj) 29 | 30 | when Date 31 | visit_date(obj) 32 | 33 | when DateTime 34 | visit_datetime(obj) 35 | 36 | when Array 37 | visit_array(obj) 38 | 39 | when Hash 40 | visit_hash(obj) 41 | 42 | when ->(x) { defined?(Numo::NArray) && obj.is_a?(Numo::NArray) } 43 | visit_array(obj.to_a) 44 | 45 | when ->(x) { defined?(Numpy::NDArray) && obj.is_a?(Numpy::NDArray) } 46 | visit_array(obj.to_a) 47 | 48 | when ->(x) { defined?(PyCall::List) && obj.is_a?(PyCall::List) } 49 | visit_array(obj.to_a) 50 | 51 | when ->(x) { defined?(PyCall::Tuple) && obj.is_a?(PyCall::Tuple) } 52 | visit_array(obj.to_a) 53 | 54 | when ->(x) { defined?(PyCall::Dict) && obj.is_a?(PyCall::Dict) } 55 | visit_hash(obj.to_h) 56 | 57 | when ->(x) { defined?(Pandas::Series) && obj.is_a?(Pandas::Series) } 58 | visit_array(obj.to_a) 59 | 60 | else 61 | str = String.try_convert(obj) 62 | return str unless str.nil? 63 | 64 | ary = Array.try_convert(obj) 65 | return visit_array(ary) unless ary.nil? 66 | 67 | hsh = Hash.try_convert(obj) 68 | return visit_hash(hsh) unless hsh.nil? 69 | 70 | type_error(obj) 71 | end 72 | end 73 | 74 | private def visit_float(obj) 75 | obj = obj.to_f 76 | rescue RangeError 77 | type_error(obj) 78 | else 79 | case 80 | when obj.finite? 81 | obj 82 | else 83 | nil 84 | end 85 | end 86 | 87 | private def visit_time(obj) 88 | obj.iso8601(6) 89 | end 90 | 91 | private def visit_date(obj) 92 | obj.iso8601(6) 93 | end 94 | 95 | private def visit_datetime(obj) 96 | obj.iso8601(6) 97 | end 98 | 99 | private def visit_array(obj) 100 | obj.map {|x| visit(x) } 101 | end 102 | 103 | private def visit_hash(obj) 104 | obj.map { |key, value| 105 | [ 106 | key, 107 | visit(value) 108 | ] 109 | }.to_h 110 | end 111 | 112 | private def type_error(obj) 113 | raise TypeError, "Unable to convert to JSON: %p" % obj 114 | end 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/charty/backends/rubyplot.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Charty 4 | module Backends 5 | class Rubyplot 6 | Backends.register(:rubyplot, self) 7 | 8 | class << self 9 | def prepare 10 | require 'rubyplot' 11 | end 12 | end 13 | 14 | def initialize 15 | @plot = ::Rubyplot 16 | end 17 | 18 | def label(x, y) 19 | end 20 | 21 | def series=(series) 22 | @series = series 23 | end 24 | 25 | def render_layout(layout) 26 | (_fig, axes) = *@plot.subplots(nrows: layout.num_rows, ncols: layout.num_cols) 27 | layout.rows.each_with_index do |row, y| 28 | row.each_with_index do |cel, x| 29 | plot = layout.num_rows > 1 ? axes[y][x] : axes[x] 30 | plot(plot, cel) 31 | end 32 | end 33 | @plot.show 34 | end 35 | 36 | def old_style_render(context, filename="") 37 | FileUtils.mkdir_p(File.dirname(filename)) 38 | plot(@plot, context).write(filename) 39 | end 40 | 41 | def plot(plot, context) 42 | # case 43 | # when plot.respond_to?(:xlim) 44 | # plot.xlim(context.range_x.begin, context.range_x.end) 45 | # plot.ylim(context.range_y.begin, context.range_y.end) 46 | # when plot.respond_to?(:set_xlim) 47 | # plot.set_xlim(context.range_x.begin, context.range_x.end) 48 | # plot.set_ylim(context.range_y.begin, context.range_y.end) 49 | # end 50 | 51 | figure = ::Rubyplot::Figure.new 52 | axes = figure.add_subplot 0,0 53 | axes.title = context.title if context.title 54 | axes.x_title = context.xlabel if context.xlabel 55 | axes.y_title = context.ylabel if context.ylabel 56 | 57 | case context.method 58 | when :bar 59 | context.series.each do |data| 60 | axes.bar! do |p| 61 | p.data(data.xs.to_a) 62 | p.label = data.label 63 | end 64 | end 65 | figure 66 | when :barh 67 | raise NotImplementedError 68 | when :box_plot 69 | raise NotImplementedError 70 | when :bubble 71 | context.series.each do |data| 72 | axes.bubble! do |p| 73 | p.data(data.xs.to_a, data.ys.to_a, data.zs.to_a) 74 | p.label = data.label if data.label 75 | end 76 | end 77 | figure 78 | when :curve 79 | context.series.each do |data| 80 | axes.line! do |p| 81 | p.data(data.xs.to_a, data.ys.to_a) 82 | p.label = data.label if data.label 83 | end 84 | end 85 | figure 86 | when :scatter 87 | context.series.each do |data| 88 | axes.scatter! do |p| 89 | p.data(data.xs.to_a, data.ys.to_a) 90 | p.label = data.label if data.label 91 | end 92 | end 93 | figure 94 | when :error_bar 95 | # refs. https://github.com/SciRuby/rubyplot/issues/26 96 | raise NotImplementedError 97 | when :hist 98 | raise NotImplementedError 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/charty/backends/unicode_plot.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | 3 | module Charty 4 | module Backends 5 | class UnicodePlot 6 | Backends.register(:unicode_plot, self) 7 | 8 | class << self 9 | def prepare 10 | require 'unicode_plot' 11 | end 12 | end 13 | 14 | def begin_figure 15 | @figure = nil 16 | @layout = {} 17 | end 18 | 19 | def bar(bar_pos, _group_names, values, colors, _orient, **kwargs) 20 | @figure = { 21 | type: :bar, 22 | bar_pos: bar_pos, 23 | values: values, 24 | } 25 | end 26 | 27 | def box_plot(plot_data, positions, orient:, **kwargs) 28 | @figure = { type: :box, data: plot_data, orient: orient } 29 | end 30 | 31 | def set_xlabel(label) 32 | @layout[:xlabel] = label 33 | end 34 | 35 | def set_ylabel(label) 36 | @layout[:ylabel] = label 37 | end 38 | 39 | def set_xticks(values) 40 | @layout[:xticks] = values 41 | end 42 | 43 | def set_xtick_labels(values) 44 | @layout[:xtick_labels] = values 45 | end 46 | 47 | def set_xlim(min, max) 48 | @layout[:xlim] = [min, max] 49 | end 50 | 51 | def disable_xaxis_grid 52 | # do nothing 53 | end 54 | 55 | def render(**kwargs) 56 | plot = case @figure[:type] 57 | when :bar 58 | ::UnicodePlot.barplot(@layout[:xtick_labels], @figure[:values], xlabel: @layout[:xlabel]) 59 | when :box 60 | xlabel = if @figure[:orient] == :v 61 | @layout[:ylabel] 62 | else 63 | @layout[:xlabel] 64 | end 65 | ::UnicodePlot.boxplot(@layout[:xtick_labels], @figure[:data], xlabel: xlabel) 66 | end 67 | sio = StringIO.new 68 | class << sio 69 | def tty?; true; end 70 | end 71 | plot.render(sio) 72 | sio.string 73 | end 74 | 75 | private 76 | 77 | def show_bar(sio, figure, i) 78 | end 79 | 80 | def show_box(sio, figure, i) 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/charty/cache_dir.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | module Charty 4 | module CacheDir 5 | module_function 6 | 7 | def cache_dir_path 8 | platform_cache_dir_path + "charty" 9 | end 10 | 11 | def platform_cache_dir_path 12 | base_dir = case RUBY_PLATFORM 13 | when /mswin/, /mingw/ 14 | ENV.fetch("LOCALAPPDATA", "~/AppData/Local") 15 | when /darwin/ 16 | "~/Library/Caches" 17 | else 18 | ENV.fetch("XDG_CACHE_HOME", "~/.cache") 19 | end 20 | Pathname(base_dir).expand_path 21 | end 22 | 23 | def path(*path_components) 24 | cache_dir_path.join(*path_components) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/charty/dash_pattern_generator.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module DashPatternGenerator 3 | NAMED_PATTERNS = { 4 | solid: "", 5 | dash: [4, 1.5], 6 | dot: [1, 1], 7 | dashdot: [3, 1.25, 1.5, 1.25], 8 | longdashdot: [5, 1, 1, 1], 9 | }.freeze 10 | 11 | def self.valid_name?(name) 12 | name = case name 13 | when Symbol, String 14 | name.to_sym 15 | else 16 | name.to_str.to_sym 17 | end 18 | NAMED_PATTERNS.key?(name) 19 | end 20 | 21 | def self.pattern_to_name(pattern) 22 | NAMED_PATTERNS.each do |key, val| 23 | return key if pattern == val 24 | end 25 | nil 26 | end 27 | 28 | def self.each 29 | return enum_for(__method__) unless block_given? 30 | 31 | NAMED_PATTERNS.each_value do |pattern| 32 | yield pattern 33 | end 34 | 35 | m = 3 36 | while true 37 | # Long and short dash combinations 38 | a = [3, 1.25].repeated_combination(m).to_a[1..-2].reverse 39 | b = [4, 1].repeated_combination(m).to_a[1..-2] 40 | 41 | # Interleave these combinations 42 | segment_list = a.zip(b).flatten(1) 43 | 44 | # Insert the gaps 45 | segment_list.each do |segment| 46 | gap = segment.min 47 | pattern = segment.map {|seg| [seg, gap] }.flatten 48 | yield pattern 49 | end 50 | 51 | m += 1 52 | end 53 | end 54 | 55 | extend Enumerable 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/charty/index.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | 3 | module Charty 4 | class Index 5 | extend Forwardable 6 | include Enumerable 7 | 8 | def initialize(values, name: nil) 9 | @values = values 10 | @name = name 11 | end 12 | 13 | attr_reader :values 14 | attr_accessor :name 15 | 16 | def_delegators :values, :length, :size, :each, :to_a 17 | 18 | def ==(other) 19 | case other 20 | when DaruIndex, PandasIndex 21 | return false if length != other.length 22 | to_a == other.to_a 23 | when Index 24 | return false if length != other.length 25 | return true if values == other.values 26 | to_a == other.to_a 27 | else 28 | super 29 | end 30 | end 31 | 32 | def [](i) 33 | case i 34 | when 0 ... length 35 | values[i] 36 | else 37 | raise IndexError, "index out of range" 38 | end 39 | end 40 | 41 | def loc(key) 42 | values.index(key) 43 | end 44 | 45 | def union(other) 46 | case other 47 | when PandasIndex 48 | index = PandasIndex.try_convert(self) 49 | return index.union(other) if index 50 | end 51 | 52 | Index.new(to_a.union(other.to_a), name: name) 53 | end 54 | end 55 | 56 | class RangeIndex < Index 57 | def initialize(values, name: nil) 58 | if values.is_a?(Range) && values.begin.is_a?(Integer) && values.end.is_a?(Integer) 59 | super 60 | else 61 | raise ArgumentError, "values must be an integer range" 62 | end 63 | end 64 | 65 | def length 66 | size 67 | end 68 | 69 | def [](i) 70 | case i 71 | when 0 ... length 72 | values.begin + i 73 | else 74 | raise IndexError, "index out of range (#{i} for 0 ... #{length})" 75 | end 76 | end 77 | 78 | def loc(key) 79 | case key 80 | when Integer 81 | if values.cover?(key) 82 | return key - values.begin 83 | end 84 | end 85 | end 86 | 87 | def union(other) 88 | case other 89 | when RangeIndex 90 | return union(other.values) 91 | when Range 92 | if disjoint_range?(values, other) 93 | return Index.new(values.to_a.union(other.to_a)) 94 | end 95 | new_beg = [values.begin, other.begin].min 96 | new_end = [values.end, other.end ].max 97 | new_range = if values.end < new_end 98 | if other.exclude_end? 99 | new_beg ... new_end 100 | else 101 | new_beg .. new_end 102 | end 103 | elsif other.end < new_end 104 | if values.exclude_end? 105 | new_beg ... new_end 106 | else 107 | new_beg .. new_end 108 | end 109 | else 110 | if values.exclude_end? && other.exclude_end? 111 | new_beg ... new_end 112 | else 113 | new_beg .. new_end 114 | end 115 | end 116 | RangeIndex.new(new_range) 117 | else 118 | super 119 | end 120 | end 121 | 122 | private def disjoint_range?(r1, r2) 123 | r1.end < r2.begin || r2.end < r1.begin 124 | end 125 | end 126 | 127 | class DaruIndex < Index 128 | def_delegators :values, :name, :name= 129 | 130 | def length 131 | size 132 | end 133 | 134 | def ==(other) 135 | case other 136 | when DaruIndex 137 | values == other.values 138 | else 139 | super 140 | end 141 | end 142 | end 143 | 144 | class PandasIndex < Index 145 | def self.try_convert(obj) 146 | case obj 147 | when PandasIndex 148 | obj 149 | when ->(x) { defined?(Pandas) && x.is_a?(Pandas::Index) } 150 | PandasIndex.new(obj) 151 | when RangeIndex, Range 152 | obj = obj.values if obj.is_a?(RangeIndex) 153 | stop = if obj.exclude_end? 154 | obj.end 155 | else 156 | obj.end + 1 157 | end 158 | PandasIndex.new(Pandas.RangeIndex.new(obj.begin, stop)) 159 | when ->(x) { defined?(Enumerator::ArithmeticSequence) && x.is_a?(Enumerator::ArithmeticSequence) } 160 | stop = if obj.exclude_end? 161 | obj.end 162 | else 163 | obj.end + 1 164 | end 165 | PandasIndex.new(Pandas::RangeIndex.new(obj.begin, stop, obj.step)) 166 | when Index, Array, DaruIndex, ->(x) { defined?(Daru) && x.is_a?(Daru::Index) } 167 | obj = obj.values if obj.is_a?(Index) 168 | PandasIndex.new(Pandas::Index.new(obj.to_a)) 169 | else 170 | nil 171 | end 172 | end 173 | 174 | def_delegators :values, :name, :name= 175 | 176 | def length 177 | size 178 | end 179 | 180 | def ==(other) 181 | case other 182 | when PandasIndex 183 | Numpy.all(values == other.values) 184 | when Index 185 | return false if length != other.length 186 | Numpy.all(values == other.values.to_a) 187 | else 188 | super 189 | end 190 | end 191 | 192 | def each(&block) 193 | return enum_for(__method__) unless block_given? 194 | 195 | i, n = 0, length 196 | while i < n 197 | yield self[i] 198 | i += 1 199 | end 200 | end 201 | 202 | def loc(key) 203 | case values 204 | when Pandas::Index 205 | values.get_loc(key) 206 | else 207 | super 208 | end 209 | end 210 | 211 | def union(other) 212 | other = PandasIndex.try_convert(other) 213 | # NOTE: Using `sort=False` in pandas.Index#union does not produce pandas.RangeIndex. 214 | # TODO: Reconsider to use `sort=True` here. 215 | PandasIndex.new(values.union(other.values, sort: false)) 216 | end 217 | 218 | private def arithmetic_sequence?(x) 219 | defined?(Enumerator::ArithmeticSequence) && x.is_a?(Enumerator::ArithmeticSequence) 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /lib/charty/iruby_helper.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module IRubyHelper 3 | module_function 4 | 5 | def iruby_notebook? 6 | # TODO: This cannot distinguish notebook and console. 7 | defined?(IRuby) 8 | end 9 | 10 | def vscode? 11 | ENV.key?("VSCODE_PID") 12 | end 13 | 14 | def nteract? 15 | ENV.key?("NTERACT_EXE") 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/charty/layout.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | class Layout 3 | def initialize(frontend, definition = :horizontal) 4 | @frontend = frontend 5 | @layout = parse_definition(definition) 6 | end 7 | 8 | def parse_definition(definition) 9 | case definition 10 | when :horizontal 11 | ArrayLayout.new 12 | when :vertical 13 | ArrayLayout.new(:vertical) 14 | else 15 | if match = definition.to_s.match(/\Agrid(\d+)x(\d+)\z/) 16 | num_cols = match[1].to_i 17 | num_rows = match[2].to_i 18 | GridLayout.new(num_cols, num_rows) 19 | end 20 | end 21 | end 22 | 23 | def <<(content) 24 | if content.respond_to?(:each) 25 | content.each {|c| self << c } 26 | else 27 | @layout << content 28 | end 29 | nil 30 | end 31 | 32 | def render(filename="") 33 | @frontend.render_layout(@layout) 34 | end 35 | end 36 | 37 | class ArrayLayout 38 | def initialize(direction=:horizontal) 39 | @array = [] 40 | @direction = direction 41 | end 42 | 43 | def <<(content) 44 | @array << content 45 | end 46 | 47 | def num_rows 48 | @direction == :horizontal ? 1 : @array.count 49 | end 50 | 51 | def num_cols 52 | @direction == :vertical ? 1 : @array.count 53 | end 54 | 55 | def rows 56 | [@array] 57 | end 58 | end 59 | 60 | class GridLayout 61 | attr_reader :num_rows, :num_cols, :rows 62 | 63 | def initialize(num_cols, num_rows) 64 | @rows = Array.new(num_rows) { Array.new(num_cols) } 65 | @num_cols = num_cols 66 | @num_rows = num_rows 67 | @cursor = 0 68 | end 69 | 70 | def <<(content) 71 | @rows[@cursor / @num_rows][@cursor % @num_cols] = content 72 | @cursor += 1 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/charty/linspace.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | class Linspace 3 | include Enumerable 4 | 5 | def initialize(range, num_step) 6 | @range = range 7 | @num_step = num_step 8 | end 9 | 10 | def each(&block) 11 | if @num_step == 1 12 | block.call(@range.begin) 13 | else 14 | step = (@range.end - @range.begin).to_r / (@num_step - 1) 15 | (@num_step - 1).times do |i| 16 | block.call(@range.begin + i * step) 17 | end 18 | 19 | unless @range.exclude_end? 20 | block.call(@range.end) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/charty/plotters.rb: -------------------------------------------------------------------------------- 1 | require_relative "plotters/abstract_plotter" 2 | require_relative "plotters/random_support" 3 | require_relative "plotters/estimation_support" 4 | require_relative "plotters/categorical_plotter" 5 | require_relative "plotters/bar_plotter" 6 | require_relative "plotters/box_plotter" 7 | require_relative "plotters/count_plotter" 8 | 9 | require_relative "plotters/vector_plotter" 10 | require_relative "plotters/relational_plotter" 11 | require_relative "plotters/scatter_plotter" 12 | require_relative "plotters/line_plotter" 13 | 14 | require_relative "plotters/distribution_plotter" 15 | require_relative "plotters/histogram_plotter" 16 | -------------------------------------------------------------------------------- /lib/charty/plotters/box_plotter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Plotters 3 | class BoxPlotter < CategoricalPlotter 4 | self.default_palette = :light 5 | self.require_numeric = true 6 | 7 | def initialize(data: nil, variables: {}, **options, &block) 8 | x, y, color = variables.values_at(:x, :y, :color) 9 | super(x, y, color, data: data, **options, &block) 10 | end 11 | 12 | attr_reader :flier_size 13 | 14 | def flier_size=(val) 15 | @flier_size = check_number(val, :flier_size, allow_nil: true) 16 | end 17 | 18 | attr_reader :line_width 19 | 20 | def line_width=(val) 21 | @line_width = check_number(val, :line_width, allow_nil: true) 22 | end 23 | 24 | attr_reader :whisker 25 | 26 | def whisker=(val) 27 | @whisker = check_number(val, :whisker, allow_nil: true) 28 | end 29 | 30 | private def render_plot(backend, **) 31 | draw_box_plot(backend) 32 | annotate_axes(backend) 33 | backend.invert_yaxis if orient == :h 34 | end 35 | 36 | private def draw_box_plot(backend) 37 | if @plot_colors.nil? 38 | plot_data = @plot_data.map do |group_data| 39 | unless group_data.empty? 40 | group_data = group_data.drop_na 41 | group_data unless group_data.empty? 42 | end 43 | end 44 | 45 | backend.box_plot(plot_data, 46 | @group_names, 47 | orient: orient, 48 | colors: @colors, 49 | gray: @gray, 50 | dodge: dodge, 51 | width: @width, 52 | flier_size: flier_size, 53 | whisker: whisker) 54 | else 55 | grouped_box_data = @color_names.map.with_index do |color_name, i| 56 | @plot_data.map.with_index do |group_data, j| 57 | unless group_data.empty? 58 | color_mask = @plot_colors[j].eq(color_name) 59 | group_data = group_data[color_mask].drop_na 60 | group_data unless group_data.empty? 61 | end 62 | end 63 | end 64 | 65 | backend.grouped_box_plot(grouped_box_data, 66 | @group_names, 67 | @color_names, 68 | orient: orient, 69 | colors: @colors, 70 | gray: @gray, 71 | dodge: dodge, 72 | width: @width, 73 | flier_size: flier_size, 74 | whisker: whisker) 75 | end 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/charty/plotters/count_plotter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Plotters 3 | class CountPlotter < BarPlotter 4 | self.require_numeric = false 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/charty/plotters/distribution_plotter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Plotters 3 | class DistributionPlotter < AbstractPlotter 4 | def flat_structure 5 | { 6 | x: :@values 7 | } 8 | end 9 | 10 | def wide_structure 11 | { 12 | x: :@values, 13 | color: :@columns 14 | } 15 | end 16 | 17 | def initialize(data:, variables:, **options, &block) 18 | x, y, color = variables.values_at(:x, :y, :color) 19 | super(x, y, color, data: data, **options, &block) 20 | 21 | setup_variables 22 | end 23 | 24 | attr_reader :weights 25 | 26 | def weights=(val) 27 | @weights = check_dimension(val, :weights) 28 | end 29 | 30 | attr_reader :variables 31 | 32 | attr_reader :color_norm 33 | 34 | def color_norm=(val) 35 | unless val.nil? 36 | raise NotImplementedError, 37 | "Specifying color_norm is not supported yet" 38 | end 39 | end 40 | 41 | attr_reader :legend 42 | 43 | def legend=(val) 44 | @legend = check_legend(val) 45 | end 46 | 47 | private def check_legend(val) 48 | check_boolean(val, :legend) 49 | end 50 | 51 | attr_reader :input_format, :plot_data, :variables, :var_types 52 | 53 | # This should be the same as one in RelationalPlotter 54 | # TODO: move this to AbstractPlotter and refactor with CategoricalPlotter 55 | private def setup_variables 56 | if x.nil? && y.nil? 57 | @input_format = :wide 58 | setup_variables_with_wide_form_dataset 59 | else 60 | @input_format = :long 61 | setup_variables_with_long_form_dataset 62 | end 63 | 64 | @var_types = @plot_data.columns.map { |k| 65 | [k, variable_type(@plot_data[k], :categorical)] 66 | }.to_h 67 | end 68 | 69 | private def setup_variables_with_wide_form_dataset 70 | unless color.nil? 71 | raise ArgumentError, 72 | "Unable to assign the following variables in wide-form data: color" 73 | end 74 | 75 | if data.nil? || data.empty? 76 | @plot_data = Charty::Table.new({}) 77 | @variables = {} 78 | return 79 | end 80 | 81 | flat = data.is_a?(Charty::Vector) 82 | if flat 83 | @plot_data = {} 84 | @variables = {} 85 | 86 | [:x, :y].each do |var| 87 | case self.flat_structure[var] 88 | when :@index 89 | @plot_data[var] = data.index.to_a 90 | @variables[var] = data.index.name 91 | when :@values 92 | @plot_data[var] = data.to_a 93 | @variables[var] = data.name 94 | end 95 | end 96 | 97 | @plot_data = Charty::Table.new(@plot_data) 98 | else 99 | numeric_columns = @data.column_names.select do |cn| 100 | @data[cn].numeric? 101 | end 102 | wide_data = @data[numeric_columns] 103 | 104 | melt_params = {var_name: :@columns, value_name: :@values } 105 | if self.wide_structure.include?(:index) 106 | melt_params[:id_vars] = :@index 107 | end 108 | 109 | @plot_data = wide_data.melt(**melt_params) 110 | @variables = {} 111 | self.wide_structure.each do |var, attr| 112 | @plot_data[var] = @plot_data[attr] 113 | 114 | @variables[var] = case attr 115 | when :@columns 116 | wide_data.columns.name 117 | when :@index 118 | wide_data.index.name 119 | else 120 | nil 121 | end 122 | end 123 | 124 | @plot_data = @plot_data[self.wide_structure.keys] 125 | end 126 | end 127 | 128 | private def setup_variables_with_long_form_dataset 129 | if data.nil? || data.empty? 130 | @plot_data = Charty::Table.new({}) 131 | @variables = {} 132 | return 133 | end 134 | 135 | plot_data = {} 136 | variables = {} 137 | 138 | { 139 | x: self.x, 140 | y: self.y, 141 | color: self.color, 142 | weights: self.weights 143 | }.each do |key, val| 144 | next if val.nil? 145 | 146 | if data.column?(val) 147 | plot_data[key] = data[val] 148 | variables[key] = val 149 | else 150 | case val 151 | when Charty::Vector 152 | plot_data[key] = val 153 | variables[key] = val.name 154 | else 155 | raise ArgumentError, 156 | "Could not interpret value %p for parameter %p" % [val, key] 157 | end 158 | end 159 | end 160 | 161 | @plot_data = Charty::Table.new(plot_data) 162 | @variables = variables.select do |var, name| 163 | @plot_data[var].notnull.any? 164 | end 165 | end 166 | 167 | private def map_color(palette: nil, order: nil, norm: nil) 168 | @color_mapper = ColorMapper.new(self, palette, order, norm) 169 | end 170 | 171 | private def map_size(sizes: nil, order: nil, norm: nil) 172 | @size_mapper = SizeMapper.new(self, sizes, order, norm) 173 | end 174 | 175 | private def map_style(markers: nil, dashes: nil, order: nil) 176 | @style_mapper = StyleMapper.new(self, markers, dashes, order) 177 | end 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /lib/charty/plotters/estimation_support.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Plotters 3 | module EstimationSupport 4 | attr_reader :estimator 5 | 6 | def estimator=(estimator) 7 | @estimator = check_estimator(estimator) 8 | end 9 | 10 | module_function def check_estimator(value) 11 | case value 12 | when :count, "count" 13 | :count 14 | when :mean, "mean" 15 | :mean 16 | when :median 17 | raise NotImplementedError, 18 | "median estimator has not been supported yet" 19 | when Proc 20 | raise NotImplementedError, 21 | "a callable estimator has not been supported yet" 22 | else 23 | raise ArgumentError, 24 | "invalid value for estimator (%p for :mean)" % value 25 | end 26 | end 27 | 28 | attr_reader :ci 29 | 30 | def ci=(ci) 31 | @ci = check_ci(ci) 32 | end 33 | 34 | private def check_ci(value) 35 | case value 36 | when nil 37 | nil 38 | when :sd, "sd" 39 | :sd 40 | when 0..100 41 | value 42 | when Numeric 43 | raise ArgumentError, 44 | "ci must be in 0..100, but %p is given" % value 45 | else 46 | raise ArgumentError, 47 | "invalid value for ci (%p for nil, :sd, or a number in 0..100)" % value 48 | end 49 | end 50 | 51 | attr_reader :n_boot 52 | 53 | def n_boot=(n_boot) 54 | @n_boot = check_n_boot(n_boot) 55 | end 56 | 57 | private def check_n_boot(value) 58 | case value 59 | when Integer 60 | if value <= 0 61 | raise ArgumentError, 62 | "n_boot must be larger than zero, but %p is given" % value 63 | end 64 | value 65 | else 66 | raise ArgumentError, 67 | "invalid value for n_boot (%p for an integer > 0)" % value 68 | end 69 | end 70 | 71 | attr_reader :units 72 | 73 | def units=(units) 74 | @units = check_dimension(units, :units) 75 | unless units.nil? 76 | raise NotImplementedError, 77 | "Specifying units variable is not supported yet" 78 | end 79 | end 80 | 81 | include RandomSupport 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/charty/plotters/random_support.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Plotters 3 | module RandomSupport 4 | attr_reader :random 5 | 6 | def random=(random) 7 | @random = check_random(random) 8 | end 9 | 10 | module_function def check_random(random) 11 | case random 12 | when nil 13 | Random.new 14 | when Integer 15 | Random.new(random) 16 | when Random 17 | random 18 | else 19 | raise ArgumentError, 20 | "invalid value for random (%p for a generator or a seed value)" % value 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/charty/plotters/scatter_plotter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Plotters 3 | class ScatterPlotter < RelationalPlotter 4 | def initialize(data: nil, variables: {}, **options, &block) 5 | x, y, color, style, size = variables.values_at(:x, :y, :color, :style, :size) 6 | super(x, y, color, style, size, data: data, **options, &block) 7 | end 8 | 9 | attr_reader :alpha 10 | 11 | def alpha=(val) 12 | case val 13 | when nil, :auto, 0..1 14 | @alpha = val 15 | when "auto" 16 | @alpha = val.to_sym 17 | when Numeric 18 | raise ArgumentError, 19 | "the given alpha is out of bounds " + 20 | "(%p for nil, :auto, or number 0..1)" % val 21 | else 22 | raise ArgumentError, 23 | "invalid value of alpha " + 24 | "(%p for nil, :auto, or number in 0..1)" % val 25 | end 26 | end 27 | 28 | attr_reader :line_width, :edge_color 29 | 30 | def line_width=(val) 31 | @line_width = check_number(val, :line_width, allow_nil: true) 32 | end 33 | 34 | def edge_color=(val) 35 | @line_width = check_color(val, :edge_color, allow_nil: true) 36 | end 37 | 38 | private def render_plot(backend, **) 39 | draw_points(backend) 40 | annotate_axes(backend) 41 | end 42 | 43 | private def draw_points(backend) 44 | map_color(palette: palette, order: color_order, norm: color_norm) 45 | map_size(sizes: sizes, order: size_order, norm: size_norm) 46 | map_style(markers: markers, order: style_order) 47 | 48 | data = @plot_data.drop_na 49 | 50 | # TODO: shold pass key_color to backend's scatter method. 51 | # In pyplot backend, it is passed as color parameter. 52 | 53 | x = data[:x] 54 | y = data[:y] 55 | color = data[:color] if @variables.key?(:color) 56 | style = data[:style] if @variables.key?(:style) 57 | size = data[:size] if @variables.key?(:size) 58 | 59 | # TODO: key_color 60 | backend.scatter( 61 | x, y, @variables, 62 | color: color, color_mapper: @color_mapper, 63 | style: style, style_mapper: @style_mapper, 64 | size: size, size_mapper: @size_mapper 65 | ) 66 | 67 | if legend 68 | backend.add_scatter_plot_legend(@variables, @color_mapper, @size_mapper, @style_mapper, legend) 69 | end 70 | end 71 | 72 | private def annotate_axes(backend) 73 | backend.set_title(self.title) if self.title 74 | 75 | xlabel = self.x_label || self.variables[:x] 76 | ylabel = self.y_label || self.variables[:y] 77 | backend.set_xlabel(xlabel) unless xlabel.nil? 78 | backend.set_ylabel(ylabel) unless ylabel.nil? 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/charty/plotters/vector_plotter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Plotters 3 | class VectorPlotter < AbstractPlotter 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/charty/statistics.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Statistics 3 | begin 4 | require "enumerable/statistics" 5 | 6 | def self.mean(enum) 7 | enum.mean 8 | end 9 | 10 | def self.stdev(enum, population: false) 11 | enum.stdev(population: population) 12 | end 13 | 14 | def self.histogram(ary, *args, **kwargs) 15 | ary.histogram(*args, **kwargs) 16 | end 17 | rescue LoadError 18 | def self.mean(enum) 19 | xs = enum.to_a 20 | xs.sum / xs.length.to_f 21 | end 22 | 23 | def self.stdev(enum, population: false) 24 | xs = enum.to_a 25 | n = xs.length 26 | mean = xs.sum.to_f / n 27 | ddof = population ? 0 : 1 28 | var = xs.map {|x| (x - mean)**2 }.sum / (n - ddof) 29 | Math.sqrt(var) 30 | end 31 | 32 | def self.histogram(ary, *args, **kwargs) 33 | raise NotImplementedError, 34 | "histogram is currently supported only with enumerable-statistics" 35 | end 36 | end 37 | 38 | def self.bootstrap(vector, n_boot: 2000, func: :mean, units: nil, random: nil) 39 | n = vector.size 40 | random = Charty::Plotters::RandomSupport.check_random(random) 41 | func = Charty::Plotters::EstimationSupport.check_estimator(func) 42 | 43 | if units 44 | return structured_bootstrap(vector, n_boot, units, func, random) 45 | end 46 | 47 | if defined?(Pandas::Series) || defined?(Numpy::NDArray) 48 | boot_dist = bootstrap_optimized_for_pycall(vector, n_boot, random, func) 49 | return boot_dist if boot_dist 50 | end 51 | 52 | boot_dist = Array.new(n_boot) do |i| 53 | resampler = Array.new(n) { random.rand(n) } 54 | 55 | w ||= vector.values_at(*resampler) 56 | 57 | case func 58 | when :mean 59 | mean(w) 60 | end 61 | end 62 | 63 | boot_dist 64 | end 65 | 66 | private_class_method def self.bootstrap_optimized_for_pycall(vector, n_boot, random, func) 67 | case 68 | when vector.is_a?(Charty::Vector) 69 | bootstrap_optimized_for_pycall(vector.data, n_boot, random, func) 70 | 71 | when defined?(Pandas::Series) && vector.is_a?(Pandas::Series) || vector.is_a?(Numpy::NDArray) 72 | # numpy is also available when pandas is available 73 | n = vector.size 74 | resampler = Numpy.empty(n, dtype: Numpy.intp) 75 | Array.new(n_boot) do |i| 76 | # TODO: Use Numo and MemoryView to reduce execution time 77 | # resampler = Numo::Int64.new(n).rand(n) 78 | # w = Numpy.take(vector, resampler) 79 | n.times {|i| resampler[i] = random.rand(n) } 80 | w = vector.take(resampler) 81 | 82 | case func 83 | when :mean 84 | w.mean 85 | end 86 | end 87 | end 88 | end 89 | 90 | private_class_method def self.structured_bootstrap(vector, n_boot, units, func, random) 91 | raise NotImplementedError, 92 | "structured bootstrapping has not been supported yet" 93 | end 94 | 95 | def self.bootstrap_ci(*vectors, width, n_boot: 2000, func: :mean, units: nil, random: nil) 96 | boot = bootstrap(*vectors, n_boot: n_boot, func: func, units: units, random: random) 97 | q = [50 - width / 2, 50 + width / 2] 98 | if boot.respond_to?(:percentile) 99 | boot.percentile(q) 100 | else 101 | percentile(boot, q) 102 | end 103 | end 104 | 105 | # TODO: optimize with introselect algorithm 106 | def self.percentile(a, q) 107 | return mean(a) if a.size == 0 108 | 109 | a = a.sort 110 | n = a.size 111 | q.map do |x| 112 | x = (n-1) * (x / 100.0) 113 | i = x.floor 114 | if i == x 115 | a[i] 116 | else 117 | t = x - i 118 | (1-t)*a[i] + t*a[i+1] 119 | end 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/charty/table.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Charty 4 | class ColumnAccessor 5 | def initialize(adapter) 6 | @adapter = adapter 7 | end 8 | 9 | def [](column_name) 10 | @adapter[nil, column_name] 11 | end 12 | end 13 | 14 | class Table 15 | extend Forwardable 16 | 17 | def initialize(data, **kwargs) 18 | adapter_class = TableAdapters.find_adapter_class(data) 19 | if kwargs.empty? 20 | @adapter = adapter_class.new(data) 21 | else 22 | @adapter = adapter_class.new(data, **kwargs) 23 | end 24 | 25 | @column_cache = {} 26 | end 27 | 28 | attr_reader :adapter 29 | 30 | def_delegators :adapter, :length, :column_length 31 | 32 | def_delegators :adapter, :columns, :columns= 33 | def_delegators :adapter, :index, :index= 34 | 35 | def_delegators :@adapter, :column_names, :column? 36 | 37 | def_delegator :@adapter, :data, :raw_data 38 | 39 | def ==(other) 40 | return true if equal?(other) 41 | 42 | case other 43 | when Charty::Table 44 | adapter == other.adapter 45 | else 46 | super 47 | end 48 | end 49 | 50 | def empty? 51 | length == 0 52 | end 53 | 54 | def [](key) 55 | case key 56 | when Array 57 | @adapter[nil, key] 58 | else 59 | key = case key 60 | when Symbol 61 | key 62 | else 63 | String.try_convert(key).to_sym 64 | end 65 | if @column_cache.key?(key) 66 | @column_cache[key] 67 | else 68 | @column_cache[key] = @adapter[nil, key] 69 | end 70 | end 71 | end 72 | 73 | def []=(key, values) 74 | case key 75 | when Array 76 | raise ArgumentError, 77 | "Substituting multiple keys is not supported" 78 | when Symbol 79 | # do nothing 80 | else 81 | key = key.to_str.to_sym 82 | end 83 | @adapter[key] = values 84 | end 85 | 86 | def group_by(grouper, sort: true, drop_na: true) 87 | adapter.group_by(self, grouper, sort, drop_na) 88 | end 89 | 90 | def to_a(x=nil, y=nil, z=nil) 91 | case 92 | when defined?(Daru::DataFrame) && table.kind_of?(Daru::DataFrame) 93 | table.map(&:to_a) 94 | when defined?(Numo::NArray) && table.kind_of?(Numo::NArray) 95 | table.to_a 96 | when defined?(ActiveRecord::Relation) && table.kind_of?(ActiveRecord::Relation) 97 | if z && x && y 98 | [table.pluck(x), table.pluck(y), table.pluck(z)] 99 | elsif x && y 100 | [table.pluck(x), table.pluck(y)] 101 | else 102 | raise ArgumentError, "column_names is required to convert to_a from ActiveRecord::Relation" 103 | end 104 | when table.kind_of?(Array) 105 | table 106 | else 107 | raise ArgumentError, "unsupported object: #{table.inspect}" 108 | end 109 | end 110 | 111 | def each 112 | return to_enum(__method__) unless block_given? 113 | data = to_a 114 | i, n = 0, data.size 115 | while i < n 116 | yield data[i] 117 | i += 1 118 | end 119 | end 120 | 121 | def drop_na 122 | @adapter.drop_na || self 123 | end 124 | 125 | def_delegator :adapter, :sort_values 126 | 127 | def_delegator :adapter, :reset_index 128 | 129 | def_delegator :adapter, :melt 130 | 131 | class GroupByBase 132 | end 133 | 134 | class HashGroupBy < GroupByBase 135 | def initialize(table, grouper, sort, drop_na) 136 | @table = table 137 | @grouper = check_grouper(grouper) 138 | init_groups(sort, drop_na) 139 | end 140 | 141 | private def check_grouper(grouper) 142 | case grouper 143 | when Symbol, String, Array 144 | # TODO check column existence 145 | return grouper 146 | when Charty::Vector 147 | if @table.length != grouper.length 148 | raise ArgumentError, 149 | "Wrong number of items in grouper array " + 150 | "(%p for %p)" % [val.length, @table.length] 151 | end 152 | return grouper 153 | when ->(x) { x.respond_to?(:call) } 154 | raise NotImplementedError, 155 | "A callable grouper is unsupported" 156 | else 157 | raise ArgumentError, 158 | "Unable to recognize the value for `grouper`: %p" % val 159 | end 160 | end 161 | 162 | private def init_groups(sort, drop_na) 163 | case @grouper 164 | when Symbol, String 165 | column = @table[@grouper] 166 | @indices = (0 ... @table.length).group_by do |i| 167 | column.data[i] 168 | end 169 | when Array 170 | @indices = (0 ... @table.length).group_by { |i| 171 | @grouper.map {|j| @table[j].data[i] } 172 | } 173 | when Charty::Vector 174 | @indices = (0 ... @table.length).group_by do |i| 175 | @grouper.data[i] 176 | end 177 | end 178 | 179 | if drop_na 180 | case @grouper 181 | when Array 182 | @indices.reject! {|key, | key.any? {|k| Util.missing?(k) } } 183 | else 184 | @indices.reject! {|key, | Util.missing?(key) } 185 | end 186 | end 187 | 188 | if sort 189 | @indices = @indices.sort_by {|key, | key }.to_h 190 | end 191 | end 192 | 193 | def indices 194 | @indices.dup 195 | end 196 | 197 | def group_keys 198 | @indices.keys 199 | end 200 | 201 | def each_group_key(&block) 202 | @indices.each_key(&block) 203 | end 204 | 205 | def apply(*args, &block) 206 | Charty::Table.new( 207 | each_group.map { |_key, table| 208 | block.call(table, *args) 209 | }, 210 | index: Charty::Index.new(@indices.keys, name: @grouper) 211 | ) 212 | end 213 | 214 | def each_group 215 | return enum_for(__method__) unless block_given? 216 | 217 | @indices.each_key do |key| 218 | yield(key, self[key]) 219 | end 220 | end 221 | 222 | def [](key) 223 | return nil unless @indices.key?(key) 224 | 225 | index = @indices[key] 226 | Charty::Table.new( 227 | @table.column_names.map {|col| 228 | [col, @table[col].values_at(*index)] 229 | }.to_h, 230 | index: index 231 | ) 232 | end 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /lib/charty/table_adapters.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module TableAdapters 3 | @adapters = {} 4 | 5 | def self.register(name, adapter_class) 6 | @adapters[name] = adapter_class 7 | end 8 | 9 | def self.find_adapter_class(data) 10 | @adapters.each_value do |adapter_class| 11 | return adapter_class if adapter_class.supported?(data) 12 | end 13 | raise ArgumentError, "Unsupported data class: #{data.class}" 14 | end 15 | end 16 | end 17 | 18 | require_relative 'table_adapters/base_adapter' 19 | require_relative 'table_adapters/hash_adapter' 20 | require_relative 'table_adapters/narray_adapter' 21 | require_relative 'table_adapters/datasets_adapter' 22 | require_relative 'table_adapters/daru_adapter' 23 | require_relative 'table_adapters/active_record_adapter' 24 | require_relative 'table_adapters/pandas_adapter' 25 | require_relative 'table_adapters/arrow_adapter' 26 | -------------------------------------------------------------------------------- /lib/charty/table_adapters/active_record_adapter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module TableAdapters 3 | class ActiveRecordAdapter < BaseAdapter 4 | TableAdapters.register(:active_record, self) 5 | 6 | def self.supported?(data) 7 | defined?(ActiveRecord::Relation) && data.is_a?(ActiveRecord::Relation) 8 | end 9 | 10 | def initialize(data) 11 | @data = check_type(ActiveRecord::Relation, data, :data) 12 | @column_names = @data.column_names.freeze 13 | self.columns = Index.new(@column_names) 14 | self.index = RangeIndex.new(0 ... length) 15 | end 16 | 17 | attr_reader :data, :column_names 18 | 19 | def_delegators :data, :size 20 | 21 | alias length size 22 | 23 | def column_length 24 | column_names.length 25 | end 26 | 27 | def [](row, column) 28 | fetch_records unless @columns_cache 29 | if row 30 | @columns_cache[resolve_column_index(column)][row] 31 | else 32 | column_data = @columns_cache[resolve_column_index(column)] 33 | Vector.new(column_data, index: index, name: column) 34 | end 35 | end 36 | 37 | private def resolve_column_index(column) 38 | case column 39 | when String, Symbol 40 | index = column_names.index(column.to_s) 41 | unless index 42 | raise IndexError, "invalid column name: #{column.inspect}" 43 | end 44 | index 45 | when Integer 46 | column 47 | else 48 | message = "column must be String or Integer: #{column.inspect}" 49 | raise ArgumentError, message 50 | end 51 | end 52 | 53 | private def fetch_records 54 | @columns_cache = @data.pluck(*column_names).transpose 55 | end 56 | 57 | private def check_type(type, data, name) 58 | return data if data.is_a?(type) 59 | raise TypeError, "#{name} must be a #{type}" 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/charty/table_adapters/arrow_adapter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module TableAdapters 3 | class ArrowAdapter < BaseAdapter 4 | TableAdapters.register(:arrow, self) 5 | 6 | def self.supported?(data) 7 | defined?(Arrow::Table) && data.is_a?(Arrow::Table) 8 | end 9 | 10 | def initialize(data) 11 | @data = data 12 | @column_names = @data.columns.map(&:name) 13 | self.columns = Index.new(@column_names) 14 | self.index = RangeIndex.new(0 ... length) 15 | end 16 | 17 | attr_reader :data 18 | 19 | def length 20 | @data.n_rows 21 | end 22 | 23 | def column_length 24 | @column_names.length 25 | end 26 | 27 | def compare_data_equality(other) 28 | case other 29 | when ArrowAdapter 30 | data == other.data 31 | else 32 | super 33 | end 34 | end 35 | 36 | def [](row, column) 37 | if row 38 | @data[column][row] 39 | else 40 | case column 41 | when Array 42 | Table.new(@data.select_columns(*column)) 43 | else 44 | column_data = @data[column] 45 | Vector.new(column_data.data.combine, 46 | index: index, 47 | name: column_data.name) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/charty/table_adapters/daru_adapter.rb: -------------------------------------------------------------------------------- 1 | require "delegate" 2 | 3 | module Charty 4 | module TableAdapters 5 | class DaruAdapter < BaseAdapter 6 | TableAdapters.register(:daru, self) 7 | 8 | extend Forwardable 9 | include Enumerable 10 | 11 | def self.supported?(data) 12 | defined?(Daru::DataFrame) && data.is_a?(Daru::DataFrame) 13 | end 14 | 15 | def initialize(data, columns: nil, index: nil) 16 | @data = check_type(Daru::DataFrame, data, :data) 17 | 18 | self.columns = columns unless columns.nil? 19 | self.index = index unless index.nil? 20 | end 21 | 22 | attr_reader :data 23 | 24 | def_delegator :data, :size, :length 25 | 26 | def index 27 | DaruIndex.new(data.index) 28 | end 29 | 30 | def_delegator :data, :index= 31 | 32 | def columns 33 | DaruIndex.new(data.vectors) 34 | end 35 | 36 | def columns=(values) 37 | data.vectors = Daru::Index.coerce(values) 38 | end 39 | 40 | def column_names 41 | @data.vectors.to_a 42 | end 43 | 44 | def compare_data_equality(other) 45 | case other 46 | when DaruAdapter 47 | data == other.data 48 | else 49 | super 50 | end 51 | end 52 | 53 | def [](row, column) 54 | if row 55 | @data[column][row] 56 | else 57 | column_data = if @data.has_vector?(column) 58 | @data[column] 59 | else 60 | case column 61 | when String 62 | @data[column.to_sym] 63 | else 64 | @data[column.to_s] 65 | end 66 | end 67 | Vector.new(column_data) 68 | end 69 | end 70 | 71 | def []=(key, values) 72 | case key 73 | when Symbol 74 | sym_key = key 75 | str_key = key.to_s 76 | else 77 | str_key = key.to_str 78 | sym_key = str_key.to_sym 79 | end 80 | case 81 | when @data.has_vector?(sym_key) 82 | key = sym_key 83 | when @data.has_vector?(str_key) 84 | key = str_key 85 | end 86 | 87 | case values 88 | when Charty::Vector 89 | case values.adapter 90 | when Charty::VectorAdapters::DaruVectorAdapter 91 | @data[key] = values.adapter.data 92 | else 93 | @data[key] = values.to_a 94 | end 95 | else 96 | orig_values = values 97 | values = Array.try_convert(values) 98 | if values.nil? 99 | raise ArgumentError, "`values` must be convertible to Array" 100 | end 101 | @data[key] = values 102 | end 103 | return values 104 | end 105 | 106 | private def check_type(type, data, name) 107 | return data if data.is_a?(type) 108 | raise TypeError, "#{name} must be a #{type}" 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/charty/table_adapters/datasets_adapter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module TableAdapters 3 | class DatasetsAdapter < BaseAdapter 4 | TableAdapters.register(:datasets, self) 5 | 6 | include Enumerable 7 | 8 | def self.supported?(data) 9 | defined?(Datasets::Dataset) && 10 | data.is_a?(Datasets::Dataset) 11 | end 12 | 13 | def initialize(dataset) 14 | @table = dataset.to_table 15 | @records = [] 16 | 17 | self.columns = self.column_names 18 | self.index = 0 ... length 19 | end 20 | 21 | def data 22 | @table 23 | end 24 | 25 | def column_length 26 | column_names.length 27 | end 28 | 29 | def column_names 30 | @table.column_names 31 | end 32 | 33 | def length 34 | data.n_rows 35 | end 36 | 37 | def each(&block) 38 | return to_enum(__method__) unless block_given? 39 | 40 | @table.each_record(&block) 41 | end 42 | 43 | # @param [Integer] row Row index 44 | # @param [Symbol,String,Integer] column Column index 45 | def [](row, column) 46 | if row 47 | record = @table.find_record(row) 48 | return nil if record.nil? 49 | record[column] 50 | else 51 | Vector.new(@table[column], index: index, name: column) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/charty/table_adapters/narray_adapter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module TableAdapters 3 | class NArrayAdapter < BaseAdapter 4 | TableAdapters.register(:narray, self) 5 | 6 | def self.supported?(data) 7 | defined?(Numo::NArray) && data.is_a?(Numo::NArray) && data.ndim <= 2 8 | end 9 | 10 | def initialize(data, columns: nil, index: nil) 11 | case data.ndim 12 | when 1 13 | data = data.reshape(data.length, 1) 14 | when 2 15 | # do nothing 16 | else 17 | raise ArgumentError, "Unsupported data format" 18 | end 19 | @data = data 20 | self.columns = Index.new(generate_column_names(data.shape[1], columns)) 21 | self.index = index || RangeIndex.new(0 ... length) 22 | end 23 | 24 | attr_reader :data 25 | 26 | def length 27 | data.shape[0] 28 | end 29 | 30 | def column_length 31 | data.shape[1] 32 | end 33 | 34 | def compare_data_equality(other) 35 | case other 36 | when NArrayAdapter 37 | data == other.data 38 | else 39 | super 40 | end 41 | end 42 | 43 | def [](row, column) 44 | if row 45 | @data[row, resolve_column_index(column)] 46 | else 47 | column_data = @data[true, resolve_column_index(column)] 48 | Charty::Vector.new(column_data, index: index, name: column) 49 | end 50 | end 51 | 52 | private def resolve_column_index(column) 53 | case column 54 | when String, Symbol 55 | index = column_names.index(column.to_sym) || column_names.index(column.to_s) 56 | return index if index 57 | raise IndexError, "invalid column name: #{column}" 58 | when Integer 59 | column 60 | else 61 | message = "column must be String or Integer: #{column.inspect}" 62 | raise ArgumentError, message 63 | end 64 | end 65 | 66 | private def generate_column_names(n_columns, columns) 67 | columns ||= [] 68 | if columns.length >= n_columns 69 | columns[0, n_columns] 70 | else 71 | columns + columns.length.upto(n_columns - 1).map {|i| "X#{i}" } 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/charty/util.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module Util 3 | if [].respond_to?(:filter_map) 4 | module_function def filter_map(enum, &block) 5 | enum.filter_map(&block) 6 | end 7 | else 8 | module_function def filter_map(enum, &block) 9 | enum.inject([]) do |acc, x| 10 | y = block.call(x) 11 | if y 12 | acc.push(y) 13 | else 14 | acc 15 | end 16 | end 17 | end 18 | end 19 | 20 | module_function def missing?(val) 21 | val.nil? || nan?(val) 22 | end 23 | 24 | module_function def nan?(val) 25 | val.respond_to?(:nan?) && val.nan? 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/charty/vector.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | 3 | module Charty 4 | class Vector 5 | extend Forwardable 6 | include Enumerable 7 | 8 | def self.try_convert(obj) 9 | case obj 10 | when self 11 | obj 12 | else 13 | if VectorAdapters.find_adapter_class(obj, exception: false) 14 | new(obj) 15 | end 16 | end 17 | end 18 | 19 | def initialize(data, index: nil, name: nil) 20 | adapter_class = VectorAdapters.find_adapter_class(data) 21 | @adapter = adapter_class.new(data) 22 | self.index = index unless index.nil? 23 | self.name = name unless name.nil? 24 | end 25 | 26 | attr_reader :adapter 27 | 28 | def_delegators :adapter, :data 29 | def_delegators :adapter, :index, :index= 30 | def_delegators :adapter, :==, :[], :[]= 31 | 32 | def_delegators :adapter, :length 33 | def_delegators :adapter, :name, :name= 34 | 35 | alias size length 36 | 37 | def_delegators :adapter, :iloc 38 | def_delegators :adapter, :to_a 39 | def_delegators :adapter, :each 40 | def_delegators :adapter, :empty? 41 | 42 | def_delegators :adapter, :boolean?, :numeric?, :categorical? 43 | def_delegators :adapter, :categories 44 | def_delegators :adapter, :unique_values 45 | def_delegators :adapter, :group_by 46 | def_delegators :adapter, :drop_na 47 | def_delegators :adapter, :values_at 48 | 49 | def_delegators :adapter, :eq, :notnull 50 | 51 | alias completecases notnull 52 | 53 | def_delegators :adapter, :mean, :stdev, :percentile 54 | 55 | def_delegators :adapter, :scale, :scale_inverse 56 | 57 | def scale(method) 58 | case method 59 | when :linear 60 | self 61 | when :log 62 | adapter.log_scale(method) 63 | else 64 | raise ArgumentError, 65 | "Invalid scaling method: %p" % method 66 | end 67 | end 68 | 69 | def scale_inverse(method) 70 | case method 71 | when :linear 72 | self 73 | when :log 74 | adapter.inverse_log_scale(method) 75 | else 76 | raise ArgumentError, 77 | "Invalid scaling method: %p" % method 78 | end 79 | end 80 | 81 | # TODO: write test 82 | def categorical_order(order=nil) 83 | if order.nil? 84 | case 85 | when categorical? 86 | order = categories 87 | else 88 | order = unique_values.compact 89 | if numeric? 90 | order.sort_by! {|x| Util.missing?(x) ? Float::INFINITY : x } 91 | end 92 | end 93 | order.compact! 94 | end 95 | order 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/charty/vector_adapters.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | 3 | module Charty 4 | module VectorAdapters 5 | class UnsupportedVectorData < StandardError; end 6 | 7 | @adapters = {} 8 | 9 | def self.register(name, adapter_class) 10 | @adapters[name] = adapter_class 11 | end 12 | 13 | def self.find_adapter_class(data, exception: true) 14 | @adapters.each_value do |adapter_class| 15 | return adapter_class if adapter_class.supported?(data) 16 | end 17 | if exception 18 | raise UnsupportedVectorData, "Unsupported vector data (#{data.class})" 19 | end 20 | end 21 | 22 | class BaseAdapter 23 | extend Forwardable 24 | include Enumerable 25 | 26 | def self.adapter_name 27 | name[/:?(\w+)Adapter\z/, 1] 28 | end 29 | 30 | private def check_data(data) 31 | return data if self.class.supported?(data) 32 | raise UnsupportedVectorData, "Unsupported vector data (#{data.class})" 33 | end 34 | 35 | attr_reader :data 36 | 37 | def_delegators :data, :length, :size 38 | 39 | def ==(other) 40 | case other.adapter 41 | when BaseAdapter 42 | return false if other.index != index 43 | if respond_to?(:compare_data_equality) 44 | compare_data_equality(other.adapter) 45 | elsif other.adapter.respond_to?(:compare_data_equality) 46 | other.adapter.compare_data_equality(self) 47 | else 48 | case other.adapter 49 | when self.class 50 | data == other.data 51 | else 52 | false 53 | end 54 | end 55 | else 56 | super 57 | end 58 | end 59 | 60 | def_delegator :data, :[], :iloc 61 | 62 | def_delegators :data, :[], :[]= 63 | def_delegators :data, :each, :to_a, :empty? 64 | 65 | # Take values at the given positional indices (without indexing) 66 | def values_at(*indices) 67 | indices.map {|i| data[i] } 68 | end 69 | 70 | def where_in_array(mask) 71 | mask = check_mask_vector(mask) 72 | masked_data = [] 73 | masked_index = [] 74 | mask.each_with_index do |f, i| 75 | case f 76 | when true, 1 77 | masked_data << data[i] 78 | masked_index << index[i] 79 | end 80 | end 81 | return masked_data, masked_index 82 | end 83 | 84 | private def check_mask_vector(mask) 85 | # ensure mask is boolean vector 86 | case mask 87 | when Charty::Vector 88 | unless mask.boolean? 89 | raise ArgumentError, "Unable to lookup items by a nonboolean vector" 90 | end 91 | mask 92 | else 93 | Charty::Vector.new(mask) 94 | end 95 | end 96 | 97 | def mean 98 | Statistics.mean(data) 99 | end 100 | 101 | def stdev(population: false) 102 | Statistics.stdev(data, population: population) 103 | end 104 | 105 | def percentile(q) 106 | Statistics.percentile(data, q) 107 | end 108 | 109 | def log_scale(method) 110 | Charty::Vector.new( 111 | self.map {|x| Math.log10(x) }, 112 | index: index, 113 | name: name 114 | ) 115 | end 116 | 117 | def inverse_log_scale(method) 118 | Charty::Vector.new( 119 | self.map {|x| 10.0 ** x }, 120 | index: index, 121 | name: name 122 | ) 123 | end 124 | end 125 | 126 | module NameSupport 127 | attr_reader :name 128 | 129 | def name=(value) 130 | @name = check_name(value) 131 | end 132 | 133 | private def check_name(value) 134 | value = String.try_convert(value) || value 135 | case value 136 | when String, Symbol 137 | value 138 | else 139 | raise ArgumentError, 140 | "name must be a String or a Symbol (#{value.class} is given)" 141 | end 142 | end 143 | end 144 | 145 | module IndexSupport 146 | attr_reader :index 147 | 148 | def [](key) 149 | case key 150 | when Charty::Vector 151 | where(key) 152 | else 153 | iloc(key_to_loc(key)) 154 | end 155 | end 156 | 157 | def []=(key, val) 158 | super(key_to_loc(key), val) 159 | end 160 | 161 | private def key_to_loc(key) 162 | loc = self.index.loc(key) 163 | if loc.nil? 164 | if key.respond_to?(:to_int) 165 | loc = key.to_int 166 | else 167 | raise KeyError.new("key not found: %p" % key, 168 | receiver: __method__, key: key) 169 | end 170 | end 171 | loc 172 | end 173 | 174 | def index=(values) 175 | @index = check_and_convert_index(values, :index, length) 176 | end 177 | 178 | private def check_and_convert_index(values, name, expected_length) 179 | case values 180 | when Index, Range 181 | else 182 | unless (ary = Array.try_convert(values)) 183 | raise ArgumentError, "invalid object for %s: %p" % [name, values] 184 | end 185 | values = ary 186 | end 187 | if expected_length != values.size 188 | raise ArgumentError, 189 | "invalid length for %s (%d for %d)" % [name, values.size, expected_length] 190 | end 191 | case values 192 | when Index 193 | values 194 | when Range 195 | RangeIndex.new(values) 196 | when Array 197 | Index.new(values) 198 | end 199 | end 200 | end 201 | end 202 | end 203 | 204 | require_relative "vector_adapters/array_adapter" 205 | require_relative "vector_adapters/arrow_adapter" 206 | require_relative "vector_adapters/daru_adapter" 207 | require_relative "vector_adapters/narray_adapter" 208 | require_relative "vector_adapters/numpy_adapter" 209 | require_relative "vector_adapters/pandas_adapter" 210 | require_relative "vector_adapters/vector_adapter" 211 | -------------------------------------------------------------------------------- /lib/charty/vector_adapters/array_adapter.rb: -------------------------------------------------------------------------------- 1 | require "date" 2 | 3 | module Charty 4 | module VectorAdapters 5 | class ArrayAdapter < BaseAdapter 6 | VectorAdapters.register(:array, self) 7 | 8 | extend Forwardable 9 | include Enumerable 10 | 11 | def self.supported?(data) 12 | case data 13 | when Array 14 | case data[0] 15 | when Numeric, String, Time, Date, DateTime, true, false, nil 16 | true 17 | else 18 | false 19 | end 20 | else 21 | false 22 | end 23 | end 24 | 25 | def initialize(data, index: nil) 26 | @data = check_data(data) 27 | self.index = index || RangeIndex.new(0 ... length) 28 | end 29 | 30 | include NameSupport 31 | include IndexSupport 32 | 33 | def_delegators :data, :values_at, :to_a 34 | 35 | def where(mask) 36 | masked_data, masked_index = where_in_array(mask) 37 | Charty::Vector.new(masked_data, index: masked_index, name: name) 38 | end 39 | 40 | def first_nonnil 41 | data.drop_while(&:nil?).first 42 | end 43 | 44 | def boolean? 45 | case first_nonnil 46 | when true, false 47 | true 48 | else 49 | false 50 | end 51 | end 52 | 53 | def numeric? 54 | case first_nonnil 55 | when Numeric 56 | true 57 | else 58 | false 59 | end 60 | end 61 | 62 | def categorical? 63 | false 64 | end 65 | 66 | def categories 67 | nil 68 | end 69 | 70 | def_delegator :data, :uniq, :unique_values 71 | 72 | def group_by(grouper) 73 | groups = data.each_index.group_by {|i| grouper[i] } 74 | groups.map { |g, vals| 75 | vals.collect! {|i| self[i] } 76 | [g, Charty::Vector.new(vals)] 77 | }.to_h 78 | end 79 | 80 | def drop_na 81 | if numeric? 82 | Charty::Vector.new(data.reject { |x| Util.missing?(x) }) 83 | else 84 | Charty::Vector.new(data.compact) 85 | end 86 | end 87 | 88 | def eq(val) 89 | Charty::Vector.new(data.map {|x| x == val }, 90 | index: index, 91 | name: name) 92 | end 93 | 94 | def notnull 95 | Charty::Vector.new(data.map {|x| ! Util.missing?(x) }, 96 | index: index, 97 | name: name) 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/charty/vector_adapters/arrow_adapter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module VectorAdapters 3 | class ArrowAdapter < BaseAdapter 4 | VectorAdapters.register(:arrow, self) 5 | 6 | include Enumerable 7 | include NameSupport 8 | include IndexSupport 9 | 10 | def self.supported?(data) 11 | (defined?(Arrow::Array) && data.is_a?(Arrow::Array)) || 12 | (defined?(Arrow::ChunkedArray) && data.is_a?(Arrow::ChunkedArray)) 13 | end 14 | 15 | def initialize(data) 16 | @data = check_data(data) 17 | self.index = index || RangeIndex.new(0 ... length) 18 | end 19 | 20 | def size 21 | @data.length 22 | end 23 | 24 | def empty? 25 | @data.length.zero? 26 | end 27 | 28 | def where(mask) 29 | mask = check_mask_vector(mask) 30 | mask_data = mask.data 31 | unless mask_data.is_a?(Arrow::BooleanArray) 32 | mask_data = mask.to_a 33 | mask_data = mask_data.map(&:nonzero?) if mask_data[0].is_a?(Integer) 34 | mask_data = Arrow::BooleanArray.new(mask_data) 35 | end 36 | masked_data = @data.filter(mask_data) 37 | masked_index = [] 38 | mask_data.to_a.each_with_index do |boolean, i| 39 | masked_index << index[i] if boolean 40 | end 41 | Vector.new(masked_data, index: masked_index, name: name) 42 | end 43 | 44 | def boolean? 45 | case @data 46 | when Arrow::BooleanArray 47 | true 48 | when Arrow::ChunkedArray 49 | @data.value_data_type.is_a?(Arrow::BooleanDataType) 50 | else 51 | false 52 | end 53 | end 54 | 55 | def numeric? 56 | case @data 57 | when Arrow::NumericArray 58 | true 59 | when Arrow::ChunkedArray 60 | @data.value_data_type.is_a?(Arrow::NumericDataType) 61 | else 62 | false 63 | end 64 | end 65 | 66 | def categorical? 67 | case @data 68 | when Arrow::StringArray, Arrow::DictionaryArray 69 | true 70 | when Arrow::ChunkedArray 71 | case @data.value_data_type 72 | when Arrow::StringArray, Arrow::DictionaryDataType 73 | true 74 | else 75 | false 76 | end 77 | else 78 | false 79 | end 80 | end 81 | 82 | def categories 83 | if @data.respond_to?(:dictionary) 84 | dictionary = @data.dictionary 85 | else 86 | dictionary = @data.dictionary_encode.dictionary 87 | end 88 | dictionary.to_a 89 | end 90 | 91 | def unique_values 92 | @data.unique.to_a 93 | end 94 | 95 | def group_by(grouper) 96 | grouper = Vector.new(grouper) unless grouper.is_a?(Vector) 97 | group_keys = grouper.unique_values 98 | grouper_data = grouper.data 99 | unless grouper_data.is_a?(Arrow::Array) 100 | grouper_data = Arrow::Array.new(grouper.to_a) 101 | end 102 | equal = Arrow::Function.find("equal") 103 | group_keys.map { |key| 104 | if key.nil? 105 | target_vector = Vector.new([nil] * @data.n_nulls) 106 | else 107 | mask = equal.execute([grouper_data, key]).value 108 | target_vector = Vector.new(@data.filter(mask)) 109 | end 110 | [key, target_vector] 111 | }.to_h 112 | end 113 | 114 | def drop_na 115 | if @data.n_nulls.zero? 116 | Vector.new(@data, index: index, name: name) 117 | else 118 | data_without_null = 119 | Arrow::Function.find("drop_null").execute([@data]).value 120 | Vector.new(data_without_null) 121 | end 122 | end 123 | 124 | def eq(val) 125 | mask = Arrow::Function.find("equal").execute([@data, val]).value 126 | Vector.new(mask, index: index, name: name) 127 | end 128 | 129 | def notnull 130 | if @data.n_nulls.zero? 131 | mask = Arrow::BooleanArray.new([true] * @data.length) 132 | else 133 | mask = Arrow::BooleanArray.new(@data.length, 134 | @data.null_bitmap, 135 | nil, 136 | 0) 137 | end 138 | Vector.new(mask, index: index, name: name) 139 | end 140 | 141 | def mean 142 | @data.mean 143 | end 144 | 145 | def stdev(population: false) 146 | options = Arrow::VarianceOptions.new 147 | if population 148 | options.ddof = 0 149 | else 150 | options.ddof = 1 151 | end 152 | Arrow::Function.find("stddev").execute([@data], options).value.value 153 | end 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /lib/charty/vector_adapters/daru_adapter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module VectorAdapters 3 | class DaruVectorAdapter < BaseAdapter 4 | VectorAdapters.register(:daru_vector, self) 5 | 6 | def self.supported?(data) 7 | defined?(Daru::Vector) && data.is_a?(Daru::Vector) 8 | end 9 | 10 | def initialize(data) 11 | @data = check_data(data) 12 | end 13 | 14 | def_delegator :data, :size, :length 15 | 16 | def index 17 | DaruIndex.new(data.index) 18 | end 19 | 20 | def index=(new_index) 21 | case new_index 22 | when DaruIndex 23 | data.index = new_index.values 24 | when Index 25 | data.index = new_index.to_a 26 | else 27 | data.index = new_index 28 | end 29 | end 30 | 31 | def_delegators :data, :name, :name= 32 | 33 | def compare_data_equality(other) 34 | case other 35 | when DaruVectorAdapter 36 | data == other.data 37 | else 38 | to_a == other.to_a 39 | end 40 | end 41 | 42 | def [](key) 43 | case key 44 | when Charty::Vector 45 | where(key) 46 | else 47 | data[key] 48 | end 49 | end 50 | 51 | def_delegators :data, :[]=, :to_a 52 | 53 | def values_at(*indices) 54 | indices.map {|i| data.at(i) } 55 | end 56 | 57 | def where(mask) 58 | masked_data, masked_index = where_in_array(mask) 59 | Charty::Vector.new(Daru::Vector.new(masked_data, index: masked_index), name: name) 60 | end 61 | 62 | def where_in_array(mask) 63 | mask = check_mask_vector(mask) 64 | masked_data = [] 65 | masked_index = [] 66 | mask.each_with_index do |f, i| 67 | case f 68 | when true, 1 69 | masked_data << data[i] 70 | masked_index << data.index.key(i) 71 | end 72 | end 73 | return masked_data, masked_index 74 | end 75 | 76 | def first_nonnil 77 | data.drop_while(&:nil?).first 78 | end 79 | 80 | def boolean? 81 | case 82 | when numeric?, categorical? 83 | false 84 | else 85 | case first_nonnil 86 | when true, false 87 | true 88 | else 89 | false 90 | end 91 | end 92 | end 93 | 94 | def_delegators :data, :numeric? 95 | def_delegator :data, :category?, :categorical? 96 | 97 | def categories 98 | data.categories.compact if categorical? 99 | end 100 | 101 | def unique_values 102 | data.uniq.to_a 103 | end 104 | 105 | def group_by(grouper) 106 | case grouper 107 | when Daru::Vector 108 | if grouper.category? 109 | # TODO: A categorical Daru::Vector cannot perform group_by well 110 | grouper = Daru::Vector.new(grouper.to_a) 111 | end 112 | groups = grouper.group_by.groups 113 | groups.map { |g, indices| 114 | [g.first, Charty::Vector.new(data[*indices])] 115 | }.to_h 116 | when Charty::Vector 117 | case grouper.data 118 | when Daru::Vector 119 | return group_by(grouper.data) 120 | else 121 | return group_by(Daru::Vector.new(grouper.to_a)) 122 | end 123 | else 124 | return group_by(Charty::Vector.new(grouper)) 125 | end 126 | end 127 | 128 | def drop_na 129 | values = data.reject {|x| Util.missing?(x) } 130 | Charty::Vector.new(Daru::Vector.new(values)) 131 | end 132 | 133 | def eq(val) 134 | Charty::Vector.new(data.eq(val).to_a, 135 | index: data.index.to_a, 136 | name: name) 137 | end 138 | 139 | def notnull 140 | notnull_data = data.map {|x| ! Util.missing?(x) } 141 | Charty::Vector.new(notnull_data, index: data.index.to_a, name: name) 142 | end 143 | 144 | def_delegator :data, :mean 145 | 146 | def stdev(population: false) 147 | if population 148 | data.standard_deviation_sample 149 | else 150 | data.standard_deviation_population 151 | end 152 | end 153 | 154 | def percentile(q) 155 | a = data.reject_values(*Daru::MISSING_VALUES).to_a 156 | Statistics.percentile(a, q) 157 | end 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /lib/charty/vector_adapters/narray_adapter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module VectorAdapters 3 | class NArrayAdapter < BaseAdapter 4 | VectorAdapters.register(:narray, self) 5 | 6 | extend Forwardable 7 | include Enumerable 8 | 9 | def self.supported?(data) 10 | defined?(Numo::NArray) && data.is_a?(Numo::NArray) 11 | end 12 | 13 | def initialize(data) 14 | @data = check_data(data) 15 | self.index = index || RangeIndex.new(0 ... length) 16 | end 17 | 18 | def compare_data_equality(other) 19 | case other 20 | when ArrayAdapter, NArrayAdapter 21 | data == other.data 22 | when NumpyAdapter, PandasSeriesAdapter 23 | other.compare_data_equality(self) 24 | else 25 | data == other.to_a 26 | end 27 | end 28 | 29 | include NameSupport 30 | include IndexSupport 31 | 32 | def to_a 33 | case data 34 | when Numo::Bit 35 | map {|bit| bit == 1 } 36 | else 37 | super 38 | end 39 | end 40 | 41 | def where(mask) 42 | mask = check_mask_vector(mask) 43 | case mask.data 44 | when Numo::Bit 45 | bits = mask.data 46 | masked_data = data[bits] 47 | masked_index = bits.where.map {|i| index[i] }.to_a 48 | else 49 | masked_data, masked_index = where_in_array(mask) 50 | masked_data = data.class[*masked_data] 51 | end 52 | Charty::Vector.new(masked_data, index: masked_index, name: name) 53 | end 54 | 55 | def boolean? 56 | case data 57 | when Numo::Bit 58 | true 59 | when Numo::RObject 60 | i, n = 0, data.size 61 | while i < n 62 | case data[i] 63 | when nil, true, false 64 | # do nothing 65 | else 66 | return false 67 | end 68 | i += 1 69 | end 70 | true 71 | else 72 | false 73 | end 74 | end 75 | 76 | def numeric? 77 | case data 78 | when Numo::Bit, 79 | Numo::RObject 80 | false 81 | else 82 | true 83 | end 84 | end 85 | 86 | def categorical? 87 | false 88 | end 89 | 90 | def categories 91 | nil 92 | end 93 | 94 | def unique_values 95 | existence = {} 96 | i, n = 0, data.size 97 | unique = [] 98 | while i < n 99 | x = data[i] 100 | unless existence[x] 101 | unique << x 102 | existence[x] = true 103 | end 104 | i += 1 105 | end 106 | unique 107 | end 108 | 109 | def group_by(grouper) 110 | case grouper 111 | when Charty::Vector 112 | # nothing to do 113 | else 114 | grouper = Charty::Vector.new(grouper) 115 | end 116 | 117 | group_keys = grouper.unique_values 118 | 119 | case grouper.data 120 | when Numo::NArray 121 | grouper = grouper.data 122 | else 123 | grouper = Numo::NArray[*grouper.to_a] 124 | end 125 | 126 | group_keys.map { |g| 127 | [g, Charty::Vector.new(data[grouper.eq(g)])] 128 | }.to_h 129 | end 130 | 131 | def drop_na 132 | case data 133 | when Numo::DFloat, Numo::SFloat, Numo::DComplex, Numo::SComplex 134 | Charty::Vector.new(data[~data.isnan]) 135 | when Numo::RObject 136 | where_is_nan = data.isnan 137 | values = [] 138 | i, n = 0, data.size 139 | while i < n 140 | x = data[i] 141 | unless x.nil? || where_is_nan[i] == 1 142 | values << x 143 | end 144 | i += 1 145 | end 146 | Charty::Vector.new(Numo::RObject[*values]) 147 | else 148 | self 149 | end 150 | end 151 | 152 | def eq(val) 153 | Charty::Vector.new(data.eq(val), 154 | index: index, 155 | name: name) 156 | end 157 | 158 | def notnull 159 | case data 160 | when Numo::RObject 161 | i, n = 0, length 162 | notnull_data = Numo::Bit.zeros(n) 163 | while i < n 164 | notnull_data[i] = ! Util.missing?(data[i]) 165 | i += 1 166 | end 167 | when ->(x) { x.respond_to?(:isnan) } 168 | notnull_data = ~data.isnan 169 | else 170 | notnull_data = Numo::Bit.ones(length) 171 | end 172 | Charty::Vector.new(notnull_data, index: index, name: name) 173 | end 174 | 175 | def mean 176 | data.mean(nan: true) 177 | end 178 | 179 | def stdev(population: false) 180 | s = data.stddev(nan: true) 181 | if population 182 | # Numo::NArray does not support population standard deviation 183 | n = data.isnan.sum 184 | s * (n - 1) / n 185 | else 186 | s 187 | end 188 | end 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /lib/charty/vector_adapters/numpy_adapter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module VectorAdapters 3 | class NumpyAdapter < BaseAdapter 4 | VectorAdapters.register(:numpy, self) 5 | 6 | def self.supported?(data) 7 | return false unless defined?(Numpy::NDArray) 8 | case data 9 | when Numpy::NDArray 10 | true 11 | else 12 | false 13 | end 14 | end 15 | 16 | def initialize(data) 17 | @data = check_data(data) 18 | self.index = index || RangeIndex.new(0 ... length) 19 | end 20 | 21 | attr_reader :data 22 | 23 | def_delegator :data, :size, :length 24 | 25 | def compare_data_equality(other) 26 | case other 27 | when NumpyAdapter, PandasSeriesAdapter 28 | Numpy.all(data == other.data) 29 | when BaseAdapter 30 | Numpy.all(data == other.data.to_a) 31 | else 32 | false 33 | end 34 | end 35 | 36 | include NameSupport 37 | include IndexSupport 38 | 39 | def where(mask) 40 | mask = check_mask_vector(mask) 41 | case mask.data 42 | when Numpy::NDArray, 43 | ->(x) { defined?(Pandas::Series) && x.is_a?(Pandas::Series) } 44 | mask_data = Numpy.asarray(mask.data, dtype: :bool) 45 | masked_data = data[mask_data] 46 | masked_index = mask_data.nonzero()[0].to_a.map {|i| index[i] } 47 | else 48 | masked_data, masked_index = where_in_array(mask) 49 | masked_data = Numpy.asarray(masked_data, dtype: data.dtype) 50 | end 51 | Charty::Vector.new(masked_data, index: masked_index, name: name) 52 | end 53 | 54 | def each 55 | return enum_for(__method__) unless block_given? 56 | 57 | i, n = 0, data.size 58 | while i < n 59 | yield data[i] 60 | i += 1 61 | end 62 | end 63 | 64 | def empty? 65 | data.size == 0 66 | end 67 | 68 | def boolean? 69 | builtins = PyCall.builtins 70 | case 71 | when builtins.issubclass(data.dtype.type, Numpy.bool_) 72 | true 73 | when builtins.issubclass(data.dtype.type, Numpy.object_) 74 | i, n = 0, data.size 75 | while i < n 76 | case data[i] 77 | when nil, true, false 78 | # do nothing 79 | else 80 | return false 81 | end 82 | i += 1 83 | end 84 | true 85 | else 86 | false 87 | end 88 | end 89 | 90 | def numeric? 91 | # TODO: Handle object array 92 | PyCall.builtins.issubclass(data.dtype.type, PyCall.tuple([Numpy.number, Numpy.bool_])) 93 | end 94 | 95 | def categorical? 96 | false 97 | end 98 | 99 | def categories 100 | nil 101 | end 102 | 103 | def unique_values 104 | Numpy.unique(data).to_a 105 | end 106 | 107 | def group_by(grouper) 108 | case grouper 109 | when Numpy::NDArray, 110 | ->(x) { defined?(Pandas::Series) && x.is_a?(Pandas::Series) } 111 | # Nothing todo 112 | when Charty::Vector 113 | case grouper.data 114 | when Numpy::NDArray 115 | grouper = grouper.data 116 | else 117 | grouper = Numpy.asarray(grouper.to_a) 118 | end 119 | else 120 | grouper = Numpy.asarray(Array.try_convert(grouper)) 121 | end 122 | 123 | group_keys = Numpy.unique(grouper).to_a 124 | group_keys.map { |g| 125 | [g, Charty::Vector.new(data[grouper == g])] 126 | }.to_h 127 | end 128 | 129 | def drop_na 130 | where_is_na = if numeric? 131 | Numpy.isnan(data) 132 | else 133 | (data == nil) 134 | end 135 | Charty::Vector.new(data[Numpy.logical_not(where_is_na)]) 136 | end 137 | 138 | def eq(val) 139 | Charty::Vector.new((data == val), 140 | index: index, 141 | name: name) 142 | end 143 | 144 | def notnull 145 | case 146 | when PyCall.builtins.issubclass(data.dtype.type, Numpy.object_) 147 | i, n = 0, length 148 | notnull_data = Numpy::NDArray.new(n, dtype: :bool) 149 | while i < n 150 | notnull_data[i] = ! Util.missing?(data[i]) 151 | i += 1 152 | end 153 | else 154 | notnull_data = Numpy.isnan(data) 155 | end 156 | Charty::Vector.new(notnull_data, index: index, name: name) 157 | end 158 | 159 | def mean 160 | Numpy.mean(data) 161 | end 162 | 163 | def stdev(population: false) 164 | Numpy.std(data, ddof: population ? 0 : 1) 165 | end 166 | 167 | def percentile(q) 168 | Numpy.nanpercentile(data, q) 169 | end 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/charty/vector_adapters/pandas_adapter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module VectorAdapters 3 | class PandasSeriesAdapter < BaseAdapter 4 | VectorAdapters.register(:pandas_series, self) 5 | 6 | def self.supported?(data) 7 | return false unless defined?(Pandas::Series) 8 | case data 9 | when Pandas::Series 10 | true 11 | else 12 | false 13 | end 14 | end 15 | 16 | def initialize(data) 17 | @data = check_data(data) 18 | end 19 | 20 | attr_reader :data 21 | 22 | def_delegator :data, :size, :length 23 | 24 | def index 25 | PandasIndex.new(data.index) 26 | end 27 | 28 | def index=(new_index) 29 | case new_index 30 | when PandasIndex 31 | data.index = new_index.values 32 | when Index 33 | data.index = new_index.to_a 34 | else 35 | data.index = new_index 36 | end 37 | end 38 | 39 | def_delegators :data, :name, :name= 40 | 41 | def compare_data_equality(other) 42 | case other 43 | when PandasSeriesAdapter 44 | return data.equals(other.data) 45 | when NumpyAdapter 46 | other = other.data 47 | when NArrayAdapter 48 | case other.data 49 | when Numo::Bit 50 | other = other.data.to_a 51 | other.map! {|x| [false, true][x] } 52 | else 53 | other = other.data.to_a 54 | end 55 | when BaseAdapter 56 | other = other.data.to_a 57 | else 58 | return false 59 | end 60 | 61 | data.equals(Pandas::Series.new(other, index: data.index)) 62 | end 63 | 64 | def iloc(i) 65 | data.iloc[i] 66 | end 67 | 68 | def [](key) 69 | case key 70 | when Charty::Vector 71 | where(key) 72 | else 73 | data[key] 74 | end 75 | end 76 | 77 | def_delegators :data, :[]=, :to_a 78 | 79 | def each 80 | return enum_for(__method__) unless block_given? 81 | 82 | i, n = 0, data.size 83 | while i < n 84 | yield data.iloc[i] 85 | i += 1 86 | end 87 | end 88 | 89 | def empty? 90 | data.size == 0 91 | end 92 | 93 | def values_at(*indices) 94 | data.take(indices).to_a 95 | end 96 | 97 | def where(mask) 98 | mask = check_mask_vector(mask) 99 | case mask.data 100 | when Numpy::NDArray, 101 | ->(x) { defined?(Pandas::Series) && x.is_a?(Pandas::Series) } 102 | mask_data = Numpy.asarray(mask.data, dtype: :bool) 103 | masked_data = data[mask_data] 104 | masked_index = mask_data.nonzero()[0].to_a.map {|i| index[i] } 105 | else 106 | masked_data, masked_index = where_in_array(mask) 107 | masked_data = Pandas::Series.new(masked_data, dtype: data.dtype) 108 | end 109 | Charty::Vector.new(masked_data, index: masked_index, name: name) 110 | end 111 | 112 | def where_in_array(mask) 113 | mask = check_mask_vector(mask) 114 | masked_data = [] 115 | masked_index = [] 116 | mask.each_with_index do |f, i| 117 | case f 118 | when true, 1 119 | masked_data << data.iloc[i] 120 | masked_index << index[i] 121 | end 122 | end 123 | return masked_data, masked_index 124 | end 125 | 126 | def boolean? 127 | case 128 | when Pandas.api.types.is_bool_dtype(data.dtype) 129 | true 130 | when Pandas.api.types.is_object_dtype(data.dtype) 131 | data.isin([nil, false, true]).all() 132 | else 133 | false 134 | end 135 | end 136 | 137 | def numeric? 138 | Pandas.api.types.is_numeric_dtype(data.dtype) 139 | end 140 | 141 | def categorical? 142 | Pandas.api.types.is_categorical_dtype(data.dtype) 143 | end 144 | 145 | def categories 146 | data.cat.categories.to_a if categorical? 147 | end 148 | 149 | def unique_values 150 | data.unique.to_a 151 | end 152 | 153 | def group_by(grouper) 154 | case grouper 155 | when Pandas::Series 156 | group_keys = grouper.unique.to_a 157 | groups = data.groupby(grouper) 158 | group_keys.map {|g| 159 | g_vals = groups.get_group(g) rescue [] 160 | [g, Charty::Vector.new(g_vals)] 161 | }.to_h 162 | when Charty::Vector 163 | case grouper.adapter 164 | when self.class 165 | group_by(grouper.data) 166 | else 167 | grouper = Pandas::Series.new(grouper.to_a) 168 | group_by(grouper) 169 | end 170 | else 171 | grouper = Pandas::Series.new(Array(grouper)) 172 | group_by(grouper) 173 | end 174 | end 175 | 176 | def drop_na 177 | Charty::Vector.new(data.dropna) 178 | end 179 | 180 | def eq(val) 181 | Charty::Vector.new((data == val), 182 | index: index, 183 | name: name) 184 | end 185 | 186 | def notnull 187 | Charty::Vector.new(data.notnull, index: index, name: name) 188 | end 189 | 190 | def mean 191 | data.mean() 192 | end 193 | 194 | def stdev(population: false) 195 | data.std(ddof: population ? 0 : 1) 196 | end 197 | 198 | def percentile(q) 199 | q = q.map {|x| x / 100.0 } 200 | data.quantile(q) 201 | end 202 | 203 | def log_scale(method) 204 | Charty::Vector.new(Numpy.log10(data)) 205 | end 206 | 207 | def inverse_log_scale(method) 208 | Charty::Vector.new(Numpy.power(10, data)) 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /lib/charty/vector_adapters/vector_adapter.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module VectorAdapters 3 | class VectorAdapter < BaseAdapter 4 | VectorAdapters.register(:vector, self) 5 | 6 | extend Forwardable 7 | include Enumerable 8 | 9 | def self.supported?(data) 10 | data.is_a?(Vector) 11 | end 12 | 13 | def initialize(data, index: nil) 14 | data = check_data(data) 15 | @data = reduce_nested_vector(data) 16 | self.index = index || RangeIndex.new(0 ... length) 17 | end 18 | 19 | include NameSupport 20 | include IndexSupport 21 | 22 | def_delegators :data, 23 | :boolean?, 24 | :categorical?, 25 | :categories, 26 | :drop_na, 27 | :each, 28 | :eq, 29 | :first_nonnil, 30 | :group_by, 31 | :notnull, 32 | :numeric?, 33 | :to_a, 34 | :uniq, 35 | :unique_values, 36 | :values_at, 37 | :where, 38 | :iloc 39 | 40 | def compare_data_equality(other) 41 | if other.is_a?(self.class) 42 | other = reduce_nested_vector(other.data).adapter 43 | end 44 | if other.is_a?(self.class) 45 | @data.adapter.data == other.data 46 | elsif @data.adapter.respond_to?(:compare_data_equality) 47 | @data.adapter.compare_data_equality(other) 48 | elsif other.respond_to?(:compare_data_equality) 49 | other.compare_data_equality(@data.adapter) 50 | else 51 | @data.adapter.to_a == other.to_a 52 | end 53 | end 54 | 55 | private def reduce_nested_vector(vector) 56 | while vector.adapter.is_a?(self.class) 57 | vector = vector.adapter.data 58 | end 59 | vector 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/charty/version.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | VERSION = "0.2.13" 3 | 4 | module Version 5 | numbers, TAG = VERSION.split("-") 6 | MAJOR, MINOR, MICRO = numbers.split(".").collect(&:to_i) 7 | STRING = VERSION 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib 2 | pandas 3 | -------------------------------------------------------------------------------- /test/backends_test.rb: -------------------------------------------------------------------------------- 1 | require "load_error_backend" 2 | 3 | class BackendsTest < Test::Unit::TestCase 4 | sub_test_case(".find_backend_class") do 5 | test("find by symbol") do 6 | backend_class = Charty::Backends.find_backend_class(:plotly) 7 | assert_equal(Charty::Backends::Plotly, backend_class) 8 | end 9 | 10 | test("find by string") do 11 | backend_class = Charty::Backends.find_backend_class("plotly") 12 | assert_equal(Charty::Backends::Plotly, backend_class) 13 | end 14 | 15 | test("unregistered backend") do 16 | assert_raise(Charty::BackendNotFoundError) do 17 | Charty::Backends.find_backend_class("unregistered_backend") 18 | end 19 | end 20 | 21 | test("unable to prepare backend") do 22 | assert_raise(Charty::BackendLoadError) do 23 | Charty::Backends.find_backend_class("load_error_backend") 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/dash_pattern_generator_test.rb: -------------------------------------------------------------------------------- 1 | class DashPatternGeneratorTest < Test::Unit::TestCase 2 | def setup 3 | # Generated by the following python code: 4 | # [list(x) if x else x for x in seaborn._core.unique_dashes(30)] 5 | @expected_dashes = [ 6 | "", 7 | [4, 1.5], 8 | [1, 1], 9 | [3, 1.25, 1.5, 1.25], 10 | [5, 1, 1, 1], 11 | [3, 1.25, 1.25, 1.25, 1.25, 1.25], 12 | [4, 1, 4, 1, 1, 1], 13 | [3, 1.25, 3, 1.25, 1.25, 1.25], 14 | [4, 1, 1, 1, 1, 1], 15 | [3, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25], 16 | [4, 1, 4, 1, 4, 1, 1, 1], 17 | [3, 1.25, 3, 1.25, 1.25, 1.25, 1.25, 1.25], 18 | [4, 1, 4, 1, 1, 1, 1, 1], 19 | [3, 1.25, 3, 1.25, 3, 1.25, 1.25, 1.25], 20 | [4, 1, 1, 1, 1, 1, 1, 1], 21 | [3, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25], 22 | [4, 1, 4, 1, 4, 1, 4, 1, 1, 1], 23 | [3, 1.25, 3, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25], 24 | [4, 1, 4, 1, 4, 1, 1, 1, 1, 1], 25 | [3, 1.25, 3, 1.25, 3, 1.25, 1.25, 1.25, 1.25, 1.25], 26 | [4, 1, 4, 1, 1, 1, 1, 1, 1, 1], 27 | [3, 1.25, 3, 1.25, 3, 1.25, 3, 1.25, 1.25, 1.25], 28 | [4, 1, 1, 1, 1, 1, 1, 1, 1, 1], 29 | [3, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25], 30 | [4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 1, 1], 31 | [3, 1.25, 3, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25], 32 | [4, 1, 4, 1, 4, 1, 4, 1, 1, 1, 1, 1], 33 | [3, 1.25, 3, 1.25, 3, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25], 34 | [4, 1, 4, 1, 4, 1, 1, 1, 1, 1, 1, 1], 35 | [3, 1.25, 3, 1.25, 3, 1.25, 3, 1.25, 1.25, 1.25, 1.25, 1.25] 36 | ] 37 | end 38 | 39 | test("the first 20 patterns") do 40 | generator = Charty::DashPatternGenerator 41 | assert_equal(@expected_dashes[0, 20], 42 | generator.take(20)) 43 | end 44 | 45 | test("#valid_name?") do 46 | assert_equal({ 47 | solid: true, 48 | longdash: false 49 | }, 50 | { 51 | solid: Charty::DashPatternGenerator.valid_name?(:solid), 52 | longdash: Charty::DashPatternGenerator.valid_name?(:longdash) 53 | }) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require "charty" 3 | require "test/unit" 4 | require "tmpdir" 5 | 6 | require "active_record" 7 | require "bigdecimal" 8 | require "csv" 9 | require "daru" 10 | require "datasets" 11 | require "set" # NOTE: daru needs set 12 | 13 | require "iruby" 14 | require "iruby/logger" 15 | IRuby.logger = Logger.new(STDERR, level: Logger::Severity::INFO) 16 | 17 | begin 18 | require "numo/narray" 19 | rescue LoadError 20 | end 21 | 22 | begin 23 | require "nmatrix" 24 | rescue LoadError 25 | end 26 | 27 | begin 28 | require "matplotlib" 29 | rescue LoadError, StandardError 30 | end 31 | 32 | begin 33 | require "pandas" 34 | rescue LoadError, StandardError 35 | end 36 | 37 | begin 38 | require "arrow" 39 | rescue LoadError 40 | end 41 | 42 | module Charty 43 | module PythonTestHelpers 44 | def define_python_warning_check 45 | PyCall.exec(< None: 56 | super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) 57 | 58 | def getvalue(self) -> str: 59 | assert isinstance(self.buffer, io.BytesIO) 60 | return self.buffer.getvalue().decode("UTF-8") 61 | 62 | class WarningCheckIO(CaptureIO): 63 | def __init__(self, pattern: str) -> None: 64 | super().__init__() 65 | self.pattern = pattern 66 | 67 | def write(self, s: str) -> int: 68 | if re.search(self.pattern, s) is not None: 69 | raise WarningCheckError(s) 70 | return super().write(s) 71 | 72 | @contextlib.contextmanager 73 | def warning_check(pattern: str): 74 | old = sys.stderr 75 | setattr(sys, "stderr", WarningCheckIO(pattern)) 76 | try: 77 | yield 78 | finally: 79 | setattr(sys, "stderr", old) 80 | PYTHON 81 | end 82 | 83 | def python_warning_check(pattern) 84 | f = PyCall.eval("warning_check") 85 | f.(pattern) 86 | rescue PyCall::PyError 87 | define_python_warning_check 88 | python_warning_check(pattern) 89 | end 90 | 91 | def check_python_warning(pattern, &block) 92 | PyCall.with(python_warning_check(pattern), &block) 93 | end 94 | end 95 | 96 | module TestHelpers 97 | include PythonTestHelpers 98 | 99 | def setup 100 | super 101 | if pandas_available? 102 | check_python_warning("FutureWarning") do 103 | yield 104 | end 105 | else 106 | yield 107 | end 108 | end 109 | 110 | module_function def arrow_available? 111 | defined?(::Arrow::Table) and Arrow::Version::MAJOR >= 6 112 | end 113 | 114 | module_function def arrow_required 115 | omit("red-arrow 6.0.0 or later is requried") unless arrow_available? 116 | end 117 | 118 | module_function def numo_available? 119 | defined?(::Numo::NArray) 120 | end 121 | 122 | module_function def numo_required 123 | omit("Numo::NArray is requried") unless numo_available? 124 | end 125 | 126 | module_function def nmatrix_available? 127 | return false if RUBY_VERSION >= "3.0" # SEGV occurs in NMatrix on Ruby >= 3.0 128 | defined?(::NMatrix::VERSION::STRING) 129 | end 130 | 131 | module_function def nmatrix_required 132 | omit("NMatrix is requried") unless nmatrix_available? 133 | end 134 | 135 | module_function def matplotlib_available? 136 | defined?(::Matplotlib) 137 | end 138 | 139 | module_function def matplotlib_required 140 | omit("Matplotlib is required") unless matplotlib_available? 141 | end 142 | 143 | module_function def numpy_available? 144 | pandas_available? 145 | end 146 | 147 | module_function def numpy_required 148 | omit("Numpy is required") unless numpy_available? 149 | end 150 | 151 | module_function def pandas_available? 152 | defined?(::Pandas) 153 | end 154 | 155 | module_function def pandas_required 156 | omit("Pandas is required") unless pandas_available? 157 | end 158 | 159 | def assert_near(c1, c2, eps=1e-8) 160 | assert_equal(c1.class, c2.class) 161 | c1.components.zip(c2.components).each do |x1, x2| 162 | x1, x2 = [x1, x2].map(&:to_f) 163 | assert { (x1 - x2).abs < eps } 164 | end 165 | end 166 | end 167 | 168 | module RenderingTestHelpers 169 | include Charty::TestHelpers 170 | 171 | def setup_data(adapter_name) 172 | setup_array_data 173 | case adapter_name 174 | when :arrow 175 | arrow_required 176 | setup_arrow_data 177 | when :daru 178 | setup_daru_data 179 | when :nmatrix 180 | nmatrix_required 181 | setup_nmatrix_data 182 | when :numo 183 | numo_required 184 | setup_numo_data 185 | when :pandas 186 | pandas_required 187 | setup_pandas_data 188 | when :numpy 189 | pandas_required 190 | setup_numpy_data 191 | end 192 | end 193 | 194 | def setup_backend(backend_name) 195 | case backend_name 196 | when :pyplot 197 | if defined?(Matplotlib) 198 | setup_pyplot_backend 199 | else 200 | matplotlib_required 201 | end 202 | end 203 | Charty::Backends.use(backend_name) 204 | end 205 | 206 | def setup_pyplot_backend 207 | require "matplotlib" 208 | Matplotlib.use("agg") 209 | end 210 | 211 | def render_plot(backend_name, plot, **kwargs) 212 | plot.render(**kwargs) 213 | end 214 | end 215 | 216 | module IRubyTestHelper 217 | def setup_iruby 218 | @__iruby_config_dir = Dir.mktmpdir("iruby-test") 219 | @__iruby_config_path = Pathname.new(@__iruby_config_dir) + "config.json" 220 | File.write(@__iruby_config_path, { 221 | control_port: 50160, 222 | shell_port: 57503, 223 | transport: "tcp", 224 | signature_scheme: "hmac-sha256", 225 | stdin_port: 52597, 226 | hb_port: 42540, 227 | ip: "127.0.0.1", 228 | iopub_port: 40885, 229 | key: "a0436f6c-1916-498b-8eb9-e81ab9368e84" 230 | }.to_json) 231 | 232 | @__original_iruby_kernel_instance = IRuby::Kernel.instance 233 | 234 | IRuby::Kernel.new(@__iruby_config_path.to_s, "test") 235 | $stdout = STDOUT 236 | $stderr = STDERR 237 | end 238 | 239 | def teardown_iruby 240 | IRuby::Kernel.instance = @__original_iruby_kernel_instance 241 | end 242 | end 243 | end 244 | -------------------------------------------------------------------------------- /test/lib/load_error_backend.rb: -------------------------------------------------------------------------------- 1 | module Charty 2 | module TestHelpers 3 | class LoadErrorBackend 4 | Backends.register(:load_error_backend, self) 5 | 6 | def self.prepare 7 | raise LoadError, "LoadErrorBackend" 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/plot_methods/box_plot_test.rb: -------------------------------------------------------------------------------- 1 | class PlotMethodsBoxPlotTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | sub_test_case("rendering") do 5 | include Charty::RenderingTestHelpers 6 | 7 | def setup_array_data 8 | @data = { 9 | y: Array.new(100) {|i| rand }, 10 | x: Array.new(100) {|i| ["foo", "bar"][rand(2)] }, 11 | c: Array.new(100) {|i| ["red", "green", "blue"][rand(3)] } 12 | } 13 | end 14 | 15 | def setup_arrow_data 16 | @data = Arrow::Table.new(@data) 17 | end 18 | 19 | def setup_daru_data 20 | @data = Daru::DataFrame.new(@data) 21 | @data[:x] = @data[:x].to_category 22 | @data[:c] = @data[:c].to_category 23 | end 24 | 25 | def setup_nmatrix_data 26 | omit("TODO: Support NMaatrix") 27 | @data[:x] = NMatrix.new([100], @data[:x], dtype: :object) 28 | @data[:c] = NMatrix.new([100], @data[:c], dtype: :object) 29 | @data[:y] = NMatrix.new([100], @data[:y], dtype: :float64) 30 | end 31 | 32 | def setup_numo_data 33 | @data[:y] = Numo::DFloat[*@data[:y]] 34 | @data[:x] = Numo::RObject[*@data[:x]] 35 | @data[:c] = Numo::RObject[*@data[:c]] 36 | end 37 | 38 | def setup_pandas_data 39 | @data = Pandas::DataFrame.new(data: @data) 40 | end 41 | 42 | def setup_numpy_data 43 | @data = { 44 | y: Numpy.asarray(@data[:y], dtype: Numpy.float64), 45 | x: Numpy.asarray(@data[:x], dtype: :str), 46 | c: Numpy.asarray(@data[:c], dtype: :str) 47 | } 48 | end 49 | 50 | data(:adapter, 51 | [:array, :arrow, :daru, :numo, :nmatrix, :numpy, :pandas], 52 | keep: true) 53 | data(:backend, [:plotly, :pyplot], keep: true) 54 | def test_box_plot(data) 55 | adapter_name, backend_name = data.values_at(:adapter, :backend) 56 | setup_data(adapter_name) 57 | setup_backend(backend_name) 58 | plot = Charty.box_plot(data: @data, x: :x, y: :y) 59 | assert_nothing_raised do 60 | render_plot(backend_name, plot) 61 | end 62 | end 63 | 64 | def test_box_plot_with_color(data) 65 | adapter_name, backend_name = data.values_at(:adapter, :backend) 66 | setup_data(adapter_name) 67 | setup_backend(backend_name) 68 | plot = Charty.box_plot(data: @data, x: :x, y: :y, color: :c) 69 | assert_nothing_raised do 70 | render_plot(backend_name, plot) 71 | end 72 | end 73 | 74 | def test_box_plot_infer_orient(data) 75 | adapter_name, backend_name = data.values_at(:adapter, :backend) 76 | setup_data(adapter_name) 77 | setup_backend(backend_name) 78 | plot = Charty.box_plot(data: @data, x: :y, y: :x) 79 | assert_nothing_raised do 80 | render_plot(backend_name, plot) 81 | end 82 | end 83 | 84 | def test_box_plot_h(data) 85 | adapter_name, backend_name = data.values_at(:adapter, :backend) 86 | setup_data(adapter_name) 87 | setup_backend(backend_name) 88 | assert_raise(ArgumentError.new("Horizontal orientation requires numeric `x` variable")) do 89 | Charty.box_plot(data: @data, x: :x, y: :y, orient: :h) 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/plot_methods/count_plot_test.rb: -------------------------------------------------------------------------------- 1 | class PlotMethodsCountPlotTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | sub_test_case("function-call style") do 5 | # TODO: write tests here 6 | end 7 | 8 | sub_test_case("DSL style") do 9 | # TODO: write tests here 10 | end 11 | 12 | sub_test_case("rendering") do 13 | include Charty::RenderingTestHelpers 14 | 15 | def setup_array_data 16 | @data = { 17 | y: Array.new(100) {|i| rand }, 18 | x: Array.new(100) {|i| ["foo", "bar"][rand(2)] }, 19 | c: Array.new(100) {|i| ["red", "blue", "green"][rand(3)] } 20 | } 21 | end 22 | 23 | def setup_arrow_data 24 | @data = Arrow::Table.new(@data) 25 | end 26 | 27 | def setup_daru_data 28 | @data = Daru::DataFrame.new(@data) 29 | @data[:x] = @data[:x].to_category 30 | @data[:c] = @data[:c].to_category 31 | end 32 | 33 | def setup_nmatrix_data 34 | omit("TODO: Support NMatrix") 35 | @data[:x] = NMatrix.new([100], @data[:x], dtype: :object) 36 | @data[:c] = NMatrix.new([100], @data[:c], dtype: :object) 37 | @data[:y] = NMatrix.new([100], @data[:y], dtype: :float64) 38 | end 39 | 40 | def setup_numo_data 41 | @data[:y] = Numo::DFloat[*@data[:y]] 42 | end 43 | 44 | def setup_pandas_data 45 | @data = Pandas::DataFrame.new(data: @data) 46 | end 47 | 48 | def setup_numpy_data 49 | @data[:x] = Numpy.asarray(@data[:x], dtype: :str) 50 | @data[:c] = Numpy.asarray(@data[:c], dtype: :str) 51 | @data[:y] = Numpy.asarray(@data[:y]) 52 | end 53 | 54 | data(:adapter, 55 | [:array, :arrow, :daru, :numo, :nmatrix, :numpy, :pandas], 56 | keep: true) 57 | data(:backend, [:plotly, :pyplot], keep: true) 58 | def test_count_plot(data) 59 | adapter_name, backend_name = data.values_at(:adapter, :backend) 60 | setup_data(adapter_name) 61 | setup_backend(backend_name) 62 | plot = Charty.count_plot(data: @data, x: :x) 63 | assert_nothing_raised do 64 | render_plot(backend_name, plot) 65 | end 66 | end 67 | 68 | def test_count_plot_with_color(data) 69 | adapter_name, backend_name = data.values_at(:adapter, :backend) 70 | setup_data(adapter_name) 71 | setup_backend(backend_name) 72 | plot = Charty.count_plot(data: @data, x: :x, color: :c) 73 | assert_nothing_raised do 74 | render_plot(backend_name, plot) 75 | end 76 | end 77 | 78 | def test_count_plot_infer_orient(data) 79 | adapter_name, backend_name = data.values_at(:adapter, :backend) 80 | setup_data(adapter_name) 81 | setup_backend(backend_name) 82 | plot = Charty.count_plot(data: @data, y: :x) 83 | assert_nothing_raised do 84 | render_plot(backend_name, plot) 85 | end 86 | end 87 | 88 | # TODO: Support numeric data 89 | def test_count_plot_numeric(data) 90 | omit("Unsupported yet") 91 | adapter_name, backend_name = data.values_at(:adapter, :backend) 92 | setup_data(adapter_name) 93 | setup_backend(backend_name) 94 | plot = Charty.count_plot(data: @data, x: :y) 95 | assert_nothing_raised do 96 | render_plot(backend_name, plot) 97 | end 98 | end 99 | 100 | def test_count_plot_both_x_y(data) 101 | adapter_name, backend_name = data.values_at(:adapter, :backend) 102 | setup_data(adapter_name) 103 | setup_backend(backend_name) 104 | assert_raise(ArgumentError.new("Unable to pass both x and y to count_plot")) do 105 | Charty.count_plot(data: @data, x: :x, y: :y) 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /test/plot_methods/hist_plot_test.rb: -------------------------------------------------------------------------------- 1 | class PlotMethodHistPlotTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | sub_test_case("rendering") do 5 | include Charty::RenderingTestHelpers 6 | 7 | data(:adapter, [:array, :arrow]) 8 | data(:backend, [:pyplot, :plotly]) 9 | def test_hist_plot_with_flat_vectors(data) 10 | backend_name = data[:backend] 11 | setup_backend(backend_name) 12 | data = Array.new(500) { rand } 13 | plot = Charty.hist_plot(data: data) 14 | assert_nothing_raised do 15 | render_plot(backend_name, plot) 16 | end 17 | end 18 | 19 | sub_test_case("wide form") do 20 | data(:adapter, [:array, :arrow, :pandas], keep: true) 21 | data(:backend, [:pyplot, :plotly], keep: true) 22 | def test_hist_plot_with_wide_form(data) 23 | adapter_name, backend_name = data.values_at(:adapter, :backend) 24 | setup_data(adapter_name) 25 | setup_backend(backend_name) 26 | plot = Charty.hist_plot(data: @data, x_label: "Foo Bar") 27 | assert_nothing_raised do 28 | assert_equal("Foo Bar", plot.x_label) 29 | render_plot(backend_name, plot) 30 | end 31 | end 32 | 33 | def setup_array_data 34 | @data = @array_data = { 35 | red: Array.new(100) {|i| rand }, 36 | blue: Array.new(100) {|i| rand + 1}, 37 | green: Array.new(100) {|i| rand + 2 }, 38 | } 39 | end 40 | 41 | def setup_arrow_data 42 | @data = Arrow::Table.new(@array_data) 43 | end 44 | 45 | def setup_pandas_data 46 | pandas_required 47 | data = Pandas::DataFrame.new(data: @array_data) 48 | @data = Pandas::DataFrame.new(data: { 49 | red: data[:red].astype("float64"), 50 | blue: data[:blue].astype("float64"), 51 | green: data[:green].astype("float64") 52 | }) 53 | end 54 | end 55 | 56 | data(:adapter, [:array, :arrow, :pandas], keep: true) 57 | data(:backend, [:pyplot, :plotly], keep: true) 58 | def test_hist_plot(data) 59 | adapter_name, backend_name = data.values_at(:adapter, :backend) 60 | setup_data(adapter_name) 61 | setup_backend(backend_name) 62 | plot = Charty.hist_plot(data: @data, x: :a) 63 | assert_nothing_raised do 64 | render_plot(backend_name, plot) 65 | end 66 | end 67 | 68 | def test_hist_plot_color(data) 69 | adapter_name, backend_name = data.values_at(:adapter, :backend) 70 | setup_data(adapter_name) 71 | setup_backend(backend_name) 72 | plot = Charty.hist_plot(data: @data, x: :a, color: :c) 73 | assert_nothing_raised do 74 | render_plot(backend_name, plot) 75 | end 76 | end 77 | 78 | def setup_array_data 79 | @data = @array_data = { 80 | a: Array.new(100) {|i| rand }, 81 | c: Array.new(100) {|i| ["red", "blue", "green"][rand(3)] }, 82 | } 83 | end 84 | 85 | def setup_arrow_data 86 | @data = Arrow::Table.new(@array_data) 87 | end 88 | 89 | def setup_pandas_data 90 | pandas_required 91 | data = Pandas::DataFrame.new(data: @array_data) 92 | @data = Pandas::DataFrame.new(data: { 93 | a: data[:a].astype("float64"), 94 | c: data[:c].astype("category") 95 | }) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/plot_methods/line_plot_test.rb: -------------------------------------------------------------------------------- 1 | class PlotMethodLinePlotTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | sub_test_case("rendering") do 5 | include Charty::RenderingTestHelpers 6 | 7 | def test_line_plot_with_flat_vector 8 | backend_name = :pyplot 9 | setup_backend(backend_name) 10 | plot = Charty.line_plot(data: [1, 2, 3, 4, 5]) 11 | assert_nothing_raised do 12 | render_plot(backend_name, plot) 13 | end 14 | end 15 | 16 | def test_line_plot_with_vectors 17 | backend_name = :pyplot 18 | setup_backend(backend_name) 19 | plot = Charty.line_plot(x: [1, 2, 3, 4, 5], y: [1, 4, 2, 3, 5]) 20 | assert_nothing_raised do 21 | render_plot(backend_name, plot) 22 | end 23 | end 24 | 25 | data(:adapter, [:array, :arrow], keep: true) 26 | data(:backend, [:pyplot, :plotly], keep: true) 27 | def test_line_plot(data) 28 | adapter_name, backend_name = data.values_at(:adapter, :backend) 29 | setup_data(adapter_name) 30 | setup_backend(backend_name) 31 | plot = Charty.line_plot(data: @data, x: :x, y: :y) 32 | assert_nothing_raised do 33 | render_plot(backend_name, plot) 34 | end 35 | end 36 | 37 | def test_line_plot_with_numeric_color(data) 38 | adapter_name, backend_name = data.values_at(:adapter, :backend) 39 | setup_data(adapter_name) 40 | setup_backend(backend_name) 41 | plot = Charty.line_plot(data: @data, x: :x, y: :y, color: :d) 42 | assert_nothing_raised do 43 | render_plot(backend_name, plot) 44 | end 45 | end 46 | 47 | def test_line_plot_with_categorical_color(data) 48 | adapter_name, backend_name = data.values_at(:adapter, :backend) 49 | setup_data(adapter_name) 50 | setup_backend(backend_name) 51 | plot = Charty.line_plot(data: @data, x: :x, y: :y, color: :c) 52 | assert_nothing_raised do 53 | render_plot(backend_name, plot) 54 | end 55 | end 56 | 57 | def test_line_plot_with_numeric_size(data) 58 | adapter_name, backend_name = data.values_at(:adapter, :backend) 59 | setup_data(adapter_name) 60 | setup_backend(backend_name) 61 | plot = Charty.line_plot(data: @data, x: :x, y: :y, size: :d) 62 | assert_nothing_raised do 63 | render_plot(backend_name, plot) 64 | end 65 | end 66 | 67 | def test_line_plot_with_categorical_size(data) 68 | omit("TODO: support categorical variable in size dimension") 69 | adapter_name, backend_name = data.values_at(:adapter, :backend) 70 | setup_data(adapter_name) 71 | setup_backend(backend_name) 72 | plot = Charty.line_plot(data: @data, x: :x, y: :y, size: :c) 73 | assert_nothing_raised do 74 | render_plot(backend_name, plot) 75 | end 76 | end 77 | 78 | def test_line_plot_with_style(data) 79 | adapter_name, backend_name = data.values_at(:adapter, :backend) 80 | setup_data(adapter_name) 81 | setup_backend(backend_name) 82 | plot = Charty.line_plot(data: @data, x: :x, y: :y, style: :c) 83 | assert_nothing_raised do 84 | render_plot(backend_name, plot) 85 | end 86 | end 87 | 88 | def test_line_plot_error_bar_sd(data) 89 | adapter_name, backend_name = data.values_at(:adapter, :backend) 90 | setup_data(adapter_name) 91 | setup_backend(backend_name) 92 | plot = Charty.line_plot(data: @data, x: :x, y: :y, error_bar: :sd) 93 | assert_nothing_raised do 94 | render_plot(backend_name, plot) 95 | end 96 | end 97 | 98 | def test_line_plot_xy_log(data) 99 | adapter_name, backend_name = data.values_at(:adapter, :backend) 100 | setup_data(adapter_name) 101 | setup_backend(backend_name) 102 | plot = Charty.line_plot(data: @data, x: :x, y: :y, x_scale: :log, y_scale: :log) 103 | assert_nothing_raised do 104 | render_plot(backend_name, plot) 105 | end 106 | end 107 | 108 | def setup_array_data 109 | @data = { 110 | y: Array.new(100) {|i| rand }, 111 | x: Array.new(100) {|i| rand(100) }, 112 | c: Array.new(100) {|i| ["red", "blue", "green"][rand(3)] }, 113 | d: Array.new(100) {|i| rand(10..50) } 114 | } 115 | end 116 | 117 | def setup_arrow_data 118 | @data = Arrow::Table.new(@data) 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /test/plotter_test.rb: -------------------------------------------------------------------------------- 1 | class PlotterTest < Test::Unit::TestCase 2 | def setup 3 | @plotter = Charty::Plotter.new(:plotly) 4 | @data = { 5 | foo: [1, 2, 3, 4, 5, 6, 7], 6 | square: [1, 4, 9, 16, 25, 36, 49], 7 | cubic: [1, 8, 27, 64, 125, 216, 343], 8 | } 9 | end 10 | 11 | test("#table=") do 12 | @plotter.table = @data 13 | assert_equal(Charty::Vector.new(@data[:foo]), 14 | @plotter.table[:foo].data) 15 | assert_equal(Charty::Vector.new(@data[:square]), 16 | @plotter.table[:square].data) 17 | assert_equal(Charty::Vector.new(@data[:cubic]), 18 | @plotter.table[:cubic].data) 19 | end 20 | 21 | test("#to_bar") do 22 | @plotter.table = @data 23 | context = @plotter.to_bar(:foo, :cubic) 24 | assert_kind_of(Charty::RenderContext, 25 | context) 26 | assert_equal(@data[:foo], 27 | context.series[0].xs.to_a) 28 | assert_equal(@data[:cubic], 29 | context.series[0].ys.to_a) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/run.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # TODO 4 | # $VERBOSE = true 5 | 6 | require "pathname" 7 | 8 | base_dir = Pathname(__dir__).parent.expand_path 9 | 10 | lib_dir = base_dir + "lib" 11 | test_dir = base_dir + "test" 12 | test_lib_dir = test_dir + "lib" 13 | 14 | $LOAD_PATH.unshift(lib_dir.to_s) 15 | $LOAD_PATH.unshift(test_lib_dir.to_s) 16 | 17 | require_relative "helper" 18 | 19 | exit(Test::Unit::AutoRunner.run(true, test_dir.to_s)) 20 | -------------------------------------------------------------------------------- /test/table/arrow_test.rb: -------------------------------------------------------------------------------- 1 | class TableArrowTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | def setup 5 | arrow_required 6 | @data = Arrow::Table.new(a: [1, 2, 3, 4], 7 | b: [5, 6, 7, 8], 8 | c: [9, 10, 11, 12]) 9 | @table = Charty::Table.new(@data) 10 | end 11 | 12 | test("#column_names") do 13 | assert_equal(["a", "b", "c"], 14 | @table.column_names) 15 | end 16 | 17 | sub_test_case("with string column name") do 18 | sub_test_case("#[]") do 19 | test("class") do 20 | assert_equal({ 21 | "a" => Charty::Vector, 22 | "b" => Charty::Vector, 23 | "c" => Charty::Vector, 24 | }, 25 | { 26 | "a" => @table["a"].class, 27 | "b" => @table["b"].class, 28 | "c" => @table["c"].class, 29 | }) 30 | end 31 | 32 | test("name") do 33 | assert_equal({ 34 | "a" => "a", 35 | "b" => "b", 36 | "c" => "c", 37 | }, 38 | { 39 | "a" => @table["a"].name, 40 | "b" => @table["b"].name, 41 | "c" => @table["c"].name, 42 | }) 43 | end 44 | 45 | test("values") do 46 | assert_equal({ 47 | "a" => Numo::DFloat[1, 2, 3, 4], 48 | "b" => Numo::DFloat[5, 6, 7, 8], 49 | "c" => Numo::DFloat[9, 10, 11, 12], 50 | }, 51 | { 52 | "a" => @table["a"].data, 53 | "b" => @table["b"].data, 54 | "c" => @table["c"].data, 55 | }) 56 | end 57 | end 58 | end 59 | 60 | sub_test_case("with symbol column name") do 61 | sub_test_case("#[]") do 62 | sub_test_case("with default index") do 63 | test("class") do 64 | assert_equal({ 65 | :a => Charty::Vector, 66 | :b => Charty::Vector, 67 | :c => Charty::Vector, 68 | }, 69 | { 70 | :a => @table[:a].class, 71 | :b => @table[:b].class, 72 | :c => @table[:c].class, 73 | }) 74 | end 75 | 76 | test("name") do 77 | assert_equal({ 78 | :a => "a", 79 | :b => "b", 80 | :c => "c", 81 | }, 82 | { 83 | :a => @table[:a].name, 84 | :b => @table[:b].name, 85 | :c => @table[:c].name, 86 | }) 87 | end 88 | 89 | test("values") do 90 | assert_equal({ 91 | :a => Arrow::Array.new([1, 2, 3, 4]), 92 | :b => Arrow::Array.new([5, 6, 7, 8]), 93 | :c => Arrow::Array.new([9, 10, 11, 12]), 94 | }, 95 | { 96 | :a => @table[:a].data, 97 | :b => @table[:b].data, 98 | :c => @table[:c].data, 99 | }) 100 | end 101 | end 102 | 103 | sub_test_case("with non-default index") do 104 | def test_aref 105 | @table.index = [1, 20, 300, 4000] 106 | assert_equal([1, 20, 300, 4000], 107 | @table[:a].index.to_a) 108 | end 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /test/table/csv_test.rb: -------------------------------------------------------------------------------- 1 | class TableCSVTest < Test::Unit::TestCase 2 | def setup 3 | @data = CSV.parse(< [ 5 | "Kingfisher", 6 | "Snow", 7 | "Bud Light", 8 | "Tiger Beer", 9 | "Budweiser", 10 | ], 11 | "Gallons sold" => [ 12 | 500, 13 | 400, 14 | 450, 15 | 200, 16 | 250, 17 | ] 18 | ) 19 | @table = Charty::Table.new(@data) 20 | end 21 | 22 | def test_length 23 | assert_equal(5, 24 | @table.length) 25 | end 26 | 27 | sub_test_case("#index") do 28 | sub_test_case("without explicit index") do 29 | def test_index 30 | assert_equal({ 31 | class: Charty::DaruIndex, 32 | length: 5, 33 | values: [0, 1, 2, 3, 4], 34 | }, 35 | { 36 | class: @table.index.class, 37 | length: @table.index.length, 38 | values: @table.index.to_a 39 | }) 40 | end 41 | end 42 | 43 | sub_test_case("with explicit range index") do 44 | def test_index 45 | @table.index = 10...15 46 | assert_equal({ 47 | class: Charty::DaruIndex, 48 | length: 5, 49 | values: [10, 11, 12, 13, 14], 50 | }, 51 | { 52 | class: @table.index.class, 53 | length: @table.index.length, 54 | values: @table.index.to_a 55 | }) 56 | end 57 | end 58 | 59 | sub_test_case("with explicit string index") do 60 | def test_index 61 | @table.index = ["a", "b", "c", "d", "e"] 62 | assert_equal({ 63 | class: Charty::DaruIndex, 64 | length: 5, 65 | values: ["a", "b", "c", "d", "e"] 66 | }, 67 | { 68 | class: @table.index.class, 69 | length: @table.index.length, 70 | values: @table.index.to_a 71 | }) 72 | end 73 | end 74 | 75 | sub_test_case(".name") do 76 | def test_index_name 77 | values = [@table.index.name] 78 | @table.index.name = "abc" 79 | values << @table.index.name 80 | assert_equal([nil, "abc"], values) 81 | end 82 | end 83 | end 84 | 85 | sub_test_case("#columns") do 86 | sub_test_case("default columns") do 87 | def test_columns 88 | assert_equal({ 89 | class: Charty::DaruIndex, 90 | length: 2, 91 | values: ["Beer", "Gallons sold"], 92 | }, 93 | { 94 | class: @table.columns.class, 95 | length: @table.columns.length, 96 | values: @table.columns.to_a 97 | }) 98 | end 99 | end 100 | 101 | sub_test_case("with range columns") do 102 | def test_columns 103 | @table.columns = 3...5 104 | assert_equal({ 105 | class: Charty::DaruIndex, 106 | length: 2, 107 | values: [3, 4], 108 | }, 109 | { 110 | class: @table.columns.class, 111 | length: @table.columns.length, 112 | values: @table.columns.to_a 113 | }) 114 | end 115 | end 116 | 117 | sub_test_case("with string columns") do 118 | def test_columns 119 | @table.columns = ["a", "b"] 120 | assert_equal({ 121 | class: Charty::DaruIndex, 122 | length: 2, 123 | values: ["a", "b"], 124 | }, 125 | { 126 | class: @table.columns.class, 127 | length: @table.columns.length, 128 | values: @table.columns.to_a 129 | }) 130 | end 131 | end 132 | 133 | sub_test_case(".name") do 134 | def test_columns_name 135 | values = [@table.columns.name] 136 | @table.columns.name = "abc" 137 | values << @table.columns.name 138 | assert_equal([nil, "abc"], values) 139 | end 140 | end 141 | end 142 | 143 | test("#column_names") do 144 | assert_equal(["Beer", "Gallons sold"], 145 | @table.column_names) 146 | end 147 | 148 | sub_test_case("#[]") do 149 | sub_test_case("with default index") do 150 | test("class") do 151 | assert_equal(Charty::Vector, 152 | @table["Beer"].class) 153 | end 154 | 155 | test("names") do 156 | assert_equal({ 157 | "Beer" => "Beer", 158 | "Gallons sold" => "Gallons sold" 159 | }, 160 | { 161 | "Beer" => @table["Beer"].name, 162 | "Gallons sold" => @table["Gallons sold"].name 163 | }) 164 | end 165 | 166 | test("values") do 167 | vectors = [ 168 | Daru::Vector.new([ 169 | "Kingfisher", 170 | "Snow", 171 | "Bud Light", 172 | "Tiger Beer", 173 | "Budweiser", 174 | ]), 175 | Daru::Vector.new([ 176 | 500, 177 | 400, 178 | 450, 179 | 200, 180 | 250, 181 | ]), 182 | ] 183 | assert_equal({ 184 | "Beer" => vectors[0], 185 | "Gallons sold" => vectors[1] 186 | }, 187 | { 188 | "Beer" => @table["Beer"].data, 189 | "Gallons sold" => @table["Gallons sold"].data 190 | }) 191 | end 192 | end 193 | 194 | sub_test_case("with non-default index") do 195 | def test_aref 196 | @table.index = [1, 20, 300, 4000, 50000] 197 | assert_equal([1, 20, 300, 4000, 50000], 198 | @table["Beer"].index.to_a) 199 | end 200 | end 201 | end 202 | 203 | sub_test_case("#drop_na") do 204 | def test_equality 205 | omit("TODO: Support drop_na in daru table adapter") 206 | end 207 | end 208 | end 209 | -------------------------------------------------------------------------------- /test/table/pandas_test.rb: -------------------------------------------------------------------------------- 1 | class TablePandasTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | def setup 5 | pandas_required 6 | 7 | @data = Pandas::DataFrame.new(data: [[1, 2, 3], [4, 5, 6]], columns: ["a", "b", "c"]) 8 | @table = Charty::Table.new(@data) 9 | end 10 | 11 | test("#column_names") do 12 | assert_equal(["a", "b", "c"], 13 | @table.column_names) 14 | end 15 | 16 | sub_test_case("#columns") do 17 | def test_columns 18 | assert_equal(["a", "b", "c"], 19 | @table.columns.to_a) 20 | end 21 | 22 | sub_test_case(".name") do 23 | def test_columns_name 24 | values = [@table.columns.name] 25 | @table.columns.name = "abc" 26 | values << @table.columns.name 27 | assert_equal([nil, "abc"], values) 28 | end 29 | end 30 | end 31 | 32 | def test_length 33 | assert_equal(2, 34 | @table.length) 35 | end 36 | 37 | sub_test_case("#index") do 38 | def test_index 39 | assert_equal([0, 1], 40 | @table.index.to_a) 41 | end 42 | 43 | sub_test_case(".name") do 44 | def test_index_name 45 | values = [@table.index.name] 46 | @table.index.name = "abc" 47 | values << @table.index.name 48 | assert_equal([nil, "abc"], values) 49 | end 50 | end 51 | end 52 | 53 | sub_test_case("#[]") do 54 | test("with default index") do 55 | value = @table["b"] 56 | assert_equal({ 57 | class: Charty::Vector, 58 | length: 2, 59 | name: "b", 60 | values: [2, 5] 61 | }, 62 | { 63 | class: value.class, 64 | length: value.length, 65 | name: value.name, 66 | values: value.to_a 67 | }) 68 | end 69 | 70 | sub_test_case("with non-default index") do 71 | def test_aref 72 | @table.index = [1, 20] 73 | assert_equal([1, 20], 74 | @table["b"].index.to_a) 75 | end 76 | end 77 | end 78 | 79 | sub_test_case("#drop_na") do 80 | def setup 81 | pandas_required 82 | @data = Pandas::DataFrame.new(data: { 83 | foo: [1, Float::NAN, 3, 4, 5], 84 | bar: [10, 20, 30, 40, 50], 85 | baz: ["a", "b", "c", nil, "e"] 86 | }) 87 | @table = Charty::Table.new(@data) 88 | end 89 | 90 | def test_equality 91 | assert_equal(Charty::Table.new( 92 | Pandas::DataFrame.new( 93 | data: { 94 | foo: [1.0, 3.0, 5.0], 95 | bar: [10, 30, 50], 96 | baz: ["a", "c", "e"] 97 | }, 98 | index: [0, 2, 4] 99 | ) 100 | ), 101 | @table.drop_na) 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/table/table_aset_test.rb: -------------------------------------------------------------------------------- 1 | class TableAsetTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | if {}.respond_to?(:transform_keys) 5 | def transform_keys(h, &block) 6 | h.transform_keys(&block) 7 | end 8 | else 9 | def transform_keys(h) 10 | h.map {|k, v| [yield(k), v] }.to_h 11 | end 12 | end 13 | 14 | def raw_data(key_type) 15 | raw_data = { 16 | foo: [1, 2, 3, 4, 5], 17 | bar: ["a", "b", "c", "d", "e"] 18 | } 19 | case key_type 20 | when :string 21 | transform_keys(raw_data, &:to_s) 22 | else 23 | raw_data 24 | end 25 | end 26 | 27 | sub_test_case("Array Hash") do 28 | data(:data_key_type, [:string, :symbol], keep: true) 29 | data(:aset_key_type, [:string, :symbol], keep: true) 30 | def test_aset_existing(data) 31 | data_key_type, aset_key_type = data.values_at(:data_key_type, :aset_key_type) 32 | table = Charty::Table.new(raw_data(data_key_type), index: [2, 4, 6, 8, 10]) 33 | key = case aset_key_type 34 | when :symbol 35 | :bar 36 | else 37 | "bar" 38 | end 39 | table[key] = [10, 20, 30, 40, 50] 40 | expected_data = { 41 | foo: [1, 2, 3, 4, 5], 42 | bar: [10, 20, 30, 40, 50] 43 | } 44 | expected_data = transform_keys(expected_data, &:to_s) if data_key_type == :string 45 | assert_equal(Charty::Table.new(expected_data, index: [2, 4, 6, 8, 10]), 46 | table) 47 | end 48 | 49 | def test_aset_new(data) 50 | data_key_type, aset_key_type = data.values_at(:data_key_type, :aset_key_type) 51 | table = Charty::Table.new(raw_data(data_key_type), index: [2, 4, 6, 8, 10]) 52 | key = case aset_key_type 53 | when :symbol 54 | :bar 55 | else 56 | "bar" 57 | end 58 | table[key] = [10, 20, 30, 40, 50] 59 | expected_data = { 60 | foo: [1, 2, 3, 4, 5], 61 | bar: [10, 20, 30, 40, 50] 62 | } 63 | expected_data = transform_keys(expected_data, &:to_s) if data_key_type == :string 64 | assert_equal(Charty::Table.new(expected_data, index: [2, 4, 6, 8, 10]), 65 | table) 66 | end 67 | end 68 | 69 | sub_test_case("Daru") do 70 | data(:data_key_type, [:string, :symbol], keep: true) 71 | data(:aset_key_type, [:string, :symbol], keep: true) 72 | def test_aset_existing(data) 73 | data_key_type, aset_key_type = data.values_at(:data_key_type, :aset_key_type) 74 | data = Daru::DataFrame.new(raw_data(data_key_type)) 75 | table = Charty::Table.new(data, index: [2, 4, 6, 8, 10]) 76 | key = case aset_key_type 77 | when :symbol 78 | :bar 79 | else 80 | "bar" 81 | end 82 | table[key] = [10, 20, 30, 40, 50] 83 | expected_data = { 84 | foo: [1, 2, 3, 4, 5], 85 | bar: [10, 20, 30, 40, 50] 86 | } 87 | expected_data = transform_keys(expected_data, &:to_s) if data_key_type == :string 88 | expected_data = Daru::DataFrame.new(expected_data) 89 | assert_equal(Charty::Table.new(expected_data, index: [2, 4, 6, 8, 10]), 90 | table) 91 | end 92 | 93 | def test_aset_new(data) 94 | data_key_type, aset_key_type = data.values_at(:data_key_type, :aset_key_type) 95 | data = Daru::DataFrame.new(raw_data(data_key_type)) 96 | table = Charty::Table.new(data, index: [2, 4, 6, 8, 10]) 97 | key = case aset_key_type 98 | when :symbol 99 | :bar 100 | else 101 | "bar" 102 | end 103 | table[key] = [10, 20, 30, 40, 50] 104 | expected_data = { 105 | foo: [1, 2, 3, 4, 5], 106 | bar: [10, 20, 30, 40, 50] 107 | } 108 | expected_data = transform_keys(expected_data, &:to_s) if data_key_type == :string 109 | expected_data = Daru::DataFrame.new(expected_data) 110 | assert_equal(Charty::Table.new(expected_data, index: [2, 4, 6, 8, 10]), 111 | table) 112 | end 113 | end 114 | 115 | sub_test_case("Pandas") do 116 | def setup 117 | pandas_required 118 | end 119 | 120 | data(:data_key_type, [:string, :symbol], keep: true) 121 | data(:aset_key_type, [:string, :symbol], keep: true) 122 | def test_aset_existing(data) 123 | data_key_type, aset_key_type = data.values_at(:data_key_type, :aset_key_type) 124 | data = Pandas::DataFrame.new(data: raw_data(data_key_type)) 125 | table = Charty::Table.new(data, index: [2, 4, 6, 8, 10]) 126 | key = case aset_key_type 127 | when :symbol 128 | :bar 129 | else 130 | "bar" 131 | end 132 | table[key] = [10, 20, 30, 40, 50] 133 | expected_data = { 134 | foo: Pandas::Series.new([1, 2, 3, 4, 5]), 135 | bar: Pandas::Series.new([10, 20, 30, 40, 50], dtype: "object") 136 | } 137 | expected_data = transform_keys(expected_data, &:to_s) if data_key_type == :string 138 | expected_data = Pandas::DataFrame.new(data: expected_data) 139 | assert_equal(Charty::Table.new(expected_data, index: [2, 4, 6, 8, 10]), 140 | table) 141 | end 142 | 143 | def test_aset_new(data) 144 | data_key_type, aset_key_type = data.values_at(:data_key_type, :aset_key_type) 145 | data = Pandas::DataFrame.new(data: raw_data(data_key_type)) 146 | table = Charty::Table.new(data, index: [2, 4, 6, 8, 10]) 147 | key = case aset_key_type 148 | when :symbol 149 | :bar 150 | else 151 | "bar" 152 | end 153 | table[key] = [10, 20, 30, 40, 50] 154 | expected_data = { 155 | foo: Pandas::Series.new([1, 2, 3, 4, 5]), 156 | bar: Pandas::Series.new([10, 20, 30, 40, 50], dtype: "object") 157 | } 158 | expected_data = transform_keys(expected_data, &:to_s) if data_key_type == :string 159 | expected_data = Pandas::DataFrame.new(data: expected_data) 160 | assert_equal(Charty::Table.new(expected_data, index: [2, 4, 6, 8, 10]), 161 | table) 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/table/table_melt_test.rb: -------------------------------------------------------------------------------- 1 | class TableMeltTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | sub_test_case("generic table data") do 5 | data(:adapter_type, [:array_hash, :daru, :pandas], keep: true) 6 | data(:key_type, [:string, :symbol], keep: true) 7 | def test_melt_without_id_vars(data) 8 | setup_table(data[:adapter_type], data[:key_type]) 9 | melted = @table.melt 10 | assert_equal(@expected_without_id_vars, melted) 11 | end 12 | 13 | def test_melt_with_id_vars(data) 14 | setup_table(data[:adapter_type], data[:key_type]) 15 | melted = @table.melt(id_vars: :name) 16 | assert_equal(@expected_with_id_vars, melted) 17 | end 18 | end 19 | 20 | sub_test_case("CSV") do 21 | def test_melt_without_id_vars 22 | setup_table 23 | melted = @table.melt 24 | assert_equal(@expected_without_id_vars, melted) 25 | end 26 | 27 | def test_melt_with_id_vars 28 | setup_table 29 | melted = @table.melt(id_vars: :name) 30 | assert_equal(@expected_with_id_vars, melted) 31 | end 32 | 33 | def setup_table 34 | @table = Charty::Table.new(csv_table) 35 | @expected_without_id_vars = Charty::Table.new(expected_data_without_id_vars) 36 | @expected_with_id_vars = Charty::Table.new(expected_data_with_id_vars) 37 | end 38 | end 39 | 40 | def setup_table(adapter_type, key_type) 41 | send("setup_table_by_#{adapter_type}", key_type) 42 | end 43 | 44 | def setup_table_by_array_hash(key_type) 45 | @table = Charty::Table.new(raw_data(key_type)) 46 | @expected_without_id_vars = Charty::Table.new(expected_data_without_id_vars) 47 | @expected_with_id_vars = Charty::Table.new(expected_data_with_id_vars) 48 | end 49 | 50 | def setup_table_by_daru(key_type) 51 | @table = Charty::Table.new(Daru::DataFrame.new(raw_data(key_type))) 52 | @expected_without_id_vars = Charty::Table.new(expected_data_without_id_vars) 53 | @expected_with_id_vars = Charty::Table.new(expected_data_with_id_vars) 54 | end 55 | 56 | def setup_table_by_pandas(key_type) 57 | pandas_required 58 | csv = csv_table.by_col! 59 | data = csv.headers.map { |cn| 60 | [cn, csv[cn]] 61 | }.to_h 62 | @table = Charty::Table.new(Pandas::DataFrame.new(data: data)) 63 | @expected_without_id_vars = Charty::Table.new(Pandas::DataFrame.new(data: expected_data_without_id_vars)) 64 | @expected_with_id_vars = Charty::Table.new(Pandas::DataFrame.new(data: expected_data_with_id_vars)) 65 | end 66 | 67 | def expected_data_without_id_vars 68 | { 69 | variable: [ 70 | "name", "name", 71 | "2018", "2018", 72 | "2019", "2019", 73 | "2020", "2020" 74 | ], 75 | value: [ 76 | "GOOG", "AAPL", 77 | 1035.61, 39.44, 78 | 1337.02, 73.41, 79 | 1751.88, 132.69 80 | ] 81 | } 82 | end 83 | 84 | def expected_data_with_id_vars 85 | { 86 | name: [ 87 | "GOOG", "AAPL", 88 | "GOOG", "AAPL", 89 | "GOOG", "AAPL" 90 | ], 91 | variable: [ 92 | "2018", "2018", 93 | "2019", "2019", 94 | "2020", "2020" 95 | ], 96 | value: [ 97 | 1035.61, 39.44, 98 | 1337.02, 73.41, 99 | 1751.88, 132.69 100 | ] 101 | } 102 | end 103 | 104 | def raw_data(key_type) 105 | csv = csv_table.by_col! 106 | csv.headers.map { |cn| 107 | key = if key_type == :string 108 | cn 109 | else 110 | cn.to_sym 111 | end 112 | [key, csv[cn]] 113 | }.to_h 114 | end 115 | 116 | def csv_table 117 | CSV.parse(<<~END_CSV, headers: true, converters: :all) 118 | name,2018,2019,2020 119 | GOOG,1035.61,1337.02,1751.88 120 | AAPL,39.44,73.41,132.69 121 | END_CSV 122 | end 123 | end 124 | 125 | -------------------------------------------------------------------------------- /test/table/table_reset_index_test.rb: -------------------------------------------------------------------------------- 1 | class TableResetIndexTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | sub_test_case("with unnamed index") do 5 | data(:table_adapter, [:daru, :hash, :pandas], keep: true) 6 | def test_reset_index(data) 7 | setup_table(data[:table_adapter]) 8 | assert_equal(@expected_result, 9 | @table.reset_index) 10 | end 11 | 12 | def setup_table(table_adapter) 13 | @data = { 14 | a: [5, 4, 3, 2, 1], 15 | } 16 | @index = ["a", "b", "c", "d", "e"] 17 | 18 | case table_adapter 19 | when :daru 20 | setup_table_by_daru 21 | when :hash 22 | setup_table_by_hash 23 | when :pandas 24 | setup_table_by_pandas 25 | end 26 | end 27 | 28 | def setup_table_by_daru 29 | omit("TODO: reset_index with daru") 30 | end 31 | 32 | def setup_table_by_hash 33 | @table = Charty::Table.new(@data, index: @index) 34 | @expected_result = Charty::Table.new( 35 | { index: @index }.merge(@data) 36 | ) 37 | end 38 | 39 | def setup_table_by_pandas 40 | pandas_required 41 | 42 | @table = Charty::Table.new(Pandas::DataFrame.new(data: @data), index: @index) 43 | @expected_result = Charty::Table.new( 44 | Pandas::DataFrame.new(data: {index: @index}.merge(@data))) 45 | end 46 | end 47 | 48 | sub_test_case("with named index") do 49 | data(:table_adapter, [:daru, :hash, :pandas], keep: true) 50 | def test_reset_index(data) 51 | setup_table(data[:table_adapter]) 52 | result = @table.group_by(@grouper).apply(:a) { |table, var| 53 | { 54 | var => table[var].mean, 55 | "#{var}_min": table[var].min, 56 | "#{var}_max": table[var].max 57 | } 58 | }.reset_index 59 | assert_equal(@expected_result, 60 | result) 61 | end 62 | 63 | def setup_table(table_adapter) 64 | @data = { 65 | a: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 66 | b: [1, 1, 1, 4, 4, 3, 2, 3, 3, 2, 4], 67 | c: ["A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C"] 68 | } 69 | @grouper = :b 70 | @expected_indices = { 71 | 1 => [0, 1, 2], 72 | 2 => [6, 9], 73 | 3 => [5, 7, 8], 74 | 4 => [3, 4, 10] 75 | } 76 | @expected_applied_data = { 77 | a: @expected_indices.values.map {|is| @data[:a].values_at(*is).mean }, 78 | a_min: @expected_indices.values.map {|is| @data[:a].values_at(*is).min }, 79 | a_max: @expected_indices.values.map {|is| @data[:a].values_at(*is).max } 80 | } 81 | 82 | case table_adapter 83 | when :daru 84 | setup_table_by_daru 85 | when :hash 86 | setup_table_by_hash 87 | when :pandas 88 | setup_table_by_pandas 89 | end 90 | end 91 | 92 | def setup_table_by_daru 93 | omit("TODO: reset_index with daru") 94 | end 95 | 96 | def setup_table_by_hash 97 | @table = Charty::Table.new(@data) 98 | @expected_result = Charty::Table.new( 99 | { b: @expected_indices.keys }.merge(@expected_applied_data) 100 | ) 101 | end 102 | 103 | def setup_table_by_pandas 104 | pandas_required 105 | 106 | @table = Charty::Table.new(Pandas::DataFrame.new(data: @data)) 107 | 108 | df = Pandas::DataFrame.new(data: { 109 | b: @expected_indices.keys, 110 | a: @expected_applied_data[:a], 111 | a_min: @expected_applied_data[:a_min].map(&:to_f), 112 | a_max: @expected_applied_data[:a_max].map(&:to_f) 113 | }) 114 | @expected_result = Charty::Table.new(df) 115 | end 116 | end 117 | end 118 | 119 | -------------------------------------------------------------------------------- /test/table/table_sort_values_test.rb: -------------------------------------------------------------------------------- 1 | class TableSortValuesTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | sub_test_case("generic case") do 5 | data(:table_adapter, [:daru, :hash, :pandas], keep: true) 6 | def test_with_one_column(data) 7 | setup_table(data[:table_adapter]) 8 | assert_equal(@expected_one_column, 9 | @table.sort_values(:b)) 10 | end 11 | 12 | def test_with_two_column(data) 13 | setup_table(data[:table_adapter]) 14 | assert_equal(@expected_two_column, 15 | @table.sort_values([:b, :c])) 16 | end 17 | 18 | sub_test_case("with red-datasets") do 19 | def test_sort_values 20 | omit("TODO: sort_values on datasets") 21 | end 22 | end 23 | 24 | def setup_table(table_adapter) 25 | @data = { 26 | a: Array.new(50) {|i| i }, 27 | b: Array.new(50) {|i| 1 + rand(15) }, 28 | c: Array.new(50) {|i| "ABCDE"[rand(5)] } 29 | } 30 | 31 | @order_one_column = (0 ... 50).sort_by {|i| [@data[:b][i], i] } 32 | @expected_one_column = @data.map { |k, v| 33 | [k, v.values_at(*@order_one_column)] 34 | }.to_h 35 | 36 | @order_two_column = (0 ... 50).sort_by {|i| [@data[:b][i], @data[:c][i], i] } 37 | @expected_two_column = @data.map { |k, v| 38 | [k, v.values_at(*@order_two_column)] 39 | }.to_h 40 | 41 | case table_adapter 42 | when :daru 43 | setup_table_by_daru 44 | when :hash 45 | setup_table_by_hash 46 | when :pandas 47 | setup_table_by_pandas 48 | end 49 | end 50 | 51 | def setup_table_by_daru 52 | omit("TODO: sort_values with daru") 53 | end 54 | 55 | def setup_table_by_hash 56 | @table = Charty::Table.new(@data) 57 | @expected_one_column = Charty::Table.new(@expected_one_column, index: @order_one_column) 58 | @expected_two_column = Charty::Table.new(@expected_two_column, index: @order_two_column) 59 | end 60 | 61 | def setup_table_by_pandas 62 | omit("TODO: sort_values with pandas") 63 | end 64 | end 65 | 66 | sub_test_case("including missing values") do 67 | data(:table_adapter, [:daru, :hash, :pandas], keep: true) 68 | def test_with_one_column_first(data) 69 | setup_table(data[:table_adapter]) 70 | assert_equal(@expected_one_column_first, 71 | @table.sort_values(:b, na_position: :first)) 72 | end 73 | 74 | def test_with_one_column_last(data) 75 | setup_table(data[:table_adapter]) 76 | assert_equal(@expected_one_column_last, 77 | @table.sort_values(:b, na_position: :last)) 78 | end 79 | 80 | def test_with_two_column_first(data) 81 | setup_table(data[:table_adapter]) 82 | assert_equal(@expected_two_column_first, 83 | @table.sort_values([:c, :b], na_position: :first)) 84 | end 85 | 86 | def test_with_two_column_last(data) 87 | setup_table(data[:table_adapter]) 88 | assert_equal(@expected_two_column_last, 89 | @table.sort_values([:c, :b], na_position: :last)) 90 | end 91 | 92 | sub_test_case("with red-datasets") do 93 | def test_sort_values 94 | omit("TODO: sort_values on datasets") 95 | end 96 | end 97 | 98 | def setup_table(table_adapter) 99 | nan = Float::NAN 100 | 101 | @data = { 102 | a: [1, 2, 3, 4, 5], 103 | b: [2.0, 4.0, nan, 1.0, 3.0], 104 | c: ["a", "b", "a", nil, "a"] 105 | } 106 | 107 | @order_one_column_first = [2, 3, 0, 4, 1] 108 | @expected_one_column_first = { 109 | a: [3, 4, 1, 5, 2], 110 | b: [nan, 1.0, 2.0, 3.0, 4.0], 111 | c: ["a", nil, "a", "a", "b"] 112 | } 113 | 114 | @order_one_column_last = [3, 0, 4, 1, 2] 115 | @expected_one_column_last = { 116 | a: [4, 1, 5, 2, 3], 117 | b: [1.0, 2.0, 3.0, 4.0, nan], 118 | c: [nil, "a", "a", "b", "a"] 119 | } 120 | 121 | @order_two_column_first = [3, 2, 0, 4, 1] 122 | @expected_two_column_first = { 123 | a: [4, 3, 1, 5, 2], 124 | b: [1.0, nan, 2.0, 3.0, 4.0], 125 | c: [nil, "a", "a", "a", "b"] 126 | } 127 | 128 | @order_two_column_last = [0, 4, 2, 1, 3] 129 | @expected_two_column_last = { 130 | a: [1, 5, 3, 2, 4], 131 | b: [2.0, 3.0, nan, 4.0, 1.0], 132 | c: ["a", "a", "a", "b", nil] 133 | } 134 | 135 | case table_adapter 136 | when :daru 137 | setup_table_by_daru 138 | when :hash 139 | setup_table_by_hash 140 | when :pandas 141 | setup_table_by_pandas 142 | end 143 | end 144 | 145 | def setup_table_by_daru 146 | omit("TODO: sort_values with daru") 147 | end 148 | 149 | def setup_table_by_hash 150 | @table = Charty::Table.new(@data) 151 | @expected_one_column_first = Charty::Table.new(@expected_one_column_first, index: @order_one_column_first) 152 | @expected_one_column_last = Charty::Table.new(@expected_one_column_last, index: @order_one_column_last) 153 | @expected_two_column_first = Charty::Table.new(@expected_two_column_first, index: @order_two_column_first) 154 | @expected_two_column_last = Charty::Table.new(@expected_two_column_last, index: @order_two_column_last) 155 | end 156 | 157 | def setup_table_by_pandas 158 | pandas_required 159 | @table = Charty::Table.new(Pandas::DataFrame.new(data: @data)) 160 | @expected_one_column_first = Charty::Table.new( 161 | Pandas::DataFrame.new(data: @expected_one_column_first, index: @order_one_column_first)) 162 | @expected_one_column_last = Charty::Table.new( 163 | Pandas::DataFrame.new(data: @expected_one_column_last, index: @order_one_column_last)) 164 | @expected_two_column_first = Charty::Table.new( 165 | Pandas::DataFrame.new(data: @expected_two_column_first, index: @order_two_column_first)) 166 | @expected_two_column_last = Charty::Table.new( 167 | Pandas::DataFrame.new(data: @expected_two_column_last, index: @order_two_column_last)) 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /test/table_adapters_test.rb: -------------------------------------------------------------------------------- 1 | class TableAdaptersTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | sub_test_case(".find_adapter_class") do 5 | test("for a hash of arrays") do 6 | data = { 7 | "foo" => [1, 2, 3, 4], 8 | "bar" => [5, 6, 7, 8], 9 | } 10 | assert_equal(Charty::TableAdapters::HashAdapter, 11 | Charty::TableAdapters.find_adapter_class(data)) 12 | end 13 | 14 | test("for an array of hashes") do 15 | data = [ 16 | {"foo" => 1, "bar" => 5}, 17 | {"foo" => 2, "bar" => 6}, 18 | {"foo" => 3, "bar" => 7}, 19 | {"foo" => 4, "bar" => 8}, 20 | ] 21 | assert_equal(Charty::TableAdapters::HashAdapter, 22 | Charty::TableAdapters.find_adapter_class(data)) 23 | end 24 | 25 | test("for an array of arrays") do 26 | data = [ 27 | [1, 2, 3, 4], 28 | [5, 6, 7, 8] 29 | ] 30 | assert_equal(Charty::TableAdapters::HashAdapter, 31 | Charty::TableAdapters.find_adapter_class(data)) 32 | end 33 | 34 | test("for an array of Numo::NArray arrays") do 35 | data = [ 36 | [1, 2, 3, 4], 37 | [5, 6, 7, 8] 38 | ] 39 | assert_equal(Charty::TableAdapters::HashAdapter, 40 | Charty::TableAdapters.find_adapter_class(data)) 41 | end 42 | 43 | test("for an array of scalar values") do 44 | data = [1, 2, 3, 4] 45 | assert_equal(Charty::TableAdapters::HashAdapter, 46 | Charty::TableAdapters.find_adapter_class(data)) 47 | end 48 | 49 | test("for a Daru::DataFrame") do 50 | data = Daru::DataFrame.new( 51 | "foo" => [1, 2, 3, 4], 52 | "bar" => [5, 6, 7, 8] 53 | ) 54 | assert_equal(Charty::TableAdapters::DaruAdapter, 55 | Charty::TableAdapters.find_adapter_class(data)) 56 | end 57 | 58 | test("for a Numo::NArray matrix") do 59 | numo_required 60 | 61 | data = Numo::Int32[ 62 | [1, 5, 9], 63 | [2, 6, 10], 64 | [3, 7, 11], 65 | [4, 8, 12], 66 | ] 67 | assert_equal(Charty::TableAdapters::NArrayAdapter, 68 | Charty::TableAdapters.find_adapter_class(data)) 69 | end 70 | 71 | test("for a NMatrix matrix") do 72 | nmatrix_required 73 | 74 | data = NMatrix.new([4, 3], 75 | [ 76 | 1, 5, 9, 77 | 2, 6, 10, 78 | 3, 7, 11, 79 | 4, 8, 12, 80 | ]) 81 | assert_equal(Charty::TableAdapters::NMatrixAdapter, 82 | Charty::TableAdapters.find_adapter_class(data)) 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/util_test.rb: -------------------------------------------------------------------------------- 1 | class UtilTest < Test::Unit::TestCase 2 | sub_test_case(".filter_map") do 3 | test("for array") do 4 | assert_equal([20, 40, 60, 80], 5 | Charty::Util.filter_map([1, 2, 3, 4, 5, 6, 7, 8, 9]) {|x| x*10 if x.even? }) 6 | end 7 | 8 | test("for range") do 9 | assert_equal([20, 40, 60, 80], 10 | Charty::Util.filter_map(1..9) {|x| x*10 if x.even? }) 11 | end 12 | end 13 | 14 | def test_missing_p 15 | pseudo_nil = Object.new 16 | class << pseudo_nil 17 | def nil? 18 | true 19 | end 20 | end 21 | 22 | assert_equal([ 23 | false, 24 | false, 25 | true, 26 | false, 27 | true, 28 | false, 29 | false, 30 | true, 31 | true 32 | ], 33 | [ 34 | Charty::Util.missing?(1), 35 | Charty::Util.missing?(1.1), 36 | Charty::Util.missing?(Float::NAN), 37 | Charty::Util.missing?(Float::INFINITY), 38 | Charty::Util.missing?(BigDecimal::NAN), 39 | Charty::Util.missing?("nan"), 40 | Charty::Util.missing?(Object.new), 41 | Charty::Util.missing?(nil), 42 | Charty::Util.missing?(pseudo_nil), 43 | ]) 44 | end 45 | 46 | def test_nan_p 47 | assert_equal([ 48 | false, 49 | false, 50 | true, 51 | false, 52 | true, 53 | false, 54 | false 55 | ], 56 | [ 57 | Charty::Util.nan?(1), 58 | Charty::Util.nan?(1.1), 59 | Charty::Util.nan?(Float::NAN), 60 | Charty::Util.nan?(Float::INFINITY), 61 | Charty::Util.nan?(BigDecimal::NAN), 62 | Charty::Util.nan?("nan"), 63 | Charty::Util.nan?(Object.new) 64 | ]) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/vector/iloc_test.rb: -------------------------------------------------------------------------------- 1 | class VectorIlocTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | sub_test_case("Charty::Vector#iloc") do 5 | data(:adapter_type, [:array, :daru, :narray, :numpy, :pandas], keep: true) 6 | 7 | test("with the default index") do |data| 8 | vector = setup_vector(data[:adapter_type], [10, 20, 30]) 9 | assert_equal([20, 10, 30], 10 | [vector.iloc(1), vector.iloc(0), vector.iloc(2)]) 11 | end 12 | 13 | test("with non-zero origin index") do |data| 14 | vector = setup_vector(data[:adapter_type], [10, 20, 30], index: [5, 10, 15]) 15 | assert_equal([20, 10, 30], 16 | [vector.iloc(1), vector.iloc(0), vector.iloc(2)]) 17 | end 18 | 19 | test("with string index") do |data| 20 | vector = setup_vector(data[:adapter_type], [10, 20, 30], index: ["a", "b", "c"]) 21 | assert_equal([20, 10, 30], 22 | [vector.iloc(1), vector.iloc(0), vector.iloc(2)]) 23 | end 24 | end 25 | 26 | def setup_vector(adapter_type, data, index: nil) 27 | send("setup_vector_with_#{adapter_type}", data, index) 28 | end 29 | 30 | def setup_vector_with_array(data, index) 31 | Charty::Vector.new(data, index: index) 32 | end 33 | 34 | def setup_vector_with_daru(data, index) 35 | Charty::Vector.new(Daru::Vector.new(data), index: index) 36 | end 37 | 38 | def setup_vector_with_narray(data, index) 39 | numo_required 40 | Charty::Vector.new(Numo::Int64[*data], index: index) 41 | end 42 | 43 | def setup_vector_with_numpy(data, index) 44 | numpy_required 45 | Charty::Vector.new(Numpy.asarray(data, dtype: "int64"), index: index) 46 | end 47 | 48 | def setup_vector_with_pandas(data, index) 49 | pandas_required 50 | Charty::Vector.new(Pandas::Series.new(data), index: index) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/vector/nmatrix_test.rb: -------------------------------------------------------------------------------- 1 | class VectorNMatrixTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | def setup 5 | nmatrix_required 6 | 7 | @data = NMatrix[1, 2, 3, 4, 5] 8 | @vector = Charty::Vector.new(@data) 9 | end 10 | 11 | def test_length 12 | assert_equal(5, @vector.length) 13 | end 14 | 15 | def test_name 16 | values = [@vector.name] 17 | @vector.name = "abc" 18 | values << @vector.name 19 | assert_equal([nil, "abc"], values) 20 | end 21 | 22 | sub_test_case("#index") do 23 | sub_test_case("without explicit index") do 24 | def test_index 25 | assert_equal({ 26 | class: Charty::RangeIndex, 27 | length: 5, 28 | values: [0, 1, 2, 3, 4], 29 | }, 30 | { 31 | class: @vector.index.class, 32 | length: @vector.index.length, 33 | values: @vector.index.to_a 34 | }) 35 | end 36 | end 37 | 38 | sub_test_case("with string index") do 39 | def test_index 40 | @vector.index = ["a", "b", "c", "d", "e"] 41 | assert_equal({ 42 | class: Charty::Index, 43 | length: 5, 44 | values: ["a", "b", "c", "d", "e"], 45 | }, 46 | { 47 | class: @vector.index.class, 48 | length: @vector.index.length, 49 | values: @vector.index.to_a 50 | }) 51 | end 52 | end 53 | 54 | sub_test_case(".name") do 55 | def test_index_name 56 | values = [@vector.index.name] 57 | @vector.index.name = "abc" 58 | values << @vector.index.name 59 | assert_equal([nil, "abc"], values) 60 | end 61 | end 62 | end 63 | 64 | sub_test_case("#[]") do 65 | sub_test_case("without explicit index") do 66 | def test_aref 67 | assert_equal([ 68 | 2, 69 | 4 70 | ], 71 | [ 72 | @vector[1], 73 | @vector[3] 74 | ]) 75 | end 76 | end 77 | 78 | sub_test_case("with string index") do 79 | def test_aref 80 | @vector.index = ["a", "b", "c", "d", "e"] 81 | assert_equal([ 82 | 2, 83 | 3, 84 | 4, 85 | 5 86 | ], 87 | [ 88 | @vector[1], 89 | @vector["c"], 90 | @vector["d"], 91 | @vector[4] 92 | ]) 93 | end 94 | end 95 | end 96 | 97 | test("#to_a") do 98 | assert_equal([1, 2, 3, 4, 5], 99 | @vector.to_a) 100 | end 101 | 102 | test("#data") do 103 | assert_equal({ 104 | class: NMatrix, 105 | value: NMatrix[1, 2, 3, 4, 5] 106 | }, 107 | { 108 | class: @vector.data.class, 109 | value: @vector.data 110 | }) 111 | end 112 | end 113 | 114 | -------------------------------------------------------------------------------- /test/vector/vector_equality_test.rb: -------------------------------------------------------------------------------- 1 | class VectorEqualityTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | data(:adapter_type, [:array, :daru, :narray, :nmatrix, :numpy, :pandas], keep: true) 5 | data(:other_adapter_type, [:array, :daru, :narray, :nmatrix, :numpy, :pandas], keep: true) 6 | def test_equality(data) 7 | vector = setup_vector(data[:adapter_type]) 8 | other_vector = setup_vector(data[:other_adapter_type]) 9 | 10 | assert_equal(vector, other_vector) 11 | end 12 | 13 | def test_equality_with_different_names(data) 14 | vector = setup_vector(data[:adapter_type], name: "foo") 15 | other_vector = setup_vector(data[:other_adapter_type], name: "bar") 16 | 17 | assert_equal(vector, other_vector) 18 | end 19 | 20 | def test_unequality_by_values(data) 21 | vector = setup_vector(data[:adapter_type]) 22 | other_vector = setup_vector(data[:other_adapter_type], [2, 3, 4, 5, 1]) 23 | 24 | assert_not_equal(vector, other_vector) 25 | end 26 | 27 | def test_unequality_by_index(data) 28 | vector = setup_vector(data[:adapter_type]) 29 | other_vector = setup_vector(data[:other_adapter_type], index: [10, 20, 30, 40, 50]) 30 | 31 | assert_not_equal(vector, other_vector) 32 | end 33 | 34 | def setup_vector(adapter_type, data=nil, dtype=nil, index: nil, name: nil) 35 | send("setup_vector_with_#{adapter_type}", data, dtype, index: index, name: name) 36 | end 37 | 38 | def setup_vector_with_array(data, _dtype, index:, name:) 39 | data ||= default_data 40 | Charty::Vector.new(data, index: index, name: name) 41 | end 42 | 43 | def setup_vector_with_daru(data, _dtype, index:, name:) 44 | data ||= default_data 45 | data = Daru::Vector.new(data) 46 | Charty::Vector.new(data, index: index, name: name) 47 | end 48 | 49 | def setup_vector_with_narray(data, dtype, index:, name:) 50 | numo_required 51 | data ||= default_data 52 | dtype ||= default_dtype 53 | data = numo_dtype(dtype)[*data] 54 | Charty::Vector.new(data, index: index, name: name) 55 | end 56 | 57 | def setup_vector_with_nmatrix(data, dtype, index:, name:) 58 | nmatrix_required 59 | data ||= default_data 60 | dtype ||= default_dtype 61 | data = NMatrix.new([data.length], data, dtype: dtype) 62 | Charty::Vector.new(data, index: index, name: name) 63 | end 64 | 65 | def setup_vector_with_numpy(data, dtype, index:, name:) 66 | numpy_required 67 | data ||= default_data 68 | dtype ||= default_dtype 69 | data = Numpy.asarray(data, dtype: dtype) 70 | Charty::Vector.new(data, index: index, name: name) 71 | end 72 | 73 | def setup_vector_with_pandas(data, dtype, index:, name:) 74 | numpy_required 75 | data ||= default_data 76 | dtype ||= default_dtype 77 | data = Pandas::Series.new(data, dtype: dtype) 78 | Charty::Vector.new(data, index: index, name: name) 79 | end 80 | 81 | def default_data 82 | [1, 2, 3, 4, 5] 83 | end 84 | 85 | def default_dtype 86 | :int64 87 | end 88 | 89 | def numo_dtype(type) 90 | case type 91 | when :bool 92 | Numo::Bit 93 | when :int64 94 | Numo::Int64 95 | when :float64 96 | Numo::DFloat 97 | when :object 98 | Numo::RObject 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/vector/vector_scale_test.rb: -------------------------------------------------------------------------------- 1 | class VectorScaleTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | sub_test_case("linear") do 5 | data(:adapter_type, [:array, :daru, :narray, :nmatrix, :numpy, :pandas], keep: true) 6 | def test_scale_linear(data) 7 | omit("TODO") 8 | end 9 | 10 | def test_scale_inverse_linear(data) 11 | omit("TODO") 12 | end 13 | end 14 | 15 | sub_test_case("log") do 16 | data(:adapter_type, [:array, :daru, :narray, :nmatrix, :numpy, :pandas], keep: true) 17 | def test_scale_log(data) 18 | omit("TODO") 19 | end 20 | 21 | def test_scale_inverse_log(data) 22 | omit("TODO") 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/vector/vector_values_at_test.rb: -------------------------------------------------------------------------------- 1 | class VectorValuesAtTest < Test::Unit::TestCase 2 | include Charty::TestHelpers 3 | 4 | data(:adapter_type, [:array, :daru, :narray, :nmatrix, :numpy, :pandas], keep: true) 5 | def test_values_at(data) 6 | vector = setup_vector(data[:adapter_type], [1, 2, 3, 4, 5], index: [1, 3, 5, 2, 4]) 7 | assert_equal([1, 2, 3, 5, 4, 3], 8 | vector.values_at(0, 1, 2, 4, 3, 2)) 9 | end 10 | 11 | def setup_vector(adapter_type, data, index:) 12 | send("setup_vector_with_#{adapter_type}", data, index) 13 | end 14 | 15 | def setup_vector_with_array(data, index) 16 | Charty::Vector.new(data, index: index) 17 | end 18 | 19 | def setup_vector_with_daru(data, index) 20 | Charty::Vector.new(Daru::Vector.new(data), index: index) 21 | end 22 | 23 | def setup_vector_with_narray(data, index) 24 | numo_required 25 | Charty::Vector.new(Numo::Int32[*data], index: index) 26 | end 27 | 28 | def setup_vector_with_nmatrix(data, index) 29 | nmatrix_required 30 | Charty::Vector.new(NMatrix.new([data.length], data, dtype: :int32), index: index) 31 | end 32 | 33 | def setup_vector_with_numpy(data, index) 34 | numpy_required 35 | Charty::Vector.new(Numpy.array(data, dtype: :int32), index: index) 36 | end 37 | 38 | def setup_vector_with_pandas(data, index) 39 | pandas_required 40 | Charty::Vector.new(Pandas::Series.new(data, dtype: :int32), index: index) 41 | end 42 | end 43 | --------------------------------------------------------------------------------