nyaplot 0.1.6 → 0.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,227 @@
1
+ module Nyaplot
2
+ module Glyph
3
+ class << self
4
+ #@example
5
+ # Nyaplot::Glyph.instantiate(:scatter)
6
+ def instantiate(df, name, hash)
7
+ glyph = Kernel
8
+ .const_get("Nyaplot")
9
+ .const_get("Glyph")
10
+ .const_get(name.to_s.capitalize)
11
+
12
+ hash[:data] = df
13
+ glyph.new(hash)
14
+ end
15
+ end
16
+
17
+ class Glyph2D
18
+ include Nyaplot::Base
19
+ optional_args :transform
20
+
21
+ def range(label)
22
+ if data[label].all? {|v| v.is_a? Numeric}
23
+ [data[label].min, data[label].max]
24
+ else
25
+ data[label].uniq
26
+ end
27
+ end
28
+
29
+ def range_x
30
+ range(x)
31
+ end
32
+
33
+ def range_y
34
+ range(y)
35
+ end
36
+
37
+ def position(*args) ; end
38
+ end
39
+
40
+ class Histogram < Glyph::Glyph2D
41
+ required_args :data, :value, :position, :scalex
42
+ optional_args :bin_num, :width, :color, :stroke_color, :stroke_width
43
+ type :histogram
44
+
45
+ def initialize(*args)
46
+ super
47
+ bin_num(20)
48
+ end
49
+
50
+ def before_to_json
51
+ scalex(position.x)
52
+ end
53
+
54
+ def range_x
55
+ [data[value].min, data[value].max]
56
+ end
57
+
58
+ def range_y
59
+ min, max = range_x
60
+ bin = (max - min)/bin_num
61
+ max_bin = (1..bin_num).to_a
62
+ .map{|val| [min+bin*(val-1), min+bin*val]}
63
+ .map{|range| data[value].to_a.select{|val| val>=range[0] && val<range[1]}.length}
64
+ .max
65
+ print max_bin
66
+ [0, max_bin*1.2]
67
+ end
68
+ end
69
+
70
+ class Scatter < Glyph::Glyph2D
71
+ required_args :data, :x, :y, :position
72
+ optional_args :color, :shape, :size, :stroke_color, :stroke_width
73
+ type :scatter
74
+
75
+ # Change symbol size according to data in specified column
76
+ def size_by(column_name)
77
+ range = size.nil? ? [10, 100] : size
78
+ df_scale = Nyaplot::DataFrameScale.new(data: data, column: column_name, range: range)
79
+ scale = Nyaplot::RowScale.new(column: column_name, scale: df_scale)
80
+ self.size(scale)
81
+ end
82
+
83
+ # Change symbol shape according to data in specified column
84
+ # Value range (ex. "circle", "diamond", ...) can be specified using Scatter#shape
85
+ # @example
86
+ # x = ["a", "b", "a"]; y = [1, 2, 3]
87
+ # sc = Scatter.new(data: data, x: :x, y: :y)
88
+ # sc.shape_by(:x) #-> circle, triangle-up, circle (default)
89
+ # sc.shape([:square, :cross]).shape_by(:x) #-> square, cross, square
90
+ #
91
+ def shape_by(column_name)
92
+ range = shape.nil? ? ['circle','triangle-up', 'diamond', 'square', 'triangle-down', 'cross'] : shape
93
+ df_scale = Nyaplot::DataFrameScale.new(data: data, column: column_name, range: range)
94
+ scale = Nyaplot::RowScale.new(column: column_name, scale: df_scale)
95
+ self.shape(scale)
96
+ end
97
+ end
98
+
99
+ class Line < Glyph::Glyph2D
100
+ required_args :data, :x, :y, :position
101
+ optional_args :color, :stroke_width
102
+ type :line
103
+ end
104
+
105
+ class Rect < Glyph::Glyph2D
106
+ required_args :data, :x, :y
107
+ optional_args :width, :height, :color, :stroke_width, :stroke_color, :x_base, :y_base
108
+ type :rect
109
+ end
110
+
111
+ class LineSegment < Glyph::Glyph2D
112
+ required_args :data, :x1, :y1, :x2, :y2
113
+ optional_args :color, :stroke_width
114
+ type :line_segment
115
+ end
116
+
117
+ class Bar < Glyph::Rect
118
+ optional_args :bin_size
119
+
120
+ # @example
121
+ # Plot.new(:bar, x: :[:hoge, :nya, :nyan], y: [100, 200, 10])
122
+ # -> df = DataFrame.new(x: [:hoge, :nya, :nyan], y: [100, 200, 10])
123
+ # -> Bar.new(x: :x, y: :y).data(df)
124
+ #
125
+ def initialize(*args)
126
+ super()
127
+ y_base "bottom"
128
+ bin_size 0.8
129
+ data args.first[:data]
130
+ @x_label = args.first[:x]
131
+ @y_label = args.first[:y]
132
+ end
133
+
134
+ def range_x
135
+ data[@x_label]
136
+ end
137
+
138
+ def range_y
139
+ column = data[@y_label]
140
+ min = column.min < 0 ? column.min : 0
141
+ [min, column.max]
142
+ end
143
+
144
+ def position(pos)
145
+ x RowScale.new(column: @x_label, scale: pos.x)
146
+ y(pos.y.range.max)
147
+ scale = Scale.new(range: pos.y.range.reverse, domain: pos.y.domain, type: pos.y.type)
148
+ height RowScale.new(column: @y_label, scale: scale)
149
+ width((pos.x.range.max/self.range_x.length)*bin_size)
150
+ end
151
+ end
152
+
153
+ class HeatMap < Glyph::Rect
154
+ end
155
+
156
+ class Box < Glyph::Rect
157
+ optional_args :bin_size
158
+ attr_reader :child
159
+
160
+ # @example
161
+ # df = DataFrame.new({hoge: [1,2,3], nya: [2,3,4], nyan: [5,6,7]})
162
+ # Plot.from(df).add(:box, :columns: [:hoge, :nya, :nyan])
163
+ #
164
+ def initialize(*args)
165
+ super()
166
+ bin_size 0.9
167
+ rows = args.first[:columns].map do |label|
168
+ column = args.first[:data][label]
169
+ q1, q3 = column.percentil(25), column.percentil(75)
170
+ h = q3 - q1
171
+ min = q1 - column.min > 1.5*h ? q1-1.5*h : column.min
172
+ max = column.max - q3 > 1.5*h ? q3+1.5*h : column.max
173
+ [
174
+ label,
175
+ max,
176
+ min,
177
+ q1,
178
+ q3,
179
+ q3 - q1,
180
+ column.median,
181
+ column.to_a.select{|val| val < min || val > max}.to_a
182
+ ]
183
+ end
184
+
185
+ columns = rows.transpose
186
+ @outlier = columns.pop
187
+ @child = LineSegment.new
188
+
189
+ class << @child
190
+ def range_x ; []; end
191
+ def range_y ; [Float::INFINITY, -Float::INFINITY]; end
192
+ end
193
+
194
+ add_dependency(@child)
195
+ data(DataFrame.new(columns, labels: [:label, :max, :min, :q1, :q3, :height, :median]))
196
+ end
197
+
198
+ def range_x
199
+ data[:label]
200
+ end
201
+
202
+ def range_y
203
+ [data[:min].min, data[:max].max]
204
+ end
205
+
206
+ def position(pos)
207
+ padding = pos.x.range.max/data.length
208
+
209
+ x RowScale.new(column: :label, scale: pos.x)
210
+ y RowScale.new(column: :q3, scale: pos.y)
211
+ height RowScale.new(column: :height, scale: pos.y)
212
+ width padding * bin_size
213
+ transform("translate(" + ((padding*(1-bin_size))/2).to_s + ",0)")
214
+
215
+ @child
216
+ .data(data)
217
+ .x1(self.x)
218
+ .x2(self.x)
219
+ .y1(RowScale.new(column: :min, scale: pos.y))
220
+ .y2(RowScale.new(column: :max, scale: pos.y))
221
+ .stroke_width(1.5)
222
+ .color("#000")
223
+ .transform("translate(" + (padding/2).to_s + ",0)")
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,94 @@
1
+ module Nyaplot
2
+ module Interactive
3
+ @@callbacks = {}
4
+
5
+ def init_interactive_layer
6
+ path = File.expand_path("../templates/init_interactive.js", __FILE__)
7
+ js = File.read(path)
8
+ IRuby.display(IRuby.javascript(js))
9
+ end
10
+
11
+ def self.included(cls)
12
+ init_interactive_layer
13
+ comm_id = SecureRandom.hex(16)
14
+ @@comm = IRuby::Comm.new("nyaplot_interactive", comm_id)
15
+ @@comm.open
16
+ IRuby::Kernel.instance.register_comm(comm_id, @@comm)
17
+
18
+ on_msg = Proc.new do |msg|
19
+ content = msg
20
+ # msg: {type: "selected", uuid: , range: [0, 100]}
21
+ if content[:type.to_s] == "selected"
22
+ cb = @@callbacks[content[:stage_uuid.to_s]]
23
+ cb.call(content[:range.to_s])
24
+ end
25
+ end
26
+ @@comm.on_msg(on_msg)
27
+ @@comm.send({type: "hello"})
28
+ end
29
+ end
30
+
31
+ class Filter
32
+ include Nyaplot::Base
33
+ required_args :scalex, :scaley, :stage_uuid
34
+ optional_args :opacity, :color
35
+ type :interactive_layer
36
+ end
37
+
38
+ class Plot2D
39
+ def add_filter(&block)
40
+ if (stages = @pane.dependency.select{|d| d.is_a?(Nyaplot::Stage2D)}).length == 1
41
+ stages.first.add_filter(&block)
42
+ else
43
+ raise "specify stage to add filter."
44
+ end
45
+ end
46
+ end
47
+
48
+ class Stage2D
49
+ def add_filter(&block)
50
+ @filter = Filter.new(stage_uuid: @uuid)
51
+ add_sheet(@filter)
52
+ @@callbacks[@uuid] = block
53
+ end
54
+
55
+ def before_to_json
56
+ unless @filter.nil?
57
+ xscale = @axis.xscale
58
+ yscale = @axis.yscale
59
+ @filter.scalex(xscale).scaley(yscale)
60
+ end
61
+ end
62
+ end
63
+
64
+ class EmptyPlot
65
+ include PlotBase
66
+
67
+ def initialize(*args)
68
+ @pane = args.select{|a| a.is_a?(Nyaplot::Pane)}.first
69
+ args.delete(@pane)
70
+ @others = args
71
+ end
72
+
73
+ def update
74
+ # should be refactored
75
+ old_pane_uuid = @pane.uuid
76
+ msg = {
77
+ type: :update,
78
+ model: self.to_json
79
+ }
80
+ new_pane_uuid = @pane.uuid
81
+ msg[:model].gsub!(new_pane_uuid, old_pane_uuid)
82
+ @pane.uuid = old_pane_uuid
83
+ @@comm.send(msg)
84
+ STDERR.puts msg
85
+ end
86
+
87
+ def to_json(*args)
88
+ gen_list = generate_gen_list(@others)
89
+ list = gen_list.sort_by{|k, v| v}.map{|arr| arr.first.to_json}.reverse
90
+ list.push(@pane.to_json)
91
+ "[" + list.join(",") + "]"
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,43 @@
1
+ module Nyaplot
2
+ # The wrapper for pane of Nyaplotjs
3
+ # (https://github.com/domitry/Nyaplotjs/blob/v2/src/parser/pane.js)
4
+ #
5
+ # @example
6
+ # p1 = Pane.new.columns(s1, s2)
7
+ # p2 = Pane.new.columns(s3, s4)
8
+ # p3 = Pane.new.rows(p1, p2)
9
+ # p3.to_iruby
10
+
11
+ class Pane
12
+ include Nyaplot::Base
13
+
14
+ type :pane
15
+ required_args :parent_id, :layout
16
+
17
+ def before_to_json
18
+ @uuid = SecureRandom.uuid
19
+ parent_id("vis-" + @uuid)
20
+ end
21
+
22
+ def add(name, *stages)
23
+ contents = stages.map do |s|
24
+ if s.is_a? Nyaplot::Pane
25
+ s.layout
26
+ else s.is_a? Nyaplot::Stage2D
27
+ add_dependency(s)
28
+ {sync: s.uuid}
29
+ end
30
+ end
31
+ layout({type: name, contents: contents})
32
+ self
33
+ end
34
+
35
+ def self.columns(*stages)
36
+ self.new.add(:columns, *stages)
37
+ end
38
+
39
+ def self.rows(*stages)
40
+ self.new.add(:rows, *stages)
41
+ end
42
+ end
43
+ end
data/lib/nyaplot/plot.rb CHANGED
@@ -1,150 +1,199 @@
1
1
  module Nyaplot
