rcharts 0.1.0

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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +99 -0
  4. data/Rakefile +21 -0
  5. data/app/helpers/rcharts/graph_helper/axes/axis_element/styles.rb +91 -0
  6. data/app/helpers/rcharts/graph_helper/axes/axis_element.rb +89 -0
  7. data/app/helpers/rcharts/graph_helper/axes/label_element.rb +32 -0
  8. data/app/helpers/rcharts/graph_helper/axes/tick_element.rb +38 -0
  9. data/app/helpers/rcharts/graph_helper/axes.rb +8 -0
  10. data/app/helpers/rcharts/graph_helper/categories/bar_builder.rb +32 -0
  11. data/app/helpers/rcharts/graph_helper/categories/bar_segment_element.rb +49 -0
  12. data/app/helpers/rcharts/graph_helper/categories/bars_element.rb +52 -0
  13. data/app/helpers/rcharts/graph_helper/categories/category_builder.rb +115 -0
  14. data/app/helpers/rcharts/graph_helper/element.rb +65 -0
  15. data/app/helpers/rcharts/graph_helper/element_builder.rb +42 -0
  16. data/app/helpers/rcharts/graph_helper/graph/axes.rb +41 -0
  17. data/app/helpers/rcharts/graph_helper/graph/axis/caster.rb +45 -0
  18. data/app/helpers/rcharts/graph_helper/graph/axis/positioning.rb +66 -0
  19. data/app/helpers/rcharts/graph_helper/graph/axis/ticks.rb +66 -0
  20. data/app/helpers/rcharts/graph_helper/graph/axis.rb +86 -0
  21. data/app/helpers/rcharts/graph_helper/graph/calculator.rb +91 -0
  22. data/app/helpers/rcharts/graph_helper/graph/composition.rb +35 -0
  23. data/app/helpers/rcharts/graph_helper/graph/options.rb +33 -0
  24. data/app/helpers/rcharts/graph_helper/graph.rb +8 -0
  25. data/app/helpers/rcharts/graph_helper/graph_builder.rb +270 -0
  26. data/app/helpers/rcharts/graph_helper/legend_entry_builder.rb +46 -0
  27. data/app/helpers/rcharts/graph_helper/rule_element.rb +68 -0
  28. data/app/helpers/rcharts/graph_helper/series/area_element.rb +50 -0
  29. data/app/helpers/rcharts/graph_helper/series/path.rb +153 -0
  30. data/app/helpers/rcharts/graph_helper/series/path_element.rb +72 -0
  31. data/app/helpers/rcharts/graph_helper/series/point.rb +58 -0
  32. data/app/helpers/rcharts/graph_helper/series/scatter_element.rb +87 -0
  33. data/app/helpers/rcharts/graph_helper/series/series_builder.rb +145 -0
  34. data/app/helpers/rcharts/graph_helper/tooltips/entry_builder.rb +47 -0
  35. data/app/helpers/rcharts/graph_helper/tooltips/foreign_object_element.rb +84 -0
  36. data/app/helpers/rcharts/graph_helper/tooltips/hover_target_element.rb +39 -0
  37. data/app/helpers/rcharts/graph_helper/tooltips/marker_element.rb +38 -0
  38. data/app/helpers/rcharts/graph_helper/tooltips/tooltip_builder.rb +44 -0
  39. data/app/helpers/rcharts/graph_helper/tooltips/tooltip_element.rb +45 -0
  40. data/app/helpers/rcharts/graph_helper.rb +249 -0
  41. data/lib/generators/rcharts/install/install_generator.rb +13 -0
  42. data/lib/generators/rcharts/install/templates/rcharts.css +392 -0
  43. data/lib/rcharts/engine.rb +25 -0
  44. data/lib/rcharts/percentage.rb +36 -0
  45. data/lib/rcharts/type/percentage.rb +20 -0
  46. data/lib/rcharts/type/symbol.rb +29 -0
  47. data/lib/rcharts/type.rb +9 -0
  48. data/lib/rcharts/version.rb +6 -0
  49. data/lib/rcharts.rb +52 -0
  50. data/lib/tasks/rcharts_tasks.rake +6 -0
  51. metadata +107 -0
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Series
6
+ # :nodoc:
7
+ Point = Struct.new(:x, :y) do
8
+ def to_s = "#{x},#{y}"
9
+
10
+ def +(other) = Point.new(x + other.x, y + other.y)
11
+
12
+ def -(other) = Point.new(x - other.x, y - other.y)
13
+
14
+ def control(previous_point, next_point, smoothing: 1.0, reverse: false)
15
+ (self + handle_offset(previous_point, next_point, smoothing:, reverse:)).then do |control|
16
+ clamp(control, (reverse ? previous_point : next_point))
17
+ end
18
+ end
19
+
20
+ def incomplete?
21
+ x.nil? || y.nil?
22
+ end
23
+
24
+ def complete?
25
+ !incomplete?
26
+ end
27
+
28
+ protected
29
+
30
+ def handle_offset(previous_point, next_point, smoothing: 1.0, reverse: false)
31
+ (next_point - previous_point).handle_position(smoothing: smoothing, reverse: reverse,
32
+ extremum: extremum?(previous_point, next_point))
33
+ end
34
+
35
+ def handle_position(smoothing: 1.0, reverse: false, extremum: false)
36
+ Point.new(Math.cos(angle(reverse:)) * length(smoothing:),
37
+ extremum ? 0 : Math.sin(angle(reverse:)) * length(smoothing:))
38
+ end
39
+
40
+ private
41
+
42
+ def angle(reverse: false) = Math.atan2(y, x) + (reverse ? Math::PI : 0)
43
+
44
+ def length(smoothing: 1.0) = Math.sqrt((x**2) + (y**2)) * smoothing
45
+
46
+ def extremum?(previous_point, next_point) = (y - previous_point.y) * (next_point.y - y) <= 0
47
+
48
+ def clamp(control, limit)
49
+ Point.new(clamp_point(:x, control, limit), clamp_point(:y, control, limit))
50
+ end
51
+
52
+ def clamp_point(point, control, limit)
53
+ control[point].clamp(*[self[point], limit[point]].minmax)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Series
6
+ class ScatterElement < PathElement # :nodoc:
7
+ attribute :marker_size, :float, default: 10
8
+ attribute :marker_margin, :float, default: 2
9
+ attribute :marker_id
10
+
11
+ private
12
+
13
+ def tags
14
+ series_tag
15
+ end
16
+
17
+ def series_tag
18
+ tag.g do
19
+ definitions_tag + path_tag
20
+ end
21
+ end
22
+
23
+ def path_tag
24
+ tag.g do
25
+ points.select(&:complete?).each do |point|
26
+ concat marker_for(point)
27
+ end
28
+ end
29
+ end
30
+
31
+ def definitions_tag
32
+ tag.defs do
33
+ cross_tag unless marker_id
34
+ end
35
+ end
36
+
37
+ def cross_tag
38
+ tag.symbol id:, x:, y:, width:, height: do
39
+ line_tag + line_tag(invert_x: true)
40
+ end
41
+ end
42
+
43
+ def line_tag(invert_x: false)
44
+ tag.line x1: invert_x ? marker_size : marker_margin, x2: invert_x ? marker_margin : marker_size, y1:, y2:,
45
+ class: 'series-path'
46
+ end
47
+
48
+ def marker_for(point)
49
+ tag.use href: marker_id || "##{id}", x: Percentage.new(point.x), y: Percentage.new(point.y),
50
+ class: ['series-path', color_class]
51
+ end
52
+
53
+ def id
54
+ [id_hash, :marker].join('-')
55
+ end
56
+
57
+ def x
58
+ total_size / -2
59
+ end
60
+
61
+ def y
62
+ total_size / -2
63
+ end
64
+
65
+ def width
66
+ total_size
67
+ end
68
+
69
+ def height
70
+ total_size
71
+ end
72
+
73
+ def y1
74
+ marker_margin
75
+ end
76
+
77
+ def y2
78
+ marker_size
79
+ end
80
+
81
+ def total_size
82
+ marker_size + marker_margin
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Series
6
+ # = \Series Builder
7
+ class SeriesBuilder < ElementBuilder
8
+ ##
9
+ # :attr_accessor:
10
+ attribute :name
11
+
12
+ ##
13
+ # :attr_accessor: index
14
+ attribute :index, :integer, default: 0
15
+
16
+ attribute :composition, default: -> { Graph::Composition.new }
17
+ attribute :series_options, default: -> { {} }
18
+ attribute :id_hash, :string, default: -> { SecureRandom.hex(4) }
19
+
20
+ # Renders the series as a line plot. Passes through <tt>:data</tt>, and <tt>:aria</tt>,
21
+ # and <tt>:class</tt> options to the tag builder.
22
+ # <%= graph_for @sales do |graph| %>
23
+ # <%= graph.series do |series| %>
24
+ # <%= series.line smooth: 0.12 %>
25
+ # <% end %>
26
+ # <% end %>
27
+ #
28
+ # ==== Options
29
+ # [<tt>:smooth</tt>] Smoothing factor for the line.
30
+ # [<tt>:axis</tt>] The axis for the series. Defaults to the first continuous axis.
31
+ # [<tt>:inline_axis</tt>] The axis for the categories. Defaults to the first discrete axis.
32
+ def line(**)
33
+ path_tag(**)
34
+ end
35
+
36
+ # Renders the series as an area plot. Passes through <tt>:data</tt>, and <tt>:aria</tt>,
37
+ # and <tt>:class</tt> options to the tag builder.
38
+ # <%= graph_for @sales do |graph| %>
39
+ # <%= graph.series do |series| %>
40
+ # <%= series.area smooth: 0.12 %>
41
+ # <% end %>
42
+ # <% end %>
43
+ #
44
+ # ==== Options
45
+ # [<tt>:smooth</tt>] Smoothing factor for the area.
46
+ # [<tt>:axis</tt>] The axis for the series. Defaults to the first continuous axis.
47
+ # [<tt>:inline_axis</tt>] The axis for the categories. Defaults to the first discrete axis.
48
+ def area(**)
49
+ area_tag(sign: :positive, **) + area_tag(sign: :negative, **)
50
+ end
51
+
52
+ # Renders the series as a scatter plot. Passes through <tt>:data</tt>, and <tt>:aria</tt>,
53
+ # and <tt>:class</tt> options to the tag builder.
54
+ # <%= graph_for @sales do |graph| %>
55
+ # <%= graph.series do |series| %>
56
+ # <%= series.scatter %>
57
+ # <% end %>
58
+ # <% end %>
59
+ # ==== Options
60
+ # [<tt>:marker_size</tt>] Size of the markers. Defaults to <tt>10.0</tt>.
61
+ # [<tt>:marker_margin</tt>] Margin around the marker symbol. Defaults to <tt>2.0</tt>.
62
+ # [<tt>:marker_id</tt>] ID of a custom marker symbol (a <tt><symbol></tt> element).
63
+ # [<tt>:axis</tt>] The axis for the series. Defaults to the first continuous axis.
64
+ # [<tt>:inline_axis</tt>] The axis for the categories. Defaults to the first discrete axis.
65
+ def scatter(**)
66
+ scatter_tag(**)
67
+ end
68
+
69
+ private
70
+
71
+ def path_tag(axis: nil, inline_axis: nil, **)
72
+ resolve_axis :continuous, axis do |continuous_axis|
73
+ resolve_axis :discrete, inline_axis do |discrete_axis|
74
+ render PathElement.new(series: current_points(discrete_axis, continuous_axis),
75
+ horizontal: discrete_axis.horizontal?, series_options:, index:, id_hash:, **)
76
+ end
77
+ end
78
+ end
79
+
80
+ def area_tag(sign: nil, axis: nil, inline_axis: nil, **)
81
+ resolve_axis :continuous, axis do |continuous_axis|
82
+ resolve_axis :discrete, inline_axis do |discrete_axis|
83
+ render AreaElement.new(series: current_points(discrete_axis, continuous_axis, signed: sign),
84
+ previous_series: previous_points(discrete_axis, continuous_axis, signed: sign),
85
+ mask_series: mask_points(discrete_axis, continuous_axis),
86
+ block_position: block_position(continuous_axis),
87
+ horizontal: discrete_axis.horizontal?,
88
+ series_options:, index:, id_hash:, **)
89
+ end
90
+ end
91
+ end
92
+
93
+ def scatter_tag(axis: nil, inline_axis: nil, **)
94
+ resolve_axis :continuous, axis do |continuous_axis|
95
+ resolve_axis :discrete, inline_axis do |discrete_axis|
96
+ render ScatterElement.new(series: current_points(discrete_axis, continuous_axis),
97
+ horizontal: discrete_axis.horizontal?,
98
+ series_options:, index:, id_hash:, **)
99
+ end
100
+ end
101
+ end
102
+
103
+ def current_points(inline_axis, block_axis, signed: nil)
104
+ series(block_axis, signed:).to_h do |key, value|
105
+ [inline_axis.position_for(key), value.try { block_axis.position_for(it) }]
106
+ end
107
+ end
108
+
109
+ def previous_points(inline_axis, block_axis, signed: nil)
110
+ return [] if series(block_axis, signed:) == previous_series(block_axis, signed:)
111
+
112
+ previous_series(block_axis, signed:).to_h do |key, value|
113
+ [inline_axis.position_for(key), block_axis.position_for(value)]
114
+ end
115
+ end
116
+
117
+ def series(block_axis, signed: nil)
118
+ composition.signed(signed)
119
+ .then { block_axis.stacked? ? it.stacked : it }
120
+ .then { it[name] }
121
+ end
122
+
123
+ def previous_series(block_axis, signed: nil)
124
+ composition.signed(signed)
125
+ .then { block_axis.stacked? ? it.stacked(exclude_current: true) : it }
126
+ .then { it[name] }
127
+ end
128
+
129
+ def mask_points(inline_axis, block_axis)
130
+ composition.sum_complete.to_h do |key, value|
131
+ [inline_axis.position_for(key), value.try { block_axis.position_for(it) }]
132
+ end
133
+ end
134
+
135
+ def block_position(block_axis)
136
+ block_axis.position_for([block_axis.adjusted_minimum, 0].max)
137
+ end
138
+
139
+ def resolve_axis(type, name = nil, &)
140
+ (name ? composition.axes.fetch(*name) : composition.axes.public_send(type)).then(&)
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Tooltips
6
+ # = Tooltip Entry Builder
7
+ class EntryBuilder < ElementBuilder
8
+ ##
9
+ # :attr_accessor:
10
+ attribute :value
11
+
12
+ ##
13
+ # :attr_accessor:
14
+ attribute :name
15
+
16
+ ##
17
+ # :attr_accessor: index
18
+ attribute :index, :integer, default: 0
19
+
20
+ attribute :series_options, default: -> { {} }
21
+
22
+ # Renders the symbol associated with the series. You can override this on a per-series basis using
23
+ # <tt>:series_options</tt> with GraphHelper#graph_for.
24
+ # To change the global set of symbols and colors, see RCharts.symbol_for and RCharts.color_class_for.
25
+ # <%= graph_for @sales do |graph| %>
26
+ # <%= graph.legend do |series| %>
27
+ # <%= series.symbol %>
28
+ # <%= series.name %>
29
+ # <% end %>
30
+ # <% end %>
31
+ def symbol
32
+ tag.span symbol_character, class: ['series-symbol', color_class]
33
+ end
34
+
35
+ private
36
+
37
+ def color_class
38
+ series_options.fetch(:color_class, RCharts.color_class_for(index))
39
+ end
40
+
41
+ def symbol_character
42
+ series_options.fetch(:symbol, RCharts.symbol_for(index))
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Tooltips
6
+ class ForeignObjectElement < Element # :nodoc:
7
+ attribute :inline_position, :'rcharts/percentage', default: Percentage::MIN
8
+ attribute :inline_axis, :'rcharts/symbol'
9
+ attribute :index, :integer, default: 0
10
+ attribute :values_count, :integer, default: 0
11
+
12
+ private
13
+
14
+ def tags(&)
15
+ tag.foreignObject width:, height:, x:, y:, class: 'tooltip-object' do
16
+ container_tag(&)
17
+ end
18
+ end
19
+
20
+ def container_tag(&)
21
+ tag.div class: ['tooltip-container', { 'anchor-end' => anchor_bottom?, 'justify-end' => anchor_right? }], &
22
+ end
23
+
24
+ def x
25
+ return unless horizontal_inline_axis?
26
+
27
+ anchor_right? ? Percentage::MIN : inline_position
28
+ end
29
+
30
+ def y
31
+ return if horizontal_inline_axis?
32
+
33
+ anchor_bottom? ? Percentage::MIN : inline_position
34
+ end
35
+
36
+ def width
37
+ return Percentage::MAX unless horizontal_inline_axis?
38
+
39
+ anchor_left? ? Percentage::MAX - inline_position : inline_position
40
+ end
41
+
42
+ def height
43
+ return Percentage::MAX if horizontal_inline_axis?
44
+
45
+ anchor_top? ? Percentage::MAX - inline_position : inline_position
46
+ end
47
+
48
+ def anchor_right?
49
+ return true unless horizontal_inline_axis?
50
+
51
+ first_half? == false
52
+ end
53
+
54
+ def anchor_left?
55
+ return false unless horizontal_inline_axis?
56
+
57
+ first_half?
58
+ end
59
+
60
+ def anchor_top?
61
+ return false if horizontal_inline_axis?
62
+
63
+ first_half?
64
+ end
65
+
66
+ def anchor_bottom?
67
+ return false if horizontal_inline_axis?
68
+
69
+ first_half? == false
70
+ end
71
+
72
+ def horizontal_inline_axis?
73
+ inline_axis == :x
74
+ end
75
+
76
+ def first_half?
77
+ return if values_count.zero? # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
78
+
79
+ index < values_count / 2.0
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Tooltips
6
+ class HoverTargetElement < Element # :nodoc:
7
+ attribute :inline_position, :'rcharts/percentage', default: Percentage::MIN
8
+ attribute :inline_size, :'rcharts/percentage', default: Percentage::MIN
9
+ attribute :inline_axis, :'rcharts/symbol'
10
+
11
+ private
12
+
13
+ def tags(&)
14
+ tag.rect x:, y:, width:, height:, class: 'tooltip-hover-target'
15
+ end
16
+
17
+ def x
18
+ inline_position - (inline_size / 2) if horizontal_inline_axis?
19
+ end
20
+
21
+ def y
22
+ inline_position - (inline_size / 2) unless horizontal_inline_axis?
23
+ end
24
+
25
+ def width
26
+ horizontal_inline_axis? ? inline_size : Percentage::MAX
27
+ end
28
+
29
+ def height
30
+ horizontal_inline_axis? ? Percentage::MAX : inline_size
31
+ end
32
+
33
+ def horizontal_inline_axis?
34
+ inline_axis == :x
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Tooltips
6
+ class MarkerElement < Element # :nodoc:
7
+ attribute :inline_position, :'rcharts/percentage'
8
+ attribute :inline_axis, :'rcharts/symbol'
9
+
10
+ private
11
+
12
+ def tags(&)
13
+ tag.line x1:, x2:, y1:, y2:, class: 'tooltip-marker'
14
+ end
15
+
16
+ def x1
17
+ horizontal_inline_axis? ? inline_position : Percentage::MIN
18
+ end
19
+
20
+ def x2
21
+ horizontal_inline_axis? ? inline_position : Percentage::MAX
22
+ end
23
+
24
+ def y1
25
+ horizontal_inline_axis? ? Percentage::MIN : inline_position
26
+ end
27
+
28
+ def y2
29
+ horizontal_inline_axis? ? Percentage::MAX : inline_position
30
+ end
31
+
32
+ def horizontal_inline_axis?
33
+ inline_axis == :x
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Tooltips
6
+ # = Tooltip Builder
7
+ class TooltipBuilder < ElementBuilder
8
+ ##
9
+ # :attr_accessor:
10
+ attribute :name
11
+
12
+ attribute :values, default: -> { {} }
13
+ attribute :series_options, default: -> { {} }
14
+
15
+ # Renders one or more series present in the data. For each series yields an EntryBuilder which
16
+ # contains the index, key, and value. See EntryBuilder for details.
17
+ # <%= graph_for @annual_sales do |graph| %>
18
+ # <%= graph.tooltips do |category| %>
19
+ # <h4><%= category.name %></h4>
20
+ # <%= category.series do |series| %>
21
+ # <%= series.name %>
22
+ # <%= series.value %>
23
+ # <% end %>
24
+ # <% end %>
25
+ # <% end %>
26
+ def series(**, &)
27
+ capture do
28
+ series_options.each_key.with_index do |key, index|
29
+ concat series_tag(key, index, **, &)
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def series_tag(name, index, **, &)
37
+ tag.div(**) do
38
+ render EntryBuilder.new(name:, value: values[name], index:, series_options: series_options[name]), &
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Tooltips
6
+ class TooltipElement < Element # :nodoc:
7
+ attribute :inline_position, :'rcharts/percentage', default: Percentage::MIN
8
+ attribute :inline_size, :'rcharts/percentage', default: Percentage::MIN
9
+ attribute :inline_axis, :'rcharts/symbol'
10
+ attribute :index, :integer, default: 0
11
+ attribute :values_count, :integer, default: 0
12
+
13
+ def tags(&)
14
+ tag.g class: 'tooltip' do
15
+ hover_target_tag + marker_tag + tooltip_tag(&)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def tooltip_tag(&)
22
+ foreign_object_tag do
23
+ content_tag(&)
24
+ end
25
+ end
26
+
27
+ def foreign_object_tag(&)
28
+ render ForeignObjectElement.new(inline_position:, inline_axis:, index:, values_count:), &
29
+ end
30
+
31
+ def content_tag(&)
32
+ tag.div class: 'tooltip-content', &
33
+ end
34
+
35
+ def marker_tag
36
+ render MarkerElement.new(inline_position:, inline_axis:)
37
+ end
38
+
39
+ def hover_target_tag
40
+ render HoverTargetElement.new(inline_position:, inline_size:, inline_axis:)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end