Skip to content

Commit 0923ac4

Browse files
committed
added code for line plots
1 parent e3e1b04 commit 0923ac4

4 files changed

Lines changed: 296 additions & 0 deletions

File tree

lib/rubyplot/axes.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ def bar! *args, &block
6464
@plots << plot
6565
end
6666

67+
def line! *args, &block
68+
plot = with_backend "Line", *args
69+
yield(plot) if block_given?
70+
@plots << plot
71+
end
72+
6773
def write file_name
6874
@plots[0].write file_name
6975
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
require_relative 'plot/scatter'
22
require_relative 'plot/bar'
3+
require_relative 'plot/line'
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
require_relative 'line/geometry'
2+
3+
# This module offers different rendering option for data points
4+
# to be used with line graph. For now we have circle and square as choices.
5+
# FIXME: refactor this module and make it a part of Line.
6+
module DotRenderers
7+
class Circle
8+
def render(d, new_x, new_y, circle_radius)
9+
d.circle(new_x, new_y, new_x - circle_radius, new_y)
10+
end
11+
end
12+
13+
class Square
14+
def render(d, new_x, new_y, circle_radius)
15+
offset = (circle_radius * 0.8).to_i
16+
corner_1 = new_x - offset
17+
corner_2 = new_y - offset
18+
corner_3 = new_x + offset
19+
corner_4 = new_y + offset
20+
d.rectangle(corner_1, corner_2, corner_3, corner_4)
21+
end
22+
end
23+
24+
def self.renderer(style)
25+
if style.to_s == 'square'
26+
Square.new
27+
else
28+
Circle.new
29+
end
30+
end
31+
end
32+
33+
module Rubyplot
34+
module MagickWrapper
35+
module Plot
36+
class Line < Artist
37+
# Dimensions of lines and dots; calculated based on dataset size if left unspecified
38+
attr_accessor :line_width
39+
attr_accessor :dot_radius
40+
# Call with target pixel width of graph (800, 400, 300), and/or
41+
# 'false' to omit lines (points only).
42+
# g = Rubyplot::Line.new(400) # 400px wide with lines.
43+
#
44+
# g = Rubyplot::Line.new(400, false) # 400px wide, no lines
45+
#
46+
# g = Rubyplot::Line.new(false) # Defaults to 800px wide, no lines
47+
def initialize(*args)
48+
raise ArgumentError, 'Wrong number of arguments' if args.length > 2
49+
if args.empty? || (!(Numeric === args.first) && !(String === args.first))
50+
super
51+
else
52+
super args.shift # TODO: Figure out a better alternative here.
53+
end
54+
55+
@geometry = Plot::Line::Geometry.new
56+
end
57+
58+
# Get the value if somebody has defined it.
59+
def baseline_value
60+
@geometry.reference_lines[:baseline][:value] if @geometry.reference_lines.key?(:baseline)
61+
end
62+
63+
# Set a value for a baseline reference line.
64+
def baseline_value=(new_value)
65+
@geometry.reference_lines[:baseline] ||= {}
66+
@geometry.reference_lines[:baseline][:value] = new_value
67+
end
68+
69+
def draw_reference_line(reference_line, left, right, top, bottom)
70+
@d = @d.push
71+
@d.stroke_color(@reference_line_default_color)
72+
@d.fill_opacity 0.0
73+
@d.stroke_dasharray(10, 20)
74+
@d.stroke_width(reference_line[:width] || @reference_line_default_width)
75+
@d.line(left, top, right, bottom)
76+
@d = @d.pop
77+
end
78+
79+
def draw
80+
super
81+
return unless @geometry.has_data
82+
83+
# Check to see if more than one datapoint was given. NaN can result otherwise.
84+
@x_increment = @geometry.column_count > 1 ?
85+
(@graph_width / (@geometry.column_count - 1).to_f) : @graph_width
86+
87+
@geometry.norm_data.each_with_index do |data_row, row_num|
88+
# Initially the previous x,y points are nil and then
89+
# they are set with values.
90+
prev_x = prev_y = nil
91+
92+
@one_point = contains_one_point_only?(data_row)
93+
94+
@d = @d.fill @plot_colors[row_num]
95+
data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index|
96+
x_data = data_row[DATA_VALUES_X_INDEX]
97+
if x_data.nil?
98+
new_x = @graph_left + (@x_increment * index)
99+
draw_label(new_x, index)
100+
else
101+
new_x = get_x_coord(x_data[index], @graph_width, @graph_left)
102+
@labels.each do |label_pos, _|
103+
draw_label(@graph_left + ((label_pos -
104+
@geometry.minimum_x_value) *
105+
@graph_width) /
106+
(@geometry.maximum_x_value -
107+
@geometry.minimum_x_value), label_pos)
108+
end
109+
end
110+
unless data_point
111+
# we can't draw a line for a null data point, we can still label the axis though
112+
prev_x = prev_y = nil
113+
next
114+
end
115+
new_y = @graph_top + (@graph_height - data_point * @graph_height)
116+
# Reset each time to avoid thin-line errors.
117+
# @d = @d.stroke data_row[DATA_COLOR_INDEX]
118+
# @d = @d.fill data_row[DATA_COLOR_INDEX]
119+
@d = @d.stroke_opacity 1.0
120+
@d = @d.stroke_width line_width ||
121+
clip_value_if_greater_than(@columns / (@geometry.norm_data.first[DATA_VALUES_INDEX].size * 4), 5.0)
122+
123+
circle_radius = dot_radius ||
124+
clip_value_if_greater_than(@columns / (@geometry.norm_data.first[DATA_VALUES_INDEX].size * 2.5), 5.0)
125+
126+
if !@geometry.hide_lines && !prev_x.nil? && !prev_y.nil?
127+
@d = @d.line(prev_x, prev_y, new_x, new_y)
128+
elsif @one_point
129+
# Show a circle if there's just one_point
130+
@d = DotRenderers.renderer(@geometry.dot_style).render(@d, new_x, new_y, circle_radius)
131+
end
132+
133+
unless @geometry.hide_dots
134+
@d = DotRenderers.renderer(@geometry.dot_style).render(@d, new_x, new_y, circle_radius)
135+
end
136+
137+
prev_x = new_x
138+
prev_y = new_y
139+
end
140+
end
141+
142+
@d.draw(@base_image)
143+
end
144+
145+
# Returns the X co-ordinate of a given data point.
146+
def get_x_coord(x_data_point, width, offset)
147+
x_data_point * width + offset
148+
end
149+
150+
# This method allows one to plot a dataset with both X and Y data.
151+
#
152+
# Parameters:
153+
# name: string, the title of the datasets.
154+
# x_data_points: an array containing the x data points for the graph.
155+
# y_data_points: an array containing the y data points for the graph.
156+
#
157+
# or
158+
#
159+
# name: string, the title of the dataset.
160+
# xy_data_points: an array containing both x and y data points for the graph.
161+
#
162+
#
163+
# Notes:
164+
# -if (x_data_points.length != y_data_points.length) an error is
165+
# returned.
166+
# -if you want to use a preset theme, you must set it before calling
167+
# dataxy().
168+
#
169+
# Example:
170+
# g = Rubyplot::Line.new
171+
# g.title = "X/Y Dataset"
172+
# g.dataxy("Apples", [1,3,4,5,6,10], [1, 2, 3, 4, 4, 3])
173+
# g.dataxy("Bapples", [1,3,4,5,7,9], [1, 1, 2, 2, 3, 3])
174+
# g.dataxy("Capples", [[1,1],[2,3],[3,4],[4,5],[5,7],[6,9]])
175+
# #you can still use the old data method too if you want:
176+
# g.data("Capples", [1, 1, 2, 2, 3, 3])
177+
# #labels will be drawn at the x locations of the keys passed in.
178+
# In this example the lables are drawn at x positions 2, 4, and 6:
179+
# g.labels = {0 => '2003', 2 => '2004', 4 => '2005', 6 => '2006'}
180+
# The 0 => '2003' label will be ignored since it is outside the chart range.
181+
def dataxy(_name, x_data_points = [], y_data_points = [], _color = nil)
182+
raise ArgumentError, 'x_data_points is nil!' if x_data_points.empty?
183+
184+
if x_data_points.all? { |p| p.is_a?(Array) && p.size == 2 }
185+
y_data_points = x_data_points.map { |p| p[1] }
186+
x_data_points = x_data_points.map { |p| p[0] }
187+
end
188+
189+
raise ArgumentError, 'x_data_points.length != y_data_points.length!' if
190+
x_data_points.length != y_data_points.length
191+
192+
# call the existing data routine for the y data.
193+
data(y_data_points, label: :name)
194+
195+
x_data_points = Array(x_data_points) # make sure it's an array
196+
# append the x data to the last entry that was just added in the @data member
197+
@data.last[DATA_VALUES_X_INDEX] = x_data_points
198+
199+
# Update the global min/max values for the x data
200+
x_data_points.each do |x_data_point|
201+
next if x_data_point.nil?
202+
203+
# Setup max/min so spread starts at the low end of the data points
204+
if @geometry.maximum_x_value.nil? && @geometry.minimum_x_value.nil?
205+
@geometry.maximum_x_value = @geometry.minimum_x_value = x_data_point
206+
end
207+
208+
@geometry.maximum_x_value = x_data_point > @geometry.maximum_x_value ?
209+
x_data_point : @geometry.maximum_x_value
210+
@geometry.minimum_x_value = x_data_point < @geometry.minimum_x_value ?
211+
x_data_point : @geometry.minimum_x_value
212+
end
213+
end
214+
215+
def normalize
216+
# First call the standard math function to normalize the values based on spread.
217+
super
218+
# TODO: Take care of the reference_lines
219+
220+
# normalize the x data if it is specified
221+
@data.each_with_index do |data_row, index|
222+
norm_x_data_points = []
223+
next if data_row[DATA_VALUES_X_INDEX].nil?
224+
data_row[DATA_VALUES_X_INDEX].each do |x_data_point|
225+
norm_x_data_points << ((x_data_point.to_f -
226+
@geometry.minimum_x_value.to_f) /
227+
(@geometry.maximum_x_value.to_f -
228+
@geometry.minimum_x_value.to_f))
229+
end
230+
@geometry.norm_data[index] << norm_x_data_points
231+
end
232+
end
233+
234+
def sort_norm_data
235+
super unless @data.any? { |d| d[DATA_VALUES_X_INDEX] }
236+
end
237+
238+
def contains_one_point_only?(data_row)
239+
# A helper function that acts as a sanity check for the data.
240+
# It spins through data to determine if there is just one_value present.
241+
one_point = false
242+
data_row[DATA_VALUES_INDEX].each do |data_point|
243+
next if data_point.nil?
244+
if one_point
245+
# more than one point, bail
246+
return false
247+
end
248+
# there is at least one data point
249+
one_point = true
250+
end
251+
one_point
252+
end
253+
end
254+
end
255+
end
256+
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module Rubyplot
2+
module MagickWrapper
3+
module Plot
4+
class Line < MagickWrapper::Artist
5+
class Geometry < MagickWrapper::Artist::Geometry
6+
attr_accessor :reference_lines
7+
attr_accessor :reference_line_default_color
8+
attr_accessor :reference_line_default_width
9+
attr_accessor :hide_dots
10+
attr_accessor :hide_lines
11+
attr_accessor :show_vertical_markers
12+
attr_accessor :dot_style
13+
attr_accessor :maximum_x_value
14+
attr_accessor :minimum_x_value
15+
16+
def initialize
17+
super
18+
@reference_lines = {}
19+
@reference_line_default_color = 'red'
20+
@reference_line_default_width = 5
21+
22+
@hide_dots = @hide_lines = false
23+
@dot_style = 'circle' # Options present for Circle and Square dot style.
24+
25+
@maximum_x_value = nil
26+
@minimum_x_value = nil
27+
@hide_line_markers = true
28+
end
29+
end # class Geometry
30+
end # class Line
31+
end # module Plot
32+
end # module MagickWrapper
33+
end # module Rubyplot

0 commit comments

Comments
 (0)