2
+ # Base module for Plot-something of Nyaplot. (e.g. Nyaplot::Plot2D or Nyaplot::Plot)
3
+ # Plot have one root named "pane".
4
+ # Plot resolve dependency according to pane, create model, and generate html code according to it.
5
+ module PlotBase
6
+ def initialize(*args)
7
+ @pane = Nyaplot::Pane.new
8
+ end
2
9
 
3
- # Jsonizable Object to which diagrams are registered
4
- # Properties of Nyaplot::Plot are embeded into the JSON object as a part of property 'panes' by Nyaplot::Frame
5
- class Plot
6
- include Jsonizable
7
- # @!attribute width
8
- # @return [Numeric] the width
9
- # @!attribute height
10
- # @return [Numeric] the height
11
- # @!attribute margin
12
- # @return [Hash] the margin
13
- # @!attribute xrange
14
- # @return [Array<Numeric>, Array<String>, Array<Symbol>] the name of width set
15
- # @!attribute yrange
16
- # @return [Array<Numeric>, Array<String>, Array<Symbol>] the name of width set
17
- # @!attribute x_label
18
- # @return [String] the name of label placed along x-axis
19
- # @!attribute y_label
20
- # @return [String] the name of label placed along y-axis
21
- # @!attribute bg_color
22
- # @return [String] the code of color which background is filled in
23
- # @!attribute grid_color
24
- # @return [String] the code of color which grid lines are filled in
25
- # @!attribute legend
26
- # @return [Boolean] whether to show legend or not
27
- # @!attribute legend_width
28
- # @return [Numeric] the width of legend area
29
- # @!attribute legend_options
30
- # @return [Hash] the name of width set
31
- # @!attribute zoom
32
- # @return [Boolean] whether to enable zooming
33
- # @!attribute rotate_x_label
34
- # @return [Numeric] the angle to rotate x label (radian)
35
- # @!attribute rotate_y_label
36
- # @return [Numeric] the angle to rotate y label (radian)
37
- # @!attribute x_scale
38
- # @return [String] the type of scale ("linear", "log" and "pow" are supported.)
39
- # @!attribute y_scale
40
- # @return [String] the type of scale ("linear", "log" and "pow" are supported.)
41
- define_properties(:diagrams, :filter)
42
- define_group_properties(:options, [:width, :height, :margin, :xrange, :yrange, :x_label, :y_label, :bg_color, :grid_color, :legend, :legend_width, :legend_options, :zoom, :rotate_x_label, :rotate_y_label, :x_scale, :y_scale])
43
-
44
- def initialize(&block)
45
- init_properties
46
- set_property(:diagrams, [])
47
- set_property(:options, {})
48
- set_property(:width, nil)
49
- set_property(:legend, nil)
50
- set_property(:zoom, nil)
51
-
52
- yield if block_given?
53
- end
54
-
55
- # Add diagram with Array
56
- # @param [Symbol] type the type of diagram to add
57
- # @param [Array<Array>] *data array from which diagram is created
58
- # @example
59
- # plot.add(:scatter, [0,1,2], [0,1,2])
60
- def add(type, *data)
61
- labels = data.map.with_index{|d, i| 'data' + i.to_s}
62
- raw_data = data.each.with_index.reduce({}){|memo, (d, i)| memo[labels[i]]=d; next memo}
63
- df = DataFrame.new(raw_data)
64
- return add_with_df(df, type, *labels)
65
- end
66
-
67
- # Add diagram with DataFrame
68
- # @param [DataFrame] DataFrame from which diagram is created
69
- # @param [Symbol] type the type of diagram to add
70
- # @param [Array<Symbol>] *labels column labels for x, y or some other dimension
71
- # @example
72
- # df = Nyaplot::DataFrame.new({x: [0,1,2], y: [0,1,2]})
73
- # plot.add(df, :scatter, :x, :y)
74
- def add_with_df(df, type, *labels)
75
- diagram = Diagram.new(df, type, labels)
76
- diagrams = get_property(:diagrams)
77
- diagrams.push(diagram)
78
- return diagram
10
+ # @return
11
+ # {Obj1: 2, Obj2: 0, Obj3: 15, .., Objn: 0}
12
+ def generate_gen_list(stack)
13
+ gen = 0; gen_list = {}
14
+
15
+ while stack.length > 0
16
+ stack = stack.reduce([]) do |memo, obj|
17
+ gen_list[obj] = gen
18
+ obj.resolve_dependency if obj.respond_to? :resolve_dependency
19
+ memo.concat(obj.dependency)
20
+ next memo
21
+ end
22
+ gen += 1
23
+ end
24
+
25
+ gen_list
79
26
  end
