gnuplotrb 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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