├── .editorconfig ├── .gitignore ├── .overcommit.yml ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── README.md └── images │ ├── bars.png │ ├── histograms.png │ ├── lines.png │ └── scatters.png ├── shard.yml ├── spec ├── aquaplot_spec.cr ├── common │ ├── helpers_spec.cr │ └── options_spec.cr ├── plot │ └── base_spec.cr └── spec_helper.cr ├── src ├── aquaplot.cr ├── common │ ├── exceptions.cr │ ├── helpers.cr │ └── options.cr ├── plot │ ├── base.cr │ ├── plot2d.cr │ └── plot3d.cr ├── series │ ├── bar.cr │ ├── base.cr │ ├── function.cr │ ├── histogram.cr │ ├── line.cr │ └── scatter.cr └── util │ ├── font.cr │ ├── offset.cr │ └── title.cr └── static ├── function_3d.png ├── line_options.png └── trig_functions.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /bin/ 3 | /lib/ 4 | /.shards/ 5 | *.dwarf 6 | .DS_Store 7 | 8 | # Libraries don't need dependency lock 9 | # Dependencies will be locked in applications that use them 10 | /shard.lock 11 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | PreCommit: 2 | CustomScript: 3 | enabled: true 4 | command: crystal spec 5 | CustomScript: 6 | enabled: true 7 | command: crystal format 8 | TrailingWhitespace: 9 | enabled: true 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | install: 3 | - shards install 4 | script: 5 | - crystal spec 6 | - crystal bin/ameba.cr 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Crystal Data 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aquaplot 2 | 3 | [![Join the chat at https://gitter.im/crystal-data/aquaplot](https://badges.gitter.im/crystal-data/aquaplot.svg)](https://gitter.im/crystal-data/aquaplot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | AquaPlot is a data visualization library for [crystal-lang](https://crystal-lang.org/). It provides an easy to user interface to create visually 6 | appealing charts. This project is currently in extremely unstable and active development. Contributions are both welcomed and encouraged, 7 | to get this library to a stable and useful state. 8 | 9 | ## Installation 10 | 11 | 1. Add the dependency to your `shard.yml`: 12 | 13 | ```yaml 14 | dependencies: 15 | aquaplot: 16 | github: crystal-data/aquaplot 17 | ``` 18 | 19 | 2. Run `shards install` 20 | 21 | ## Usage 22 | 23 | Gnuplot is required. Please review your operating system's installation 24 | instructions to install the library. 25 | 26 | ```crystal 27 | require "aquaplot" 28 | ``` 29 | 30 | ### Function Charts 31 | 32 | ```crystal 33 | fns = ["sin(x)", "cos(x)", "tan(x)", "atan(x)", "asin(x)"].map do |fn| 34 | AquaPlot::Function.new fn 35 | end 36 | 37 | plt = AquaPlot::Plot.new fns 38 | plt.show 39 | plt.close 40 | ``` 41 | 42 | ![aquaplot chart](./static/trig_functions.png) 43 | 44 | ### 3D Function Charts 45 | 46 | ```crystal 47 | fns = ["x**2 + y**2", "x**2 - y**2", "x**2 * y**2", "x**2 / y**2"].map do |fn| 48 | AquaPlot::Function.new fn 49 | end 50 | 51 | plt = AquaPlot::Plot3D.new fns 52 | plt.set_key("left box") 53 | plt.show 54 | plt.close 55 | ``` 56 | 57 | ![aquaplot 3d chart](./static/function_3d.png) 58 | 59 | ### 2D Line Charts 60 | 61 | ```crystal 62 | lines = (1...5).map do |n| 63 | AquaPlot::Line.new (0...10).map { |el| Random.rand(50) }, title: "Line #{n}" 64 | end 65 | 66 | lines[0].show_points 67 | lines[1].set_linewidth 1 68 | 69 | plt = AquaPlot::Plot.new lines 70 | 71 | plt.set_title("Showing Some Options") 72 | 73 | plt.show 74 | plt.close 75 | ``` 76 | 77 | ![aquaplot 3d chart](./static/line_options.png) 78 | 79 | ## Development 80 | 81 | TODO: Write development instructions here 82 | 83 | ## Contributing 84 | 85 | 1. Fork it () 86 | 2. Create your feature branch (`git checkout -b my-new-feature`) 87 | 3. Commit your changes (`git commit -am 'Add some feature'`) 88 | 4. Push to the branch (`git push origin my-new-feature`) 89 | 5. Create a new Pull Request 90 | 91 | ## Contributors 92 | 93 | - [Chris Zimmerman](https://github.com/christopherzimmerman) - creator and maintainer 94 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # AquaPlot Examples 2 | 3 | ## Line Chart 4 | 5 | ```crystal 6 | lines = (0...3).map do |e| 7 | Line.new random_data, title: "Line #{e+1}" 8 | end 9 | 10 | plt = Plot.new lines 11 | plt.savefig("images/lines.png") 12 | plt.close 13 | ``` 14 | 15 | ![line chart](./images/lines.png) 16 | 17 | 18 | ## Scatter Chart 19 | 20 | ```crystal 21 | scatters = (0...3).map do |e| 22 | Scatter.new random_data, title: "Scatter #{e+1}" 23 | end 24 | 25 | plt = Plot.new scatters 26 | plt.savefig("images/scatters.png") 27 | plt.close 28 | ``` 29 | 30 | ![scatter chart](./images/scatters.png) 31 | 32 | 33 | ## Bar Chart 34 | 35 | ```crystal 36 | bars = (0...3).map do |e| 37 | Bar.new random_data, title: "Bar #{e+1}" 38 | end 39 | 40 | plt = Plot.new bars 41 | plt.savefig("images/bars.png") 42 | plt.close 43 | ``` 44 | 45 | ![bar chart](./images/bars.png) 46 | 47 | ## Histogram Chart 48 | 49 | ```crystal 50 | histograms = (0...3).map do |e| 51 | Histogram.new random_data, title: "Histogram #{e+1}" 52 | end 53 | 54 | plt = Plot.new histograms 55 | plt.savefig("images/histograms.png") 56 | plt.close 57 | ``` 58 | 59 | ![histogram chart](./images/histograms.png) 60 | -------------------------------------------------------------------------------- /examples/images/bars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crystal-data/aquaplot/a0373276c15996842826214b7f580c88a589347e/examples/images/bars.png -------------------------------------------------------------------------------- /examples/images/histograms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crystal-data/aquaplot/a0373276c15996842826214b7f580c88a589347e/examples/images/histograms.png -------------------------------------------------------------------------------- /examples/images/lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crystal-data/aquaplot/a0373276c15996842826214b7f580c88a589347e/examples/images/lines.png -------------------------------------------------------------------------------- /examples/images/scatters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crystal-data/aquaplot/a0373276c15996842826214b7f580c88a589347e/examples/images/scatters.png -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: aquaplot 2 | version: 0.1.0 3 | 4 | authors: 5 | - Chris Zimmerman 6 | 7 | crystal: 0.31.1 8 | 9 | license: MIT 10 | 11 | development_dependencies: 12 | ameba: 13 | github: crystal-ameba/ameba 14 | version: ~> 0.10.1 15 | -------------------------------------------------------------------------------- /spec/aquaplot_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | -------------------------------------------------------------------------------- /spec/common/helpers_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../src/common/helpers" 2 | require "../../src/series/base" 3 | 4 | TMP_PATH = "/tmp/test.dat" 5 | 6 | describe "Helpers" do 7 | it "Ensure _create_data_file creates a file" do 8 | _create_data_file [1, 2, 3], TMP_PATH 9 | File.exists?(TMP_PATH).should be_true 10 | File.delete(TMP_PATH) 11 | end 12 | 13 | it "Ensure _create_data_file contents are correct" do 14 | _create_data_file [1, 2, 3], TMP_PATH 15 | data = File.read(TMP_PATH) 16 | data.should eq("1\n2\n3") 17 | File.delete(TMP_PATH) 18 | end 19 | 20 | it "Ensure _create_data_file contents with header" do 21 | _create_data_file [1, 2, 3], TMP_PATH, "X" 22 | data = File.read(TMP_PATH) 23 | data.should eq("#X\n1\n2\n3") 24 | File.delete(TMP_PATH) 25 | end 26 | 27 | it "Ensure _create_data_file creates 2d file" do 28 | _create_data_file [[1, 2], [3, 4]], TMP_PATH 29 | File.exists?(TMP_PATH).should be_true 30 | File.delete(TMP_PATH) 31 | end 32 | 33 | it "Ensure _create_data_file finds bad headers" do 34 | expect_raises(AquaPlot::Exceptions::ShapeError) do 35 | _create_data_file [[1, 2], [3, 4]], TMP_PATH, ["X", "Y", "Z"] 36 | end 37 | end 38 | 39 | it "Ensure _create_data_file 2d contents are correct" do 40 | _create_data_file [[1, 2], [3, 4]], TMP_PATH 41 | data = File.read(TMP_PATH) 42 | data.should eq("1 2\n3 4\n") 43 | File.delete(TMP_PATH) 44 | end 45 | 46 | it "Ensure _create_data_file 2d contents are correct with header" do 47 | _create_data_file [[1, 2], [3, 4]], TMP_PATH, ["X", "Y"] 48 | data = File.read(TMP_PATH) 49 | data.should eq("#X Y\n1 2\n3 4\n") 50 | end 51 | 52 | it "Ensure _temporary_file finds missing directory" do 53 | expect_raises(AquaPlot::Exceptions::DirectoryNotFoundError) do 54 | _temporary_file("/aksfjsal") 55 | end 56 | end 57 | 58 | it "Ensure _option_to_string creates valid string option" do 59 | option = _option_to_string "foo", "bar" 60 | option.should eq("foo bar") 61 | end 62 | 63 | it "Ensure _option_to_string ignores empty string option" do 64 | option = _option_to_string "foo", "" 65 | option.should be_nil 66 | end 67 | 68 | it "Ensure _option_to_string provides quotes for string option" do 69 | option = _option_to_string "foo", "bar", quotes: true 70 | option.should eq("foo 'bar'") 71 | end 72 | 73 | it "Ensure _option_to_string creates valid numeric option" do 74 | option = _option_to_string "foo", 2 75 | option.should eq("foo 2") 76 | end 77 | 78 | it "Ensure _option_to_string ignores empty numeric option" do 79 | option = _option_to_string "foo", -1 80 | option.should be_nil 81 | end 82 | 83 | it "Ensure _option_to_string provides quotes for numeric option" do 84 | option = _option_to_string "foo", 2, quotes: true 85 | option.should eq("foo '2'") 86 | end 87 | 88 | it "Ensure _setting_to_string produces a valid string setting" do 89 | setting = _setting_to_string "foo", "bar" 90 | setting.should eq("set foo bar") 91 | end 92 | 93 | it "Ensure _setting_to_string ignores empty string setting" do 94 | setting = _setting_to_string "foo", "" 95 | setting.should be_nil 96 | end 97 | 98 | it "Ensure _setting_to_string provides quotes for string setting" do 99 | setting = _setting_to_string "foo", "bar", quotes: true 100 | setting.should eq("set foo 'bar'") 101 | end 102 | 103 | it "Ensure _toggle_to_string creates an option when true" do 104 | toggle = _toggle_to_string "foo", true 105 | toggle.should eq("set foo") 106 | end 107 | 108 | it "Ensure _toggle_to_string ignores option when false" do 109 | toggle = _toggle_to_string "foo", false 110 | toggle.should be_nil 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/common/options_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../src/common/options" 2 | require "../../src/common/exceptions" 3 | 4 | describe "Options" do 5 | it "Ensure offsets set_key mutates option" do 6 | key = "offsets" 7 | offset = AquaPlot::Util::Offset.new 0, 0, 0, 1 8 | offset.set_key(key) 9 | offset.key.should eq(key) 10 | end 11 | 12 | it "Ensure offsets raises KeyError without key" do 13 | offset = AquaPlot::Util::Offset.new 1, 1, 1, 1 14 | expect_raises(AquaPlot::Exceptions::KeyError) do 15 | offset.to_s 16 | end 17 | end 18 | 19 | it "Ensure offsets proper output" do 20 | offset = AquaPlot::Util::Offset.new 1, 1, 1, 1, "foo" 21 | offset.to_s.should eq("set foo 1, 1, 1, 1") 22 | end 23 | 24 | it "Ensure offsets empty output with empty offset" do 25 | offset = AquaPlot::Util::Offset.new 0, 0, 0, 0, "foo" 26 | offset.to_s.should be_nil 27 | end 28 | 29 | it "Ensure XY set_key mutates option" do 30 | key = "foobar" 31 | xy = AquaPlot::Util::XY.new 5, 5 32 | xy.set_key(key) 33 | xy.key.should eq(key) 34 | end 35 | 36 | it "Ensure XY raises KeyError without key" do 37 | xy = AquaPlot::Util::XY.new 1, 1 38 | expect_raises(AquaPlot::Exceptions::KeyError) do 39 | xy.to_s 40 | end 41 | end 42 | 43 | it "Ensure XY empty output with empty option" do 44 | xy = AquaPlot::Util::XY.new 0, 0, "foo" 45 | xy.to_s.should be_nil 46 | end 47 | 48 | it "Ensure XY proper string output" do 49 | xy = AquaPlot::Util::XY.new 5, 5, "foo" 50 | xy.to_s.should eq("set foo 5, 5") 51 | end 52 | 53 | it "Ensure XY raises KeyError without key in range output" do 54 | xy = AquaPlot::Util::XY.new 5, 5 55 | expect_raises(AquaPlot::Exceptions::KeyError) do 56 | xy.to_s 57 | end 58 | end 59 | 60 | it "Ensure XY empty range output with empty range option" do 61 | xy = AquaPlot::Util::XY.new 0, 0, "foo" 62 | xy.to_range.should be_nil 63 | end 64 | 65 | it "Ensure XY proper range output" do 66 | xy = AquaPlot::Util::XY.new 5, 5, "foo" 67 | xy.to_range.should eq("set foo [5:5]") 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/plot/base_spec.cr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crystal-data/aquaplot/a0373276c15996842826214b7f580c88a589347e/spec/plot/base_spec.cr -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/aquaplot" 3 | -------------------------------------------------------------------------------- /src/aquaplot.cr: -------------------------------------------------------------------------------- 1 | require "./common/*" 2 | require "./plot/*" 3 | require "./series/*" 4 | 5 | module AquaPlot 6 | VERSION = "0.1.0" 7 | end 8 | -------------------------------------------------------------------------------- /src/common/exceptions.cr: -------------------------------------------------------------------------------- 1 | class AquaPlot::Exceptions::TypeError < Exception 2 | end 3 | 4 | class AquaPlot::Exceptions::ShapeError < Exception 5 | end 6 | 7 | class AquaPlot::Exceptions::DirectoryNotFoundError < Exception 8 | end 9 | 10 | class AquaPlot::Exceptions::KeyError < Exception 11 | end 12 | -------------------------------------------------------------------------------- /src/common/helpers.cr: -------------------------------------------------------------------------------- 1 | require "./exceptions" 2 | require "uuid" 3 | 4 | # Creates a data file containing a one-dimensional 5 | # dataset and an optional header. 6 | # 7 | # `arr` : `Indexable(Number)` 8 | # - A one dimensional array of values 9 | # `filename` : `String` 10 | # - location to write data 11 | # `header` : `String | Nil` 12 | # - optional header for the data file 13 | # `sep` : `String` 14 | # - line separator for files 15 | def _create_data_file( 16 | arr : Indexable(Number), 17 | filename : String, 18 | header : String | Nil = nil, 19 | linesep = "\n" 20 | ) 21 | f = File.open(filename, "w") 22 | 23 | # writes header if needed 24 | if !header.nil? 25 | f << "##{header}#{linesep}" 26 | end 27 | 28 | # write delimited data 29 | f << arr.join(linesep) 30 | f.close 31 | end 32 | 33 | # Creates a data file containing a one-dimensional 34 | # dataset and an optional header. 35 | # 36 | # `arr` : `Indexable(Indexable(Number))` 37 | # - A one dimensional array of values 38 | # `filename` : `String` 39 | # - location to write data 40 | # `headers` : `Indexable(String)` 41 | # - optional header for the data file 42 | # `sep` : `String` 43 | # - line separator for files 44 | def _create_data_file( 45 | arr : Indexable(Indexable(Number)), 46 | filename : String, 47 | headers : Indexable(String) = [] of String, 48 | sep = " ", 49 | linesep = "\n" 50 | ) 51 | has_header = headers.size > 0 52 | 53 | if has_header 54 | hz = headers.size 55 | jagged = !arr.all? { |row| row.size == hz } 56 | if jagged 57 | raise AquaPlot::Exceptions::ShapeError.new("Shape mismatch when comparing headers to values") 58 | end 59 | end 60 | 61 | f = File.open(filename, "w") 62 | 63 | if has_header 64 | f << "##{headers.join(sep)}#{linesep}" 65 | end 66 | arr.each do |e| 67 | f << "#{e.join(sep)}#{linesep}" 68 | end 69 | f.close 70 | end 71 | 72 | # Returns information to create a temporary file. 73 | # Currently this only supports unix temporary folder, 74 | # but whenever Crystal gets ported to windows having 75 | # this implementation in a single place will be nice. 76 | # 77 | # `tmppath` : `String` 78 | # - temporary path to place temporary file 79 | def _temporary_file(tmppath = "/tmp/") 80 | if !Dir.exists?(tmppath) 81 | raise AquaPlot::Exceptions::DirectoryNotFoundError.new("The directory #{tmppath} does not exist") 82 | end 83 | 84 | fname = UUID.random.to_s 85 | "#{tmppath}#{fname}" 86 | end 87 | 88 | # Cleans up the temporary file left by a dataset 89 | # which generally should be run on the closing of 90 | # a plot. If these are not deleted, they will be 91 | # deleted whenever the operating system clears temporary 92 | # files 93 | # 94 | # `dataset` : `DataSet` 95 | # - the object to clean up 96 | def _cleanup_dataset(dataset) 97 | if dataset.cleanup 98 | if File.exists?(dataset.filename) 99 | File.delete(dataset.filename) 100 | end 101 | end 102 | end 103 | 104 | # Formats a string option into a valid configuration 105 | # option for gnuplot 106 | # 107 | # `key` : `String` 108 | # - Name of the option 109 | # `value` : `String` 110 | # - value of the option 111 | # `quotes` : `Bool` 112 | # - necessary if an option requires quotes 113 | def _option_to_string(key : String, value : String | Nil, quotes = false) 114 | if !"#{value}".empty? 115 | prop = quotes ? "'#{value}'" : value 116 | return "#{key} #{prop}" 117 | end 118 | end 119 | 120 | # Formats a string option into a valid configuration 121 | # option for gnuplot 122 | # 123 | # `key` : `String` 124 | # - Name of the option 125 | # `value` : `Number` 126 | # - value of the option 127 | # `quotes` : `Bool` 128 | # - necessary if an option requires quotes 129 | def _option_to_string(key : String, value : Number, quotes = false) 130 | if value > 0 131 | prop = quotes ? "'#{value}'" : value 132 | return "#{key} #{prop}" 133 | end 134 | end 135 | 136 | # Formats a string setting into a valid configuration 137 | # setting for gnuplot 138 | # 139 | # `key` : `String` 140 | # - Name of the option 141 | # `value` : `String` 142 | # - value of the option 143 | # `quotes` : `Bool` 144 | # - necessary if a setting requires quotes 145 | def _setting_to_string(key : String, value : String | Nil, quotes = false) 146 | if !"#{value}".empty? 147 | prop = quotes ? "'#{value}'" : value 148 | return "set #{key} #{prop}" 149 | end 150 | end 151 | 152 | # Formats a numerical setting into a valid configuration 153 | # setting for gnuplot 154 | # 155 | # `key` : `String` 156 | # - Name of the option 157 | # `value` : `Number` 158 | # - value of the option 159 | # `quotes` : `Bool` 160 | # - necessary if a setting requires quotes 161 | def _setting_to_string(key : String, value : Number, quotes = false) 162 | if value > 0 163 | prop = quotes ? "'#{value}'" : value 164 | return "set #{key} #{prop}" 165 | end 166 | end 167 | 168 | # Formats a boolean option into a valid configuration 169 | # option for gnuplot 170 | # 171 | # `key` : `String` 172 | # - Name of the option 173 | # `value` : `Bool` 174 | # - value of the option 175 | # `quotes` : `Bool` 176 | # - necessary if an option requires quotes 177 | def _toggle_to_string(key : String, value : Bool) 178 | if value 179 | return "set #{key}" 180 | end 181 | end 182 | 183 | def _toggle_option_to_string(key : String, value : Bool) 184 | if value 185 | return "#{key}" 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /src/common/options.cr: -------------------------------------------------------------------------------- 1 | require "./exceptions" 2 | 3 | class AquaPlot::Util::Offset 4 | property left : Int32 | Float64 5 | property right : Int32 | Float64 6 | property top : Int32 | Float64 7 | property bottom : Int32 | Float64 8 | property key : String 9 | 10 | def initialize( 11 | @left = 0, 12 | @right = 0, 13 | @top = 0, 14 | @bottom = 0, 15 | @key = "" 16 | ) 17 | end 18 | 19 | def set_key(@key) 20 | end 21 | 22 | def to_s 23 | if key.empty? 24 | raise AquaPlot::Exceptions::KeyError.new("Offsets was provided an empty key") 25 | end 26 | els = [@left, @right, @top, @bottom] 27 | nonzero = els.index { |e| e != 0 } 28 | if nonzero 29 | return "set #{key} #{els.join(", ")}" 30 | end 31 | end 32 | end 33 | 34 | class AquaPlot::Util::XY 35 | property x : Int32 | Float64 36 | property y : Int32 | Float64 37 | property key : String 38 | 39 | def initialize(@x = 0, @y = 0, @key = "") 40 | end 41 | 42 | def set_key(@key) 43 | end 44 | 45 | def to_s 46 | if key.empty? 47 | raise AquaPlot::Exceptions::KeyError.new("Option was provided an empty key") 48 | end 49 | if (@x != 0) & (@y != 0) 50 | return "set #{key} #{x}, #{y}" 51 | end 52 | end 53 | 54 | def to_range 55 | if key.empty? 56 | raise AquaPlot::Exceptions::KeyError.new("Range was provided an empty key") 57 | end 58 | if (@x != 0) & (@y != 0) 59 | return "set #{key} [#{x}:#{y}]" 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /src/plot/base.cr: -------------------------------------------------------------------------------- 1 | require "../common/helpers" 2 | require "../common/options" 3 | require "../series/base" 4 | require "../util/title" 5 | 6 | class AquaPlot::GlobalPlotOptions < AquaPlot::DataSet 7 | # 8 | # GETTERS 9 | # 10 | property border : String 11 | property boxwidth : String 12 | property grid : Bool 13 | property key : String 14 | property offsets : AquaPlot::Util::Offset 15 | property output : String 16 | property samples : Int32 17 | property scale : AquaPlot::Util::XY 18 | property terminal : String 19 | property tics : String 20 | property ticslevel : String 21 | property time : Bool 22 | property title : AquaPlot::Util::Title 23 | property xlabel : String 24 | property ylabel : String 25 | property xrange : AquaPlot::Util::XY 26 | property yrange : AquaPlot::Util::XY 27 | property ticks_hash : Hash(String, String) = Hash(String, String).new 28 | 29 | # 30 | # INITIALIZATION 31 | # 32 | def initialize( 33 | @border = "", 34 | @boxwidth = "0.6 relative", 35 | @data = "histograms", 36 | @grid = true, 37 | @key = "", 38 | @offsets = AquaPlot::Util::Offset.new, 39 | @output = "", 40 | @samples = 500, 41 | @scale = AquaPlot::Util::XY.new, 42 | @terminal = "qt", 43 | @tics = "", 44 | @ticslevel = "", 45 | @time = false, 46 | @title = AquaPlot::Util::Title.new, 47 | @xlabel = "", 48 | @xrange = AquaPlot::Util::XY.new, 49 | @ylabel = "", 50 | @yrange = AquaPlot::Util::XY.new 51 | ) 52 | super() 53 | @offsets.set_key("offsets") 54 | @scale.set_key("size") 55 | @yrange.set_key("yrange") 56 | @xrange.set_key("xrange") 57 | end 58 | 59 | # 60 | # GETTERS 61 | # 62 | def get_border 63 | _setting_to_string "border", @border 64 | end 65 | 66 | def get_boxwidth 67 | _setting_to_string "boxwidth", @boxwidth 68 | end 69 | 70 | def get_data 71 | _setting_to_string "style data", @data 72 | end 73 | 74 | def get_grid 75 | _toggle_to_string "grid", @grid 76 | end 77 | 78 | def get_key 79 | _setting_to_string "key", @key 80 | end 81 | 82 | def get_offsets 83 | @offsets.to_s 84 | end 85 | 86 | def get_output 87 | _setting_to_string "output", @output, quotes: true 88 | end 89 | 90 | def get_samples 91 | _setting_to_string "samples", @samples 92 | end 93 | 94 | def get_terminal 95 | _setting_to_string "terminal", @terminal 96 | end 97 | 98 | def get_tics 99 | _setting_to_string "tics", @tics 100 | end 101 | 102 | def get_axis_tics 103 | s = "" 104 | @ticks_hash.each do |k, v| 105 | s += "set #{k}tics #{v}" 106 | s += "\n" 107 | end 108 | s 109 | end 110 | 111 | def get_ticslevel 112 | _setting_to_string "ticslevel", @ticslevel 113 | end 114 | 115 | def get_time 116 | _toggle_to_string "time", @time 117 | end 118 | 119 | def get_title 120 | @title.to_s 121 | end 122 | 123 | def get_xlabel 124 | _setting_to_string "xlabel", @xlabel, quotes: true 125 | end 126 | 127 | def get_ylabel 128 | _setting_to_string "ylabel", @ylabel, quotes: true 129 | end 130 | 131 | def get_xrange 132 | @xrange.to_range 133 | end 134 | 135 | def get_yrange 136 | @yrange.to_range 137 | end 138 | 139 | # 140 | # SETTERS 141 | # 142 | def set_border(@border) 143 | end 144 | 145 | def set_boxwidth(@boxwidth) 146 | end 147 | 148 | def set_data(@data) 149 | end 150 | 151 | def set_grid(@grid) 152 | end 153 | 154 | def set_key(@key) 155 | end 156 | 157 | def set_offsets(left = 0, right = 0, top = 0, bottom = 0) 158 | @offsets = AquaPlot::Util::Offset.new left, right, top, bottom, key: "offsets" 159 | end 160 | 161 | def set_output(@output) 162 | end 163 | 164 | def set_samples(@samples) 165 | end 166 | 167 | def set_terminal(@terminal) 168 | end 169 | 170 | def set_tics(@tics) 171 | end 172 | 173 | def set_axis_ticks(axis, options) 174 | @ticks_hash[axis] = options 175 | end 176 | 177 | def set_ticslevel(@ticslevel) 178 | end 179 | 180 | def set_time(@time) 181 | end 182 | 183 | delegate set_title_text, set_title_font, set_title_offset, set_title_color, set_title_linetype, set_title_enhanced, to: @title 184 | 185 | def set_xlabel(@xlabel) 186 | end 187 | 188 | def set_ylabel(@ylabel) 189 | end 190 | 191 | def set_xrange(x, y) 192 | @xrange = AquaPlot::Util::XY.new x, y, key: "xrange" 193 | end 194 | 195 | def set_yrange(x, y) 196 | @yrange = AquaPlot::Util::XY.new x, y, key: "yrange" 197 | end 198 | 199 | def to_s 200 | [ 201 | get_border, 202 | get_boxwidth, 203 | get_data, 204 | get_grid, 205 | get_key, 206 | get_offsets, 207 | get_output, 208 | get_terminal, 209 | get_tics, 210 | get_axis_tics, 211 | get_ticslevel, 212 | get_time, 213 | get_title, 214 | get_xlabel, 215 | get_ylabel, 216 | get_xrange, 217 | get_yrange, 218 | ].reject do |el| 219 | "#{el}".empty? 220 | end.join("\n") 221 | end 222 | end 223 | 224 | class AquaPlot::PlotBase(T) < AquaPlot::GlobalPlotOptions 225 | property figures : Array(T) 226 | 227 | def initialize(@figures : Array(T), **options) 228 | super(**options) 229 | end 230 | 231 | def initialize(figure : T, **options) 232 | super(**options) 233 | @figures = [figure] 234 | end 235 | 236 | def close 237 | @figures.each do |fig| 238 | fig.finalize 239 | end 240 | end 241 | 242 | def savefig(fname : String, terminal : String = "png") 243 | save_term = @terminal 244 | self.set_terminal(terminal) 245 | self.set_output(fname) 246 | self.show 247 | self.set_output("") 248 | self.set_terminal(save_term) 249 | end 250 | 251 | def show 252 | File.write(@filename, self.to_s) 253 | gnuplot_dispatch() 254 | File.delete(@filename) 255 | end 256 | 257 | private def gnuplot_dispatch 258 | stdout = IO::Memory.new 259 | Process.run("gnuplot -p #{@filename}", shell: true, output: stdout) 260 | if stdout 261 | puts stdout 262 | end 263 | end 264 | end 265 | -------------------------------------------------------------------------------- /src/plot/plot2d.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | 3 | class AquaPlot::Plot(T) < AquaPlot::PlotBase(T) 4 | def to_s 5 | figs = @figures.map do |fig| 6 | fig.to_s 7 | end.join(", ") 8 | 9 | "#{super}\nplot#{figs}" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/plot/plot3d.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | 3 | struct AquaPlot::View 4 | getter rotation_x : Int32? = nil 5 | getter rotation_z : Int32? = nil 6 | getter scale : Float64 = 1.0 7 | 8 | def to_s 9 | "set view #{@rotation_x},#{@rotation_z},#{@scale}" 10 | end 11 | 12 | def initialize(@rotation_x : Int32? = nil, @rotation_z : Int32? = nil, @scale : Float64 = 1.0) 13 | end 14 | end 15 | 16 | class AquaPlot::Plot3D(T) < AquaPlot::PlotBase(T) 17 | property view : View = View.new 18 | 19 | def set_view(rx : Int32, ry : Int32, scale : Float64 = 1.0) 20 | @view = View.new(rx, ry, scale) 21 | end 22 | 23 | def to_s 24 | figs = @figures.map do |fig| 25 | fig.to_s 26 | end.join(", ") 27 | 28 | s = "#{@view.to_s}\n" 29 | 30 | s + "#{super}\nsplot#{figs}" 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/series/bar.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | require "../common/helpers" 3 | require "../common/exceptions" 4 | 5 | class AquaPlot::Bar < AquaPlot::XorXY 6 | # 7 | # INITIALIZATION 8 | # 9 | property style : String = "boxes" 10 | property fillstyle : String 11 | 12 | def initialize( 13 | x : Indexable(Number), 14 | @fillstyle = "solid 0.25", 15 | **options 16 | ) 17 | super(x, **options, linewidth: 1) 18 | end 19 | 20 | def initialize( 21 | x : Indexable(Number), 22 | y : Indexable(Number), 23 | @fillstyle = "solid 0.25", 24 | **options 25 | ) 26 | super(x, y, **options, linewidth: 1) 27 | end 28 | 29 | # 30 | # GETTERS 31 | # 32 | def get_fillstyle 33 | _option_to_string "fs", @fillstyle 34 | end 35 | 36 | # 37 | # SETTERS 38 | # 39 | def set_fillstyle(@fillstyle) 40 | end 41 | 42 | # 43 | # SERIALIZER 44 | # 45 | def to_s 46 | [ 47 | super, 48 | get_fillstyle, 49 | ].join(" ") 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/series/base.cr: -------------------------------------------------------------------------------- 1 | require "../common/helpers" 2 | require "../util/title" 3 | 4 | abstract class AquaPlot::DataSet 5 | # 6 | # PROPERTIES 7 | # 8 | property filename : String 9 | property cleanup : Bool = true 10 | property title : AquaPlot::Util::Title = AquaPlot::Util::Title.new 11 | 12 | # 13 | # INITIALIZATION 14 | # 15 | def initialize(title = nil) 16 | @filename = _temporary_file 17 | @title.set_title_text(title) 18 | end 19 | 20 | # 21 | # CLEANUP 22 | # 23 | def finalize 24 | _cleanup_dataset(self) 25 | end 26 | 27 | # 28 | # GETTERS 29 | # 30 | def get_title 31 | @title.to_option 32 | end 33 | 34 | def get_filename 35 | _option_to_string "", @filename, quotes: true 36 | end 37 | 38 | # 39 | # SETTERS 40 | # 41 | def set_title(text) 42 | @title.set_title_text(text) 43 | end 44 | end 45 | 46 | abstract class AquaPlot::SeriesOptions < AquaPlot::DataSet 47 | # 48 | # GETTERS 49 | # 50 | property linecolor : String 51 | property linewidth : Int32 52 | property pointtype : Int32 53 | property pointsize : Int32 54 | property style : String = "lines" 55 | 56 | # 57 | # INITIALIZATION 58 | # 59 | def initialize( 60 | @linecolor : String = "", 61 | @linewidth : Int32 = 2, 62 | @pointtype : Int32 = 7, 63 | @pointsize : Int32 = 2, 64 | **options 65 | ) 66 | super(**options) 67 | end 68 | 69 | # 70 | # GETTERS 71 | # 72 | def get_linecolor 73 | _option_to_string "lc", @linecolor, quotes: true 74 | end 75 | 76 | def get_linewidth 77 | _option_to_string "lw", @linewidth 78 | end 79 | 80 | def get_pointtype 81 | _option_to_string "pt", @pointtype 82 | end 83 | 84 | def get_pointsize 85 | _option_to_string "ps", @pointsize 86 | end 87 | 88 | def get_style 89 | _option_to_string "with", @style 90 | end 91 | 92 | # 93 | # SETTERS 94 | # 95 | def set_linecolor(@linecolor) 96 | end 97 | 98 | def set_linewidth(@linewidth) 99 | end 100 | 101 | # 102 | # SERIALIZER 103 | # 104 | def to_s 105 | [ 106 | get_filename, 107 | get_style, 108 | get_linecolor, 109 | get_linewidth, 110 | get_pointtype, 111 | get_pointsize, 112 | get_title, 113 | ].join(" ") 114 | end 115 | end 116 | 117 | abstract class AquaPlot::XorXY < AquaPlot::SeriesOptions 118 | def initialize(x : Indexable(Number), **options) 119 | super(**options) 120 | _create_data_file(x, @filename) 121 | end 122 | 123 | def initialize(x : Indexable(Number), y : Indexable(Number), **options) 124 | super(**options) 125 | _create_data_file([x, y].transpose, @filename) 126 | end 127 | end 128 | 129 | abstract class AquaPlot::NColumns < AquaPlot::SeriesOptions 130 | def initialize(*args, labels : Indexable(Number | String) | Nil = nil, **options) 131 | super(**options) 132 | puts args.to_a.transpose 133 | end 134 | end 135 | 136 | abstract class AquaPlot::XYZ < AquaPlot::SeriesOptions 137 | 138 | def initialize( 139 | x : Indexable(Number), 140 | y : Indexable(Number), 141 | z : Indexable(Number), 142 | **options 143 | ) 144 | super(**options) 145 | _create_data_file([x, y, z].transpose, @filename) 146 | end 147 | 148 | def self.from_points(*points : Tuple(U, V, W)) forall U, V, W 149 | from_points(points.to_a) 150 | end 151 | 152 | def self.from_points(points : Array(Tuple(U, V, W))) forall U, V, W 153 | x = [] of U 154 | y = [] of V 155 | z = [] of W 156 | points.each do |i, j, k| 157 | x << i 158 | y << j 159 | z << k 160 | end 161 | new(x, y, z) 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /src/series/function.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | require "../common/helpers" 3 | 4 | class AquaPlot::Function < AquaPlot::SeriesOptions 5 | # 6 | # PROPERTIES 7 | # 8 | property function : String 9 | 10 | # 11 | # INITIALIZATION 12 | # 13 | def initialize(@function, **options) 14 | super(**options) 15 | end 16 | 17 | # 18 | # GETTERS 19 | # 20 | def get_function 21 | _option_to_string "", @function 22 | end 23 | 24 | # 25 | # SERIALIZER 26 | # 27 | def to_s 28 | [ 29 | get_function, 30 | get_style, 31 | get_linecolor, 32 | get_linewidth, 33 | get_pointtype, 34 | get_pointsize, 35 | get_title, 36 | ].join(" ") 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /src/series/histogram.cr: -------------------------------------------------------------------------------- 1 | require "./bar" 2 | 3 | class AquaPlot::Histogram < AquaPlot::Bar 4 | property style : String = "histograms" 5 | end 6 | -------------------------------------------------------------------------------- /src/series/line.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | require "../common/helpers" 3 | require "../common/exceptions" 4 | 5 | class AquaPlot::Line < AquaPlot::XorXY 6 | def show_points 7 | @style = "linespoints" 8 | end 9 | 10 | def hide_points 11 | @style = "lines" 12 | end 13 | end 14 | 15 | class AquaPlot::Line3D < AquaPlot::XYZ 16 | def show_points 17 | @style = "linespoints" 18 | end 19 | 20 | def hide_points 21 | @style = "lines" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/series/scatter.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | require "../common/helpers" 3 | require "../common/exceptions" 4 | 5 | class AquaPlot::Scatter < AquaPlot::XorXY 6 | # 7 | # INITIALIZATION 8 | # 9 | property style : String = "points" 10 | 11 | # 12 | # MUTATORS 13 | # 14 | def show_lines 15 | @style = "linespoints" 16 | end 17 | 18 | def hide_lines 19 | @style = "points" 20 | end 21 | end 22 | 23 | class AquaPlot::Scatter3D < AquaPlot::XYZ 24 | # 25 | # INITIALIZATION 26 | # 27 | property style : String = "points" 28 | 29 | # 30 | # MUTATORS 31 | # 32 | def show_lines 33 | @style = "linespoints" 34 | end 35 | 36 | def hide_lines 37 | @style = "points" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /src/util/font.cr: -------------------------------------------------------------------------------- 1 | require "../common/helpers" 2 | 3 | class AquaPlot::Util::Font 4 | private property family : String | Nil 5 | private property size : Int32 | Nil 6 | 7 | def initialize(@family = nil, @size = nil) 8 | end 9 | 10 | def update(@family, @size = nil) 11 | end 12 | 13 | def to_s 14 | size_s = "#{@size}".empty? ? "" : ",#{@size}" 15 | if !@family.nil? 16 | return "#{@family}#{size_s}" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /src/util/offset.cr: -------------------------------------------------------------------------------- 1 | require "../common/helpers" 2 | 3 | class AquaPlot::Util::OffsetXY 4 | private property x : Int32 | Nil 5 | private property y : Int32 | Nil 6 | 7 | def initialize(@x = nil, @y = nil) 8 | end 9 | 10 | def update(@x, @y) 11 | end 12 | 13 | def to_s 14 | if !@y.nil? & !@x.nil? 15 | return "#{@x},#{@y}" 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/util/title.cr: -------------------------------------------------------------------------------- 1 | require "./offset" 2 | require "./font" 3 | 4 | class AquaPlot::Util::Title 5 | private property text : String | Nil 6 | private property offset : AquaPlot::Util::OffsetXY 7 | private property font : AquaPlot::Util::Font 8 | private property textcolor : String | Nil 9 | private property linetype : Int32 | Nil 10 | private property enhanced : Bool 11 | 12 | def initialize( 13 | @text = nil, 14 | @offset = AquaPlot::Util::OffsetXY.new, 15 | @font = AquaPlot::Util::Font.new, 16 | @textcolor = nil, 17 | @linetype = nil, 18 | @enhanced = false 19 | ) 20 | end 21 | 22 | def set_title_text(@text) 23 | end 24 | 25 | def get_title_text 26 | _option_to_string "title", @text, quotes: true 27 | end 28 | 29 | def set_title_offset(x, y) 30 | self.offset.update(x, y) 31 | end 32 | 33 | def get_title_offset 34 | _option_to_string "offset", @offset.to_s 35 | end 36 | 37 | def set_title_font(family, size = nil) 38 | self.font.update(family, size) 39 | end 40 | 41 | def get_title_font 42 | _option_to_string "font", @font.to_s, quotes: true 43 | end 44 | 45 | def set_title_color(@textcolor) 46 | end 47 | 48 | def get_title_color 49 | _option_to_string "tc", @textcolor, quotes: true 50 | end 51 | 52 | def set_title_linetype(@linetype) 53 | end 54 | 55 | def get_title_linetype 56 | _option_to_string "lt", @linetype 57 | end 58 | 59 | def set_title_enhanced(@enhanced) 60 | end 61 | 62 | def get_title_enhanced 63 | _toggle_option_to_string "enhanced", @enhanced 64 | end 65 | 66 | def to_option 67 | if !"#{get_title_text}".empty? 68 | [ 69 | get_title_text, 70 | get_title_offset, 71 | get_title_font, 72 | get_title_color, 73 | get_title_linetype, 74 | get_title_enhanced, 75 | ].reject do |opt| 76 | "#{opt}".empty? 77 | end.join(" ") 78 | end 79 | end 80 | 81 | def to_s 82 | _setting_to_string "", self.to_option 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /static/function_3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crystal-data/aquaplot/a0373276c15996842826214b7f580c88a589347e/static/function_3d.png -------------------------------------------------------------------------------- /static/line_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crystal-data/aquaplot/a0373276c15996842826214b7f580c88a589347e/static/line_options.png -------------------------------------------------------------------------------- /static/trig_functions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crystal-data/aquaplot/a0373276c15996842826214b7f580c88a589347e/static/trig_functions.png --------------------------------------------------------------------------------