80
27
 
28
+ # generate model
29
+ def to_json(*args)
30
+ gen_list = generate_gen_list([@pane])
31
+ "[" + gen_list.sort_by{|k, v| v}.map{|arr| arr.first.to_json}.reverse.join(",") + "]"
32
+ end
33
+
34
+ # generate html code for <body> tag
35
+ def generate_body
36
+ path = File.expand_path("../templates/iruby.erb", __FILE__)
37
+ template = File.read(path)
38
+ model = self.to_json
39
+ id = @pane.uuid
40
+ ERB.new(template).result(binding)
41
+ end
42
+
43
+ # generate static html file
44
+ # @return [String] generated html
45
+ def generate_html
46
+ body = generate_body
47
+ init = Nyaplot.generate_init_code
48
+ path = File.expand_path("../templates/static_html.erb", __FILE__)
49
+ template = File.read(path)
50
+ ERB.new(template).result(binding)
51
+ end
81
52
 
82
- # Show plot automatically on IRuby notebook
53
+ # export static html file
54
+ def export_html(path="./plot.html")
55
+ path = File.expand_path(path, Dir::pwd)
56
+ str = generate_html
57
+ File.write(path, str)
58
+ end
59
+
60
+ # show plot automatically on IRuby notebook
83
61
  def to_iruby
