nyaplot 0.1.6 → 0.2.0.rc1

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,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