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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.simplecov +3 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +186 -0
- data/Rakefile +8 -0
- data/active_charts.gemspec +37 -0
- data/app/assets/javascripts/.keep +0 -0
- data/app/assets/stylesheets/.keep +0 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/active_charts +3 -0
- data/lib/active_charts.rb +27 -0
- data/lib/active_charts/bar_chart.rb +94 -0
- data/lib/active_charts/chart.rb +121 -0
- data/lib/active_charts/cli.rb +12 -0
- data/lib/active_charts/generators/assets.rb +25 -0
- data/lib/active_charts/generators/templates/active_charts.css.scss +136 -0
- data/lib/active_charts/generators/templates/active_charts.js +86 -0
- data/lib/active_charts/helpers.rb +9 -0
- data/lib/active_charts/helpers/bar_chart_helper.rb +20 -0
- data/lib/active_charts/helpers/collection_parser.rb +62 -0
- data/lib/active_charts/helpers/line_chart_helper.rb +23 -0
- data/lib/active_charts/helpers/scatter_plot_helper.rb +23 -0
- data/lib/active_charts/line_chart.rb +61 -0
- data/lib/active_charts/rectangular_chart.rb +64 -0
- data/lib/active_charts/scatter_plot.rb +55 -0
- data/lib/active_charts/util.rb +121 -0
- data/lib/active_charts/version.rb +3 -0
- data/lib/active_charts/xy_chart.rb +91 -0
- metadata +234 -0
@@ -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,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
|