84
- Frame.new.tap {|f| f.add(self) }.to_iruby
62
+ html = generate_body
63
+ ['text/html', html]
85
64
  end
86
65
 
87
- # Show plot on IRuby notebook
66
+ # show plot on IRuby notebook
88
67
  def show
89
- Frame.new.tap {|f| f.add(self) }.show
68
+ IRuby.display(self)
90
69
  end
70
+ end
91
71
 
92
- # export html file
93
- def export_html(path=nil)
94
- require 'securerandom'
95
- path = "./plot-" + SecureRandom.uuid().to_s + ".html" if path.nil?
96
- Frame.new.tap {|f| f.add(self) }.export_html(path)
72
+ # Base class for general 2-dimentional plots
73
+ # Plot2D have one pane, some stage2ds and glyphs
74
+ class Plot2D
75
+ include PlotBase
76
+ attr_accessor :pane, :stages, :glyphs
77
+
78
+ def initialize
79
+ super
80
+ stage = Stage2D.new
81
+ @pane = Pane.rows(stage)
82
+ @dependency = [@pane, stage]
97
83
  end
98
84
 
99
- # @return [Array<String>] names of dataframe used by diagrams belog to this plot
100
- def df_list
101
- arr=[]
102
- diagrams = get_property(:diagrams)
103
- diagrams.each{|d| arr.push(d.df_name)}
104
- return arr
105
- end
85
+ class << self
86
+ # shortcut method for Plot#add
87
+ # @example
88
+ # Plot.add(:scatter, a, b)
89
+ #
90
+ def add(*args)
91
+ self.new.add(*args)
92
+ end
106
93
 
