active_charts 1.0.1

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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_charts/util'
4
+
5
+ module ActiveCharts
6
+ module Helpers #:nodoc:
7
+ module LineChartHelper
8
+ def line_chart(collection, options = {})
9
+ LineChart.new(collection, options).to_html
10
+ end
11
+
12
+ def line_chart_for(resource_collection, columns = [], options = {})
13
+ return line_chart([[]], options) unless Util.valid_collection?(resource_collection)
14
+
15
+ parser = CollectionParser.new(resource_collection, columns, options[:label_column])
16
+ series_labels = options[:series_labels] || parser.xy_series_labels
17
+ options = options.merge(series_labels: series_labels, rows: parser.rows)
18
+
19
+ line_chart(parser.xy_collection, options)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_charts/util'
4
+
5
+ module ActiveCharts
6
+ module Helpers #:nodoc:
7
+ module ScatterPlotHelper
8
+ def scatter_plot(collection, options = {})
9
+ ScatterPlot.new(collection, options).to_html
10
+ end
11
+
12
+ def scatter_plot_for(resource_collection, columns = [], options = {})
13
+ return scatter_plot([[]], options) unless Util.valid_collection?(resource_collection)
14
+
15
+ parser = CollectionParser.new(resource_collection, columns, options[:label_column])
16
+ series_labels = options[:series_labels] || parser.xy_series_labels
17
+ options = options.merge(series_labels: series_labels, rows: parser.rows)
18
+
19
+ scatter_plot(parser.xy_collection, options)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,61 @@
1
+ module ActiveCharts
2
+ class LineChart < XYChart
3
+ attr_reader :line_labels
4
+
5
+ def chart_svg_tag
6
+ inner_html = [grid_rect_tag, ticks(x_ticks, y_ticks), lines, line_label_tags,
7
+ side_label_text_tags, bottom_label_text_tags].flatten.join('
8
+ ')
9
+
10
+ tag.svg(inner_html.html_safe, svg_options)
11
+ end
12
+
13
+ def lines
14
+ dots_specs.map.with_index do |line_dots, index|
15
+ d = line_dots.map do |dot|
16
+ [dot[:cx], dot[:cy]].join(' ')
17
+ end
18
+
19
+ tag.path('', d: 'M' + d.join(' L '), class: line_classes(index))
20
+ end
21
+ end
22
+
23
+ def line_label_tags
24
+ dots_specs.flatten.map do |dot|
25
+ tag.text(dot[:label], x: dot[:cx] + OFFSET, y: dot[:cy] - OFFSET, class: label_classes)
26
+ end
27
+ end
28
+
29
+ def dots_specs
30
+ (0..columns_count - 1).map do |col_index|
31
+ collection.map.with_index do |row, row_index|
32
+ dot_spec(row[col_index], row_index)
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def process_options(options)
40
+ super
41
+
42
+ @line_labels = options[:rows] || []
43
+ end
44
+
45
+ def dot_spec(cell, row_index)
46
+ { cx: dot_cx(cell.first), cy: dot_cy(cell.last), label: line_labels[row_index] }
47
+ end
48
+
49
+ def dot_cx(value)
50
+ Util.scaled_position(value, x_min, x_max, grid_width).round(6)
51
+ end
52
+
53
+ def dot_cy(value)
54
+ grid_height - Util.scaled_position(value, y_min, y_max, grid_height).round(6)
55
+ end
56
+
57
+ def line_classes(col)
58
+ ['ac-line-chart-line', series_class(col)].join(' ')
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,64 @@
1
+ module ActiveCharts
2
+ class RectangularChart < Chart
3
+ TOP_LEFT_OFFSET = 1
4
+
5
+ def initialize(collection, options = {})
6
+ super
7
+
8
+ prereq_calcs
9
+
10
+ values = values_calcs
11
+
12
+ width_calcs(values.map(&width_filter))
13
+ height_calcs(values.map(&height_filter))
14
+ end
15
+
16
+ attr_reader :grid_height, :grid_width, :svg_height, :svg_width
17
+
18
+ def grid_rect_tag
19
+ tag.rect(
20
+ x: TOP_LEFT_OFFSET,
21
+ y: TOP_LEFT_OFFSET,
22
+ height: grid_height - TOP_LEFT_OFFSET * 2,
23
+ width: grid_width - TOP_LEFT_OFFSET * 2,
24
+ class: 'grid'
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def process_options(options)
31
+ super
32
+
33
+ @grid_width = @svg_width = options[:width] || MARGIN * 30
34
+ @grid_height = @svg_height = options[:height] || MARGIN * 20
35
+ end
36
+
37
+ def prereq_calcs; end
38
+
39
+ def values_calcs
40
+ []
41
+ end
42
+
43
+ def width_filter
44
+ :first
45
+ end
46
+
47
+ def height_filter
48
+ :last
49
+ end
50
+
51
+ def width_calcs(_values); end
52
+
53
+ def height_calcs(_values); end
54
+
55
+ def ticks(vertical_ticks, horizontal_ticks)
56
+ (vertical_ticks.map { |x| tick_line_tag(x, x, TOP_LEFT_OFFSET, grid_height - TOP_LEFT_OFFSET) } +
57
+ horizontal_ticks.map { |y| tick_line_tag(TOP_LEFT_OFFSET, grid_width - TOP_LEFT_OFFSET, y, y) })
58
+ end
59
+
60
+ def tick_line_tag(x1, x2, y1, y2)
61
+ %(<line #{tag_options(x1: x1, x2: x2, y1: y1, y2: y2)} class="ac-grid-line" />)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,55 @@
1
+ module ActiveCharts
2
+ class ScatterPlot < XYChart
3
+ attr_reader :dot_labels
4
+
5
+ def chart_svg_tag
6
+ inner_html = [grid_rect_tag, ticks(x_ticks, y_ticks), dots,
7
+ side_label_text_tags, bottom_label_text_tags].flatten.join('
8
+ ')
9
+
10
+ tag.svg(inner_html.html_safe, svg_options)
11
+ end
12
+
13
+ def dots
14
+ whitelist = %w[cx cy class]
15
+
16
+ dots_specs.flatten.map do |dot|
17
+ [%(<circle #{tag_options(dot, whitelist)} />),
18
+ tag.text(dot[:label], x: dot[:cx] + OFFSET, y: dot[:cy] - OFFSET, class: label_classes)]
19
+ end
20
+ end
21
+
22
+ def dots_specs
23
+ collection.map.with_index do |row, row_index|
24
+ row.map.with_index do |cell, col_index|
25
+ dot_spec(cell, row_index, col_index)
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def process_options(options)
33
+ super
34
+
35
+ @dot_labels = options[:rows] || []
36
+ end
37
+
38
+ def dot_spec(cell, row_index, col_index)
39
+ { cx: dot_cx(cell.first), cy: dot_cy(cell.last), class: dot_classes(col_index),
40
+ label: dot_labels[row_index] }
41
+ end
42
+
43
+ def dot_cx(value)
44
+ Util.scaled_position(value, x_min, x_max, grid_width).round(6)
45
+ end
46
+
47
+ def dot_cy(value)
48
+ grid_height - Util.scaled_position(value, y_min, y_max, grid_height).round(6)
49
+ end
50
+
51
+ def dot_classes(col)
52
+ ['ac-scatter-plot-dot', 'ac-triggerable', series_class(col)].join(' ')
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,121 @@
1
+ module ActiveCharts
2
+ # @private
3
+ module Util
4
+ module_function
5
+
6
+ def max_values(array_of_arrays)
7
+ return [] unless array_of_arrays?(array_of_arrays)
8
+
9
+ maxes = initialize_maxes(array_of_arrays.first)
10
+
11
+ array_of_arrays[1..-1].each do |row|
12
+ row.map { |cell| safe_to_dec(cell) }
13
+ .each_with_index do |val, index|
14
+ maxes[index] = val if index > maxes.count - 1 || val > maxes[index]
15
+ end
16
+ end
17
+
18
+ maxes
19
+ end
20
+
21
+ def array_of_arrays?(item)
22
+ item.is_a?(Array) && !item.empty? && item.all? { |row| row.is_a?(Array) }
23
+ end
24
+
25
+ def initialize_maxes(row)
26
+ row.map do |cell|
27
+ safe_to_dec(cell) <= 0 ? 1 : safe_to_dec(cell)
28
+ end # solves floating 0 labels bug
29
+ end
30
+
31
+ def multiplier(data_value, pixels, precision = 6)
32
+ (pixels / safe_to_dec(data_value)).round(precision)
33
+ end
34
+
35
+ def safe_to_dec(item)
36
+ item = Date.new(item.year, item.month, item.day) if date_like?(item)
37
+ item = item.jd if item.respond_to?(:jd)
38
+
39
+ item.to_d
40
+ rescue
41
+ 0.0
42
+ end
43
+
44
+ def date_like?(item)
45
+ return false if item.respond_to?(:jd)
46
+
47
+ %i[year month day].all? do |method|
48
+ item.respond_to?(method) &&
49
+ item.send(method).class.eql?(Integer)
50
+ end
51
+ end
52
+
53
+ def date_label(val)
54
+ val = Date.jd(val) if val.class.superclass.eql?(Numeric)
55
+
56
+ val.respond_to?(:strftime) ? val.strftime('%F') : val.to_s
57
+ end
58
+
59
+ def grid_index(width, x, y)
60
+ width * y + x
61
+ end
62
+
63
+ def scaled_position(n, a, b, scale_length)
64
+ multiplier = scale_length.to_d / (b - a)
65
+
66
+ (n - a) * multiplier
67
+ end
68
+
69
+ def scale(min, max)
70
+ return [0, 1, 1] unless valid_max_min?(min, max)
71
+
72
+ step = scale_interval(min, max)
73
+
74
+ a = scale_a(min, step)
75
+ b = scale_b(max, step)
76
+
77
+ [a, b, step]
78
+ end
79
+
80
+ def scale_a(min, step)
81
+ return 0 if min.zero?
82
+
83
+ unscaled_a = (min.to_d / step).to_i
84
+ unscaled_a -= 1 if min.negative? || (min == unscaled_a * step)
85
+
86
+ unscaled_a * step
87
+ end
88
+
89
+ def scale_b(max, step)
90
+ return 0 if max.zero?
91
+
92
+ unscaled_b = (max.to_d / step).to_i
93
+ unscaled_b += 1 if max.positive? || (max == unscaled_b * step)
94
+
95
+ unscaled_b * step
96
+ end
97
+
98
+ def scale_interval(min, max)
99
+ diff = (max - min).abs
100
+
101
+ case diff
102
+ when 0..2
103
+ 0.5
104
+ when 3..10
105
+ 1
106
+ else
107
+ 10**Math.log(diff, 10).to_i
108
+ end
109
+ end
110
+
111
+ def valid_max_min?(min, max)
112
+ [min, max].all? { |n| n.class.superclass.eql?(Numeric) } && max > min
113
+ end
114
+
115
+ def valid_collection?(item)
116
+ item.respond_to?(:first) && item.first.class.superclass.eql?(ApplicationRecord)
117
+ end
118
+ end
119
+
120
+ private_constant :Util
121
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveCharts
2
+ VERSION = '1.0.1'.freeze
3
+ end
@@ -0,0 +1,91 @@
1
+ module ActiveCharts
2
+ class XYChart < RectangularChart
3
+ OFFSET = 6
4
+
5
+ def initialize(collection, options = {})
6
+ super
7
+
8
+ section_calcs
9
+ tick_calcs
10
+ end
11
+
12
+ attr_reader :x_labels, :y_labels, :x_min, :x_max, :y_min, :y_max,
13
+ :x_label_y, :y_label_x, :x_ticks, :y_ticks, :section_width, :section_height
14
+
15
+ def side_label_text_tags
16
+ y_labels.map.with_index do |label, index|
17
+ label = formatted_val(label, data_formatters[1])
18
+
19
+ tag.text(label, x: y_label_x, y: y_tick_y(index), class: 'ac-y-label')
20
+ end.join
21
+ end
22
+
23
+ def bottom_label_text_tags
24
+ x_labels.map.with_index do |label, index|
25
+ label = formatted_val(label, data_formatters[0])
26
+ classes = 'ac-x-label'
27
+ classes += ' anchor_start' if index.zero?
28
+
29
+ tag.text(label, x: x_tick_x(index), y: x_label_y, class: classes)
30
+ end.join
31
+ end
32
+
33
+ private
34
+
35
+ def prereq_calcs
36
+ @collection = collection.map do |row|
37
+ row.map { |x, y| [Util.safe_to_dec(x), Util.safe_to_dec(y)] }
38
+ end
39
+ end
40
+
41
+ def values_calcs
42
+ @collection.flatten(1)
43
+ end
44
+
45
+ def width_calcs(values)
46
+ @grid_width = svg_width - MARGIN * 4
47
+ @x_min, @x_max, x_step = Util.scale(values.min, values.max)
48
+ @x_labels = (x_min..x_max).step(x_step)
49
+ end
50
+
51
+ def height_calcs(values)
52
+ @grid_height = svg_height - label_height * 2
53
+ @y_min, @y_max, y_step = Util.scale(values.min, values.max)
54
+ @y_labels = (y_min..y_max).step(y_step)
55
+ end
56
+
57
+ def section_calcs
58
+ @section_width = grid_width.to_d / (x_labels.count - 1)
59
+ @section_height = grid_height.to_d / (y_labels.count - 1)
60
+ end
61
+
62
+ def tick_calcs
63
+ @x_label_y = x_axis_y
64
+ @y_label_x = y_axis_x
65
+ @x_ticks = (1..x_labels.size - 2).map { |i| x_tick_x(i) }
66
+ @y_ticks = (1..y_labels.size - 2).map { |i| y_tick_y(i) }
67
+ end
68
+
69
+ def x_axis_y
70
+ grid_height + label_height * 1.5
71
+ end
72
+
73
+ def y_axis_x
74
+ grid_width + OFFSET
75
+ end
76
+
77
+ def x_tick_x(index)
78
+ section_width * index
79
+ end
80
+
81
+ def y_tick_y(index)
82
+ return label_height + TOP_LEFT_OFFSET if index.eql?(y_labels.count - 1)
83
+
84
+ (section_height * (y_labels.count - 1 - index)).round(6)
85
+ end
86
+
87
+ def label_classes
88
+ [css_class + '-label', 'ac-toggleable'].join(' ')
89
+ end
90
+ end
91
+ end