gnuplotrb 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ module GnuplotRB
2
+ ##
3
+ # === Overview
4
+ # Multiplot allows to place several plots on one layout.
5
+ class Multiplot
6
+ include Plottable
7
+ ##
8
+ # Array of plots contained by this object.
9
+ attr_reader :plots
10
+
11
+ ##
12
+ # ====== Arguments
13
+ # * *plots* are Plot or Splot objects which should be placed
14
+ # on this multiplot
15
+ # * *options* will be considered as 'settable' options of gnuplot
16
+ # ('set xrange [1:10]' for { xrange: 1..10 },
17
+ # "set title 'plot'" for { title: 'plot' } etc) just as in Plot.
18
+ # Special options of Multiplot are :layout and :title.
19
+ def initialize(*plots, **options)
20
+ @plots = plots[0].is_a?(Hamster::Vector) ? plots[0] : Hamster::Vector.new(plots)
21
+ @options = Hamster.hash(options)
22
+ @terminal = Terminal.new
23
+ OptionHandling.validate_terminal_options(@options)
24
+ end
25
+
26
+ ##
27
+ # Create new Multiplot object with the same set of plots and
28
+ # given options.
29
+ def new_with_options(options)
30
+ self.class.new(@plots, options)
31
+ end
32
+
33
+ ##
34
+ # Check if given options corresponds to multiplot.
35
+ # Multiplot special options are :title and :layout.
36
+ def mp_option?(key)
37
+ %w(title layout).include?(key.to_s)
38
+ end
39
+
40
+ ##
41
+ # ====== Overview
42
+ # This outputs all the plots to term (if given) or to this
43
+ # Multiplot's own terminal.
44
+ # ====== Arguments
45
+ # * *term* - Terminal to plot to
46
+ # * *options* - will be considered as 'settable' options of gnuplot
47
+ # ('set xrange [1:10]', 'set title 'plot'' etc)
48
+ # Options passed here have priority over already existing.
49
+ # Inner options of Plots have the highest priority (except
50
+ # :term and :output which are ignored).
51
+ def plot(term = nil, **options)
52
+ all_options = @options.merge(options)
53
+ mp_options, plot_options = all_options.partition { |key, _value| mp_option?(key) }
54
+ plot_options = plot_options.merge(multiplot: mp_options.to_h)
55
+ terminal = term || (plot_options[:output] ? Terminal.new : @terminal)
56
+ terminal.set(plot_options)
57
+ @plots.each { |graph| graph.plot(terminal, multiplot_part: true) }
58
+ terminal.unset(plot_options.keys)
59
+ if plot_options[:output]
60
+ # guaranteed wait for plotting to finish
61
+ terminal.close unless term
62
+ # not guaranteed wait for plotting to finish
63
+ # work bad with terminals like svg and html
64
+ sleep 0.01 until File.size?(plot_options[:output])
65
+ end
66
+ self
67
+ end
68
+
69
+ ##
70
+ # ====== Overview
71
+ # Create new Multiplot object where plot (Plot or Splot object)
72
+ # at *position* will
73
+ # be replaced with the new one created from it by updating.
74
+ # To update a plot you can pass some options for it or a
75
+ # block, that should take existing plot (with new options if
76
+ # you gave them) and return a plot too.
77
+ # ====== Arguments
78
+ # * *position* - position of plot which you need to update
79
+ # (by default first plot is updated)
80
+ # * *options* - options to update plot with
81
+ # * method also may take a block which returns a plot
82
+ # ====== Example
83
+ # mp = Multiplot.new(Plot.new('sin(x)'), Plot.new('cos(x)'), layout: [2,1])
84
+ # updated_mp = mp.update_plot(title: 'Sin(x) and Exp(x)') { |sinx| sinx.add_dataset('exp(x)') }
85
+ def update_plot(position = 0, **options)
86
+ return self unless block_given? if options.empty?
87
+ replacement = @plots[position].options(options)
88
+ replacement = yield(replacement) if block_given?
89
+ replace_plot(position, replacement)
90
+ end
91
+
92
+ alias_method :update, :update_plot
93
+
94
+ ##
95
+ # ====== Overview
96
+ # Create new Multiplot object where plot (Plot or Splot object)
97
+ # at *position* will be replaced with the given one.
98
+ # ====== Arguments
99
+ # * *position* - position of plot which you need to update
100
+ # (by default first plot is updated)
101
+ # * *plot* - replacement for existing plot
102
+ # ====== Example
103
+ # mp = Multiplot.new(Plot.new('sin(x)'), Plot.new('cos(x)'), layout: [2,1])
104
+ # mp_with_replaced_plot = mp.replace_plot(Plot.new('exp(x)', title: 'exp instead of sin'))
105
+ def replace_plot(position = 0, plot)
106
+ self.class.new(@plots.set(position, plot), @options)
107
+ end
108
+
109
+ alias_method :replace, :replace_plot
110
+
111
+ ##
112
+ # ====== Overview
113
+ # Create new Multiplot with given *plot* added before plot at given *position*.
114
+ # (by default it adds plot at the front).
115
+ # ====== Arguments
116
+ # * *position* - position before which you want to add a plot
117
+ # * *plot* - plot you want to add
118
+ # ====== Example
119
+ # mp = Multiplot.new(Plot.new('sin(x)'), Plot.new('cos(x)'), layout: [2,1])
120
+ # enlarged_mp = mp.add_plot(Plot.new('exp(x)')).layout([3,1])
121
+ def add_plot(position = 0, plot)
122
+ self.class.new(@plots.insert(position, plot), @options)
123
+ end
124
+
125
+ alias_method :<<, :add_plot
126
+ alias_method :add, :add_plot
127
+
128
+ ##
129
+ # ====== Overview
130
+ # Create new Multiplot without plot at given position
131
+ # (by default last plot is removed).
132
+ # ====== Arguments
133
+ # * *position* - position of plot you want to remove
134
+ # ====== Example
135
+ # mp = Multiplot.new(Plot.new('sin(x)'), Plot.new('cos(x)'), layout: [2,1])
136
+ # mp_with_only_cos = mp.remove_plot(0)
137
+ def remove_plot(position = -1)
138
+ self.class.new(@plots.delete_at(position), @options)
139
+ end
140
+
141
+ alias_method :remove, :remove_plot
142
+
143
+ ##
144
+ # ====== Overview
145
+ # Equal to #plots[*args]
146
+ def [](*args)
147
+ @plots[*args]
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,152 @@
1
+ module GnuplotRB
2
+ ##
3
+ # === Overview
4
+ # Plot correspond to simple 2D visualisation
5
+ class Plot
6
+ include Plottable
7
+ ##
8
+ # Array of datasets which are plotted by this object.
9
+ attr_reader :datasets
10
+ ##
11
+ # ====== Arguments
12
+ # * *datasets* are either instances of Dataset class or
13
+ # [data, **dataset_options] arrays from which Dataset may be created
14
+ # * *options* will be considered as 'settable' options of gnuplot
15
+ # ('set xrange [1:10]' for { xrange: 1..10 },
16
+ # "set title 'plot'" for { title: 'plot' } etc)
17
+ def initialize(*datasets, **options)
18
+ @datasets = if datasets[0].is_a? Hamster::Vector
19
+ datasets[0]
20
+ else
21
+ Hamster::Vector.new(datasets).map { |ds| dataset_from_any(ds) }
22
+ end
23
+ @options = Hamster.hash(options)
24
+ @already_plotted = false
25
+ @cmd = 'plot '
26
+ @terminal = Terminal.new
27
+ OptionHandling.validate_terminal_options(@options)
28
+ yield(self) if block_given?
29
+ end
30
+
31
+ ##
32
+ # For inner use!
33
+ # Creates new Plot with existing data and given options.
34
+ def new_with_options(options)
35
+ self.class.new(@datasets, options)
36
+ end
37
+
38
+ ##
39
+ # ====== Overview
40
+ # This outputs plot to term (if given) or to this plot's own terminal.
41
+ # ====== Arguments
42
+ # * *term* - Terminal to plot to
43
+ # * *multiplot_part* - part of a multiplot. Option for inner usage
44
+ # * *options* - will be considered as 'settable' options of gnuplot
45
+ # ('set xrange [1:10]', 'set title 'plot'' etc)
46
+ # Options passed here have priority over already existing.
47
+ def plot(term = nil, multiplot_part: false, **options)
48
+ opts = @options.merge(options)
49
+ opts = opts.reject { |key, _value| [:term, :output].include?(key) } if multiplot_part
50
+ terminal = term || (opts[:output] ? Terminal.new : @terminal)
51
+ full_command = @cmd + @datasets.map { |dataset| dataset.to_s(terminal) }.join(' , ')
52
+ terminal.set(opts)
53
+ .puts(full_command)
54
+ .unset(opts.keys)
55
+ if opts[:output]
56
+ # guaranteed wait for plotting to finish
57
+ terminal.close unless term
58
+ # not guaranteed wait for plotting to finish
59
+ # work bad with terminals like svg and html
60
+ sleep 0.01 until File.size?(opts[:output])
61
+ end
62
+ @already_plotted = true
63
+ self
64
+ end
65
+
66
+ alias_method :replot, :plot
67
+
68
+ ##
69
+ # ====== Overview
70
+ # Create new Plot object where dataset at *position* will
71
+ # be replaced with the new one created from it by updating.
72
+ # ====== Arguments
73
+ # * *position* - position of dataset which you need to update
74
+ # (by default first dataset is updated)
75
+ # * *data* - data to update dataset with
76
+ # * *options* - options to update dataset with
77
+ # ====== Example
78
+ # updated_plot = plot.update_dataset(data: [x1,y1], title: 'After update')
79
+ def update_dataset(position = 0, data: nil, **options)
80
+ old_ds = @datasets[position]
81
+ new_ds = old_ds.update(data, options)
82
+ new_ds.equal?(old_ds) ? self : replace_dataset(position, new_ds)
83
+ end
84
+
85
+ ##
86
+ # ====== Overview
87
+ # Create new Plot object where dataset at *position* will
88
+ # be replaced with the given one.
89
+ # ====== Arguments
90
+ # * *position* - position of dataset which you need to update
91
+ # (by default first dataset is replaced)
92
+ # * *dataset* - dataset to replace the old one. You can also
93
+ # give here [data, **dataset_options] array from
94
+ # which Dataset may be created.
95
+ # ====== Example
96
+ # sinx = Plot.new('sin(x)')
97
+ # cosx = sinx.replace_dataset(['cos(x)'])
98
+ def replace_dataset(position = 0, dataset)
99
+ self.class.new(@datasets.set(position, dataset_from_any(dataset)), @options)
100
+ end
101
+
102
+ ##
103
+ # ====== Overview
104
+ # Create new Plot object where given dataset will
105
+ # be appended to dataset list.
106
+ # ====== Arguments
107
+ # * *dataset* - dataset to add
108
+ # ====== Example
109
+ # sinx = Plot.new('sin(x)')
110
+ # sinx_and_cosx = sinx.add(['cos(x)'])
111
+ #
112
+ # cosx_and_sinx = sinx << ['cos(x)']
113
+ def add_dataset(dataset)
114
+ self.class.new(@datasets.add(dataset_from_any(dataset)), @options)
115
+ end
116
+
117
+ alias_method :<<, :add_dataset
118
+
119
+ ##
120
+ # ====== Overview
121
+ # Create new Plot object where dataset at given position
122
+ # will be removed from dataset list.
123
+ # ====== Arguments
124
+ # * *position* - position of dataset that should be
125
+ # removed (by default last dataset is removed)
126
+ # ====== Example
127
+ # sinx_and_cosx = Plot.new('sin(x)', 'cos(x)')
128
+ # sinx = sinx_and_cosx.remove_dataset
129
+ # cosx = sinx_and_cosx.remove_dataset(0)
130
+ def remove_dataset(position = -1)
131
+ self.class.new(@datasets.delete_at(position), @options)
132
+ end
133
+
134
+ ##
135
+ # ====== Overview
136
+ # The same as Plot#datasets[args]
137
+ def [](*args)
138
+ @datasets[*args]
139
+ end
140
+
141
+ ##
142
+ # Method for inner use.
143
+ # Check if given args is a dataset and returns it. Creates
144
+ # new dataset from given args otherwise.
145
+ def dataset_from_any(source)
146
+ source.is_a?(Dataset) ? source.clone : Dataset.new(*source)
147
+ end
148
+
149
+ private :dataset_from_any,
150
+ :new_with_options
151
+ end
152
+ end
@@ -0,0 +1,18 @@
1
+ module GnuplotRB
2
+ ##
3
+ # === Overview
4
+ # Splot class correspond to simple 3D visualisation
5
+ class Splot < Plot
6
+ ##
7
+ # ==== Arguments
8
+ # * *datasets* are either instances of Dataset class or
9
+ # [data, **dataset_options] arrays
10
+ # * *options* will be considered as 'settable' options of gnuplot
11
+ # ('set xrange [1:10]' for { xrange: 1..10 }, "set title 'plot'"
12
+ # for { title: 'plot' } etc)
13
+ def initialize(*datasets, **options)
14
+ super
15
+ @cmd = 'splot '
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,70 @@
1
+ module GnuplotRB
2
+ ##
3
+ # === Overview
4
+ # This class corresponds to points we want to plot. It may be
5
+ # stored in temporary file (to allow fast update) or inside
6
+ # "$DATA << EOD ... EOD" construction. Datablock stores data passed
7
+ # to constructor and keeps datablock name or path to file where it is stored.
8
+ class Datablock
9
+ ##
10
+ # ====== Parameters
11
+ # * *data* - sequence of anything with +#to_gnuplot_points+ method.
12
+ # * *stored_in_file* true here will force this datablock to store its data
13
+ # in temporary file.
14
+ def initialize(data, stored_in_file = false)
15
+ @stored_in_file = stored_in_file
16
+ data_str = data.to_gnuplot_points
17
+ if @stored_in_file
18
+ @file_name = Dir::Tmpname.make_tmpname('tmp_data', 0)
19
+ File.write(@file_name, data_str)
20
+ name = File.join(Dir.pwd, @file_name)
21
+ ObjectSpace.define_finalizer(self, proc { File.delete(name) })
22
+ else
23
+ @data = data_str
24
+ end
25
+ end
26
+
27
+ ##
28
+ # ====== Overview
29
+ # Instantiate one more Datablock with updated data
30
+ # if data stored in here-doc. Append update to file
31
+ # if data stored there.
32
+ # ====== Parameters
33
+ # * *data* - anything with +#to_gnuplot_points+ method
34
+ def update(data)
35
+ data_str = data.to_gnuplot_points
36
+ if @stored_in_file
37
+ File.open(@file_name, 'a') { |f| f.puts "\n#{data_str}" }
38
+ self
39
+ else
40
+ Datablock.new("#{@data}\n#{data_str}", false)
41
+ end
42
+ end
43
+
44
+ ##
45
+ # ====== Overview
46
+ # Returns quoted filename if datablock stored in file or outputs
47
+ # datablock to gnuplot and returns its name otherwise.
48
+ # * *gnuplot_term* should be given if datablock not stored in file.
49
+ def name(gnuplot_term = nil)
50
+ if @stored_in_file
51
+ "'#{@file_name}'"
52
+ else
53
+ fail(ArgumentError, 'No terminal given to output datablock') unless gnuplot_term
54
+ gnuplot_term.store_datablock(@data)
55
+ end
56
+ end
57
+
58
+ alias_method :to_s, :name
59
+
60
+ ##
61
+ # ====== Overview
62
+ # Overridden #clone. Since datablock which store data
63
+ # in temporary files should not be cloned (otherwise it will cause
64
+ # double attempt to delete file), this #clone returns self for such
65
+ # cases. For other cases it just calls default #clone.
66
+ def clone
67
+ @stored_in_file ? self : super
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,233 @@
1
+ module GnuplotRB
2
+ ##
3
+ # === Overview
4
+ # Dataset keeps control of Datablock or String (some math functions like
5
+ # this 'x*sin(x)' or filename) and options related to original dataset
6
+ # in gnuplot (with, title, using etc).
7
+ class Dataset
8
+ include OptionHandling
9
+ ##
10
+ # Data represented by this dataset
11
+ attr_reader :data
12
+
13
+ ##
14
+ # Order is significant for some options
15
+ OPTION_ORDER = %w(index using axes title)
16
+
17
+ ##
18
+ # Hash of init handlers for data given in
19
+ # different containers.
20
+ INIT_HANDLERS = Hash.new(:init_default).merge(
21
+ String => :init_string,
22
+ Datablock => :init_dblock
23
+ )
24
+ INIT_HANDLERS.merge!(
25
+ Daru::DataFrame => :init_daru_frame,
26
+ Daru::Vector => :init_daru_vector
27
+ ) if defined? Daru
28
+
29
+ ##
30
+ # ====== Overview
31
+ # Creates new dataset out of given string with
32
+ # math function or filename. If *data* isn't a string
33
+ # it will create datablock to store data.
34
+ # ====== Parameters
35
+ # * *data* - String, Datablock or something acceptable by
36
+ # Datablock.new as data (e.g. [x,y] where x and y are arrays)
37
+ # * *options* - hash of options specific for gnuplot
38
+ # dataset, and some special options ('file: true' will
39
+ # make data to be stored inside temporary file).
40
+ # ====== Examples
41
+ # Math function:
42
+ # Dataset.new('x*sin(x)', with: 'lines', lw: 4)
43
+ # File with points:
44
+ # Dataset.new('points.data', with: 'lines', title: 'Points from file')
45
+ # Some data (creates datablock stored in memory):
46
+ # x = (0..5000).to_a
47
+ # y = x.map {|xx| xx*xx }
48
+ # points = [x, y]
49
+ # Dataset.new(points, with: 'points', title: 'Points')
50
+ # The same data but datablock stores it in temp file:
51
+ # Dataset.new(points, with: 'points', title: 'Points', file: true)
52
+ def initialize(data, **options)
53
+ self.send(INIT_HANDLERS[data.class], data, options)
54
+ end
55
+
56
+ ##
57
+ # Method for inner use.
58
+ def init_string(data, options)
59
+ @type, @data= if File.exist?(data)
60
+ [:datafile, "'#{data}'"]
61
+ else
62
+ [:math_function, data.clone]
63
+ end
64
+ @options = Hamster.hash(options)
65
+ end
66
+
67
+ ##
68
+ # Method for inner use.
69
+ def init_dblock(data, options)
70
+ @type = :datablock
71
+ @data = data.clone
72
+ @options = Hamster.hash(options)
73
+ end
74
+
75
+ ##
76
+ # Method for inner use.
77
+ # TODO: make it better
78
+ def init_daru_frame(data, options)
79
+ options[:title] ||= data.name
80
+ if options[:using]
81
+ options[:using] = " #{options[:using]} "
82
+ data.vectors.to_a.each_with_index do |daru_index, array_index|
83
+ options[:using].gsub!(/([\:\( ])#{daru_index}([\:\) ])/) { "#{$1}#{array_index + 2}#{$2}"}
84
+ end
85
+ options[:using].gsub!(/index/) { 1 }
86
+ options[:using].strip!
87
+ else
88
+ new_opt = (2...(2 + data.vectors.size)).to_a.join(':')
89
+ options[:using] = "#{new_opt}:xtic(1)"
90
+ end
91
+ init_default(data, options)
92
+ end
93
+
94
+ ##
95
+ # Method for inner use.
96
+ def init_daru_vector(data, options)
97
+ options[:using] ||= '2:xtic(1)'
98
+ options[:title] ||= data.name
99
+ init_default(data, options)
100
+ end
101
+
102
+ ##
103
+ # Method for inner use.
104
+ def init_default(data, file: false, **options)
105
+ @type = :datablock
106
+ @data = Datablock.new(data, file)
107
+ @options = Hamster.hash(options)
108
+ end
109
+
110
+ ##
111
+ # ====== Overview
112
+ # Converts Dataset to string containing gnuplot dataset.
113
+ # ====== Parameters
114
+ # * *terminal* - must be given if data given as Datablock and
115
+ # it does not use temp file so data should be piped out
116
+ # to gnuplot via terminal before use.
117
+ # ====== Examples
118
+ # Dataset.new('points.data', with: 'lines', title: 'Points from file').to_s
119
+ # #=> "'points.data' with lines title 'Points form file'"
120
+ # Dataset.new(points, with: 'points', title: 'Points').to_s
121
+ # #=> "$DATA1 with points title 'Points'"
122
+ def to_s(terminal = nil)
123
+ "#{@type == :datablock ? @data.name(terminal) : @data } #{options_to_string}"
124
+ end
125
+
126
+ ##
127
+ # ====== Overview
128
+ # Create string from own options
129
+ def options_to_string
130
+ options.sort_by { |key, _| OPTION_ORDER.find_index(key.to_s) || 999 }
131
+ .map { |key, value| OptionHandling.option_to_string(key, value) }
132
+ .join(' ')
133
+ end
134
+
135
+ ##
136
+ # ====== Overview
137
+ # Creates new dataset with updated data (given
138
+ # data is appended to existing) and merged options.
139
+ # Data is updated only if Dataset stores it in Datablock.
140
+ # Method does nothing if no options given and data isn't stored
141
+ # in in-memory Datablock.
142
+ # ====== Parameters
143
+ # * *data* - data to append to existing
144
+ # * *options* - hash to merge with existing options
145
+ # ====== Examples
146
+ # Updating dataset with Math formula or filename given:
147
+ # dataset = Dataset.new('file.data')
148
+ # dataset.update('asd')
149
+ # #=> nothing updated
150
+ # dataset.update('asd', title: 'File')
151
+ # #=> Dataset.new('file.data', title: 'File')
152
+ # Updating dataset with data stored in Datablock:
153
+ # in_memory_points = Dataset.new(points, title: 'Old one')
154
+ # in_memory_points.update(some_update, title: 'Updated')
155
+ # #=> Dataset.new(points + some_update, title: 'Updated')
156
+ # temp_file_points = Dataset.new(points, title: 'Old one', file: true)
157
+ # temp_file_points.update(some_update)
158
+ # #=> data updated but no new dataset created
159
+ # temp_file_points.update(some_update, title: 'Updated')
160
+ # #=> data updated and new dataset with title 'Updated' returned
161
+ def update(data = nil, **options)
162
+ if data && @type == :datablock
163
+ new_datablock = @data.update(data)
164
+ if new_datablock == @data
165
+ update_options(options)
166
+ else
167
+ Dataset.new(@data.update(data), @options.merge(options))
168
+ end
169
+ else
170
+ update_options(options)
171
+ end
172
+ end
173
+
174
+ ##
175
+ # ====== Overview
176
+ # Own implementation of #clone. Creates new Dataset if
177
+ # data stored in datablock and calls super otherwise.
178
+ def clone
179
+ if @type == :datablock
180
+ Dataset.new(@data, **@options)
181
+ else
182
+ super
183
+ end
184
+ end
185
+
186
+ ##
187
+ # ====== Overview
188
+ # Creates new dataset with existing options merged with
189
+ # the given ones. Does nothing if no options given.
190
+ # ====== Parameters
191
+ # * *options* - hash to merge with existing options
192
+ # ====== Examples
193
+ # Updating dataset with Math formula or filename given:
194
+ # dataset = Dataset.new('file.data')
195
+ # dataset.update_options(title: 'File')
196
+ # #=> Dataset.new('file.data', title: 'File')
197
+ def update_options(**options)
198
+ if options.empty?
199
+ return self
200
+ else
201
+ Dataset.new(@data, @options.merge(options))
202
+ end
203
+ end
204
+
205
+ ##
206
+ # Method for inner use.
207
+ # Needed by OptionHandling to create new object when
208
+ # options are changed.
209
+ def new_with_options(options)
210
+ self.class.new(@data, options)
211
+ end
212
+
213
+ ##
214
+ # ====== Overview
215
+ # You may set options using #option_name(option_value) method.
216
+ # A new object will be constructed with selected option set.
217
+ # And finally you can get current value of any option using
218
+ # #options_name without arguments.
219
+ # ====== Examples
220
+ # dataset = Dataset.new('file.data')
221
+ # dataset.title #=> nil
222
+ # new_dataset = dataset.title('Awesome plot')
223
+ # dataset.title #=> nil
224
+ # new_dataset.title #=> 'Awesome plot'
225
+ def method_missing(meth_id, *args)
226
+ option(meth_id, *args)
227
+ end
228
+
229
+ private :init_default,
230
+ *INIT_HANDLERS.values,
231
+ :new_with_options
232
+ end
233
+ end