107
- def before_to_json
108
- diagrams = get_property(:diagrams)
109
- return if diagrams.length == 0
94
+ # shortcut method for Plot#from
95
+ # @example
96
+ # df = DataFrame.new({hoge: [1,2,3], nya: [2,3,4]})
97
+ # Plot.from(df).add(:scatter, :hoge, :nya)
98
+ #
99
+ def from(df)
100
+ self.new.from(df)
101
+ end
102
+ end
110
103
 
111
- # set default values when not specified by users
112
- zoom(true) if zoom.nil? && diagrams.all?{|d| d.zoom?}
104
+ def from(df)
105
+ if df.is_a? DataFrame
106
+ @df = df
107
+ self
108
+ else
109
+ raise ""
110
+ end
111
+ end
113
112
 
114
- if width.nil?
115
- if legend == true
116
- width(800)
113
+ # Add glyph, sheet or stage to Plot
114
+ # @example
115
+ # Plot.add(:scatter, x, y)
116
+ # Plot.add(sc)
117
+ #
118
+ def add(*args)
119
+ if args.first.is_a? Symbol
120
+ name = args.shift
121
+ raise "invalid arguments" unless args.length == 1 && args.first.is_a?(Hash)
122
+ if (hash = args.first) && !(@df.nil?)
123
+ glyph = Nyaplot::Glyph.instantiate(@df, name, hash)
117
124
  else
