|
| 1 | +require_relative 'bubble/geometry' |
| 2 | + |
| 3 | +module Rubyplot |
| 4 | + module MagickWrapper |
| 5 | + module Plot |
| 6 | + class Bubble < MagickWrapper::Plot::Scatter |
| 7 | + # Rubyplot::Bubble takes the same parameters as the Rubyplot::Bubble graph |
| 8 | + # |
| 9 | + # ==== Example |
| 10 | + # g = Rubyplot::Bubble.new |
| 11 | + # |
| 12 | + def initialize(*) |
| 13 | + super |
| 14 | + @all_colors_array = Magick.colors |
| 15 | + @plot_colors = [] |
| 16 | + @z_data = [] |
| 17 | + @geometry = Plot::Bubble::Geometry.new |
| 18 | + end |
| 19 | + |
| 20 | + # The first parameter is the name of the dataset. The next two are the |
| 21 | + # x and y axis data points contain in their own array in that respective |
| 22 | + # order. Then the Z axis data points refer to the radius of the bubble plots. |
| 23 | + # The final parameter is the color. |
| 24 | + # |
| 25 | + # Can be called multiple times with different datasets for a multi-valued |
| 26 | + # graph. |
| 27 | + # |
| 28 | + # If the color argument is nil, the next color from the default theme will |
| 29 | + # be used. |
| 30 | + # |
| 31 | + # ==== Parameters to data function |
| 32 | + # name:: String or Symbol containing the name of the dataset. |
| 33 | + # x_data_points:: An Array of x-axis data points. |
| 34 | + # y_data_points:: An Array of y-axis data points. |
| 35 | + # z_data_points:: An Array containing the radius of the data points. |
| 36 | + # color:: The hex string for the color of the dataset. Defaults to nil. |
| 37 | + # |
| 38 | + # |
| 39 | + # ==== Examples |
| 40 | + # plot = Rubyplot::Bubble.new |
| 41 | + # plot.data(:apples, [-1, 19, -4, -23], [-35, 21, 23, -4], [45, 10, 21, 9]) |
| 42 | + # plot.data(:peaches, [20, 30, -6, -3], [-1, 5, -27, -3], [13, 10, 20, 10]) |
| 43 | + # plot.write('spec/reference_images/bubble_test_1.png') |
| 44 | + # |
| 45 | + def data(x_data_points = [], y_data_points = [], z_data_points = []) |
| 46 | +# name = label == :default ? ' ' : label.to_s |
| 47 | + # the existing data routine for the y axis data |
| 48 | + data_y(y_data_points, z_data_points) |
| 49 | + # append the x data to the last entry that was just added in the @data member |
| 50 | + # last_elem = @data.length - 1 |
| 51 | + # @data[last_elem] << x_data_points |
| 52 | + @data[:x_values] = x_data_points |
| 53 | + |
| 54 | + if @geometry.maximum_x_value.nil? && @geometry.minimum_x_value.nil? |
| 55 | + @geometry.maximum_x_value = @geometry.minimum_x_value = x_data_points.first |
| 56 | + end |
| 57 | + @z_data << z_data_points |
| 58 | + x_z_array_sum = [x_data_points, z_data_points].transpose.map { |x| x.reduce(:+) } |
| 59 | + x_z_array_diff = [x_data_points, z_data_points].transpose.map { |x| x.reduce(:-) } |
| 60 | + |
| 61 | + @geometry.maximum_x_value = x_z_array_sum.max > @geometry.maximum_x_value ? |
| 62 | + x_z_array_sum.max : @geometry.maximum_x_value |
| 63 | + @geometry.minimum_x_value = x_z_array_sum.min < @geometry.minimum_x_value ? |
| 64 | + x_z_array_sum.min : @geometry.minimum_x_value |
| 65 | + |
| 66 | + @geometry.maximum_x_value = x_z_array_diff.max > @geometry.maximum_x_value ? |
| 67 | + x_z_array_diff.max : @geometry.maximum_x_value |
| 68 | + @geometry.minimum_x_value = x_z_array_diff.min < @geometry.minimum_x_value ? |
| 69 | + x_z_array_diff.min : @geometry.minimum_x_value |
| 70 | + end |
| 71 | + |
| 72 | + private |
| 73 | + |
| 74 | + # Helper function to normalize the data along Y axis. |
| 75 | + def data_y(data_points = [], z_data_points) |
| 76 | + data_points = Array(data_points) # make sure it's an array |
| 77 | + # TODO: Adding an empty color array which can be developed later |
| 78 | + # to make graphs super customizable with regards to coloring of |
| 79 | + # individual data points. |
| 80 | + @data[:y_values] = data_points |
| 81 | + # Set column count if this is larger than previous column counts |
| 82 | + @geometry.column_count = data_points.length > @geometry.column_count ? |
| 83 | + data_points.length : @geometry.column_count |
| 84 | + |
| 85 | + y_z_array_sum = [data_points, z_data_points].transpose.map { |x| x.reduce(:+) } |
| 86 | + y_z_array_diff = [data_points, z_data_points].transpose.map { |x| x.reduce(:-) } |
| 87 | + if @geometry.maximum_value.nil? && @geometry.maximum_value.nil? |
| 88 | + @geometry.maximum_value = @geometry.minimum_value = data_points.first |
| 89 | + end |
| 90 | + @geometry.maximum_value = y_z_array_sum.max > @geometry.maximum_value ? |
| 91 | + y_z_array_sum.max : @geometry.maximum_value |
| 92 | + @geometry.minimum_value = y_z_array_sum.min < @geometry.minimum_value ? |
| 93 | + y_z_array_sum.min : @geometry.minimum_value |
| 94 | + @geometry.maximum_value = y_z_array_diff.max > @geometry.maximum_value ? |
| 95 | + y_z_array_diff.max : @geometry.maximum_value |
| 96 | + @geometry.minimum_value = y_z_array_diff.min < @geometry.minimum_value ? |
| 97 | + y_z_array_diff.min : @geometry.minimum_value |
| 98 | + @geometry.has_data = true |
| 99 | + end |
| 100 | + |
| 101 | + def draw |
| 102 | + super |
| 103 | + # Check to see if more than one datapoint was given. NaN can result otherwise. |
| 104 | + @x_increment = @geometry.column_count > 1 ? |
| 105 | + (@graph_width / (@geometry.column_count - 1).to_f) : @graph_width |
| 106 | + |
| 107 | + @geometry.norm_data.each_with_index do |data_row, data_row_index| |
| 108 | + data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index| |
| 109 | + @d = @d.fill @plot_colors[data_row_index] |
| 110 | + x_value = data_row[DATA_VALUES_X_INDEX][index] |
| 111 | + next if data_point.nil? || x_value.nil? |
| 112 | + |
| 113 | + new_x = get_x_coord(x_value, @graph_width, @graph_left) |
| 114 | + new_y = @graph_top + (@graph_height - data_point * @graph_height) |
| 115 | + |
| 116 | + # Reset each time to avoid thin-line errors |
| 117 | + @d = @d.stroke_opacity 1.0 |
| 118 | + @d.fill_opacity(0.3) |
| 119 | + @d.fill_color(@plot_colors[data_row_index]) |
| 120 | + @d = @d.stroke_width @stroke_width || |
| 121 | + clip_value_if_greater_than(@columns / |
| 122 | + (@geometry.norm_data.first[1].size * 4), 5.0) |
| 123 | + circle_radius = 2 * @z_data[data_row_index][index] |
| 124 | + @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) |
| 125 | + end |
| 126 | + end |
| 127 | + @d.draw(@base_image) |
| 128 | + end |
| 129 | + end # class Bubble |
| 130 | + end # module Plot |
| 131 | + end # module MagickWrapper |
| 132 | +end # module Rubyplot |
0 commit comments