118
- width(700)
125
+ # hash: {x: [0, 1, 2], y: [1, 2, 3]}
126
+ df = DataFrame.new(hash)
127
+ arg = hash.reduce({}){|memo, val| memo[val[0]] = val[0]; memo}
128
+ glyph = Nyaplot::Glyph.instantiate(df, name, arg)
119
129
  end
120
- end
121
-
122
- [:xrange, :yrange].each do |symbol|
123
- if get_property(:options)[symbol].nil?
124
- range = []
125
- diagrams.each{|diagram| range.push(diagram.send(symbol))}
126
-
127
- if range.all? {|r| r.length == 2} # continuous data
128
- range = range.transpose
129
- range = [range[0].min, range[1].max]
130
- self.send(symbol, range)
131
- else # discrete data
132
- range.flatten!.uniq!
133
- self.send(symbol, range)
130
+ add_glyph(glyph)
131
+ else
132
+ args.each do |obj|
133
+ if obj.is_a? Nyaplot::Glyph2D
134
+ add_glyph(obj)
135
+ elsif obj.is_a? Nyaplot::Stage2D
136
+ add_stage(obj)
134
137
  end
135
138
  end
136
139
  end
140
+ self
137
141
  end
138
142
 
139
- # Shortcut method to configure plot
140
- # @example
141
- # plot = Nyaplot::Plot.new
142
- # plot.configure do
143
- # width(700)
144
- # height(700)
145
- # end
146
- def configure(&block)
147
- self.instance_eval(&block) if block_given?
143
+ def add_glyph(glyph)
144
+ stages = @dependency.select{|obj| obj.is_a? Nyaplot::Stage2D}
145
+ if stages.length == 0
146
+ stage = Nyaplot::Stage2D.new
147
+ add_stage(stage)
148
+ stage.context.add(glyph)
149
+ elsif stages.length == 1
150
+ stages.first.context.add(glyph)
151
+ else
152
+ raise "Specify stage to add the glyph."
153
+ end
154
+ @dependency.push(glyph)
155
+ end
156
+
157
+ def add_stage(stage)
158
+ @pane = Pane.columns(@pane, stage)
159
+ @dependency.push(stage)
160
+ end
161
+
162
+ def glyphs
163
+ if (s = self.stages).length == 1
164
+ context = s.first.context
165
+ context.glyphs
166
+ else
167
+ raise "Specify stage from which select glyphs from"
168
+ end
169
+ end
170
+
171
+ def glyph
172
+ self.glyphs.first
148
173
  end
174
+
175
+ def stages
176
+ @pane.dependency.select{|d| d.is_a?(Nyaplot::Stage2D)}
177
+ end
178
+
179
+ def stage
180
+ if (s = self.stages).length == 1
181
+ s.first
182
+ else
183
+ raise "This plot has 2>= stages."
184
+ end
185
+ end
186
+
187
+ def method_missing(name, *args)
188
+ super if @dependency.all?{|obj| !(obj.respond_to? name)}
189
+ @dependency.each do |obj|
190
+ break obj.send(name, *args) if obj.respond_to? name
191
+ end
192
+ self
193
+ end
194
+ end
195
+
196
+ # shortcut for Nyaplot::Plot2D
197
+ class Plot < Plot2D
149
198
  end
150
199
  end