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,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ # = \Element
6
+ #
7
+ # The base class for all elements to inherit from. Elements are designed for cases where a tag helper alone
8
+ # would be unwieldy because of the complexity of determining content or attribute values e.g. for SVG positioning
9
+ # attributes like <tt>x</tt> and <tt>y</tt>.
10
+ #
11
+ # To ensure rendering yields a predictable result every time, don't mutate element state as part of the rendering
12
+ # process. If you need to track and mutate state, use ElementBuilder instead, which is designed for this situation.
13
+ #
14
+ # By default, rendering an element produces no content. Subclasses should implement their own private #tags method
15
+ # to return actual content when rendered. All helpers and objects accessible in the parent view context are also
16
+ # accessible within #tags, so you can use tag helpers and other helpers to build markup.
17
+ #
18
+ # To pass a value to an element, first declare an attribute using the ActiveModel::Attributes API
19
+ # and then pass the value to the element's initializer (or use one of the other methods to set attributes).
20
+ # Already declared attributes are #id, #class_names (with writer alias #class=), #data, and #aria.
21
+ class Element
22
+ include ActiveModel::API
23
+ include ActiveModel::Attributes
24
+
25
+ class_attribute :view_context
26
+
27
+ delegate :render, :tag, to: :view_context
28
+ delegate_missing_to :view_context
29
+
30
+ ##
31
+ # :attr_accessor: id
32
+ attribute :id, :string
33
+
34
+ ##
35
+ # :attr_accessor:
36
+ attribute :class_names
37
+
38
+ ##
39
+ # :attr_writer: class
40
+
41
+ ##
42
+ # :attr_accessor:
43
+ attribute :data, default: -> { {} }
44
+
45
+ ##
46
+ # :attr_accessor:
47
+ attribute :aria, default: -> { {} }
48
+
49
+ alias class= class_names= # :nodoc:
50
+
51
+ def render_in(view_context, &)
52
+ with view_context: do
53
+ tags(&)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # :doc:
60
+ def tags
61
+ ''.html_safe
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ # = \Element Builder
6
+ #
7
+ # Like form builders, but for elements. Concrete builders should inherit from this class.
8
+ # Use this when you need to provide a way for users to choose between different options at rendering time and track
9
+ # rendering state (e.g. to do with iteration).
10
+ #
11
+ # Rendering a builder shouldn't itself render anything. Instead, rendering the builder with a block means that,
12
+ # just as with a form builder, the block will be called with an instance of the builder.
13
+ # That means within the block, users can then call methods on the builder, which may output markup to the buffer.
14
+ # If you need a container element, like the <tt><form></tt> associated with a form builder,
15
+ # keep that separate from the builder.
16
+ #
17
+ # The builder is the place to store information about the state of rendering independent of particular elements.
18
+ # One example is iteration, where users may need to be able to specify different markup for each individual item
19
+ # in the collection. You can allow this by rendering a builder with each object in the collection, declaring
20
+ # an index attribute on the builder to allow different behavior according to the position of the item:
21
+ # MyElementBuilder < RCharts::GraphHelper::ElementBuilder
22
+ # attribute :index, :integer
23
+ # end
24
+ #
25
+ # Like elements, builders set and delegate to the view context.
26
+ class ElementBuilder
27
+ include ActiveModel::API
28
+ include ActiveModel::Attributes
29
+
30
+ class_attribute :view_context
31
+
32
+ delegate :render, :tag, to: :view_context
33
+ delegate_missing_to :view_context
34
+
35
+ def render_in(view_context, &)
36
+ with view_context: do
37
+ block_given? ? yield(self) : ''.html_safe
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Graph
6
+ class Axes # :nodoc:
7
+ delegate :[], to: :axes
8
+
9
+ def initialize(graphable = {}, axis_options = {})
10
+ @axes = Options.new(graphable.keys, axis_options).to_h do |name, axes|
11
+ [name, axes.collect { |index, options| build_axis(graphable:, name:, index:, **options) }]
12
+ end
13
+ end
14
+
15
+ def fetch(name, index = 0)
16
+ axes.fetch(name) { raise ArgumentError, "Unknown axis #{name}" }
17
+ .fetch(index) { raise ArgumentError, "Unknown index #{index} for axis #{name}" }
18
+ end
19
+
20
+ def discrete
21
+ fetch(:x).then { it.discrete? && it } || axes.values.flatten.find(&:discrete?) || fetch(:x)
22
+ end
23
+
24
+ def continuous
25
+ fetch(:y).then { !it.discrete? && it } || axes.values.flatten.find { !it.discrete? } || fetch(:y)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :axes
31
+
32
+ def build_axis(graphable:, name:, index:, **options)
33
+ discrete = case options[:values_method].to_proc.call(graphable).first
34
+ when String, Symbol then :categorical
35
+ end
36
+ Axis.new(graphable:, name:, index:, discrete:, **options)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Graph
6
+ class Axis
7
+ class Caster # :nodoc:
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+
12
+ def casting
13
+ upcast(yield downcast)
14
+ end
15
+
16
+ def downcast
17
+ case value
18
+ when Time then value.to_i
19
+ when Date then value.jd + value.day_fraction
20
+ else value
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :value
27
+
28
+ def upcast(raw)
29
+ case value
30
+ when ActiveSupport::TimeWithZone then value.time_zone.at(raw)
31
+ when Time then Time.at(raw, in: value_zone)
32
+ when DateTime then DateTime.jd(0, 0, 0, 0, value.offset, value.start) + raw
33
+ when Date then Date.jd(raw, value.start)
34
+ else raw
35
+ end
36
+ end
37
+
38
+ def value_zone
39
+ value.zone.try { ActiveSupport::TimeZone[it] } || value.utc_offset
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Graph
6
+ class Axis
7
+ module Positioning # :nodoc:
8
+ extend ActiveSupport::Concern
9
+ include Ticks
10
+
11
+ included do
12
+ def ticks
13
+ return {} unless values.any?
14
+
15
+ @ticks ||= tick_count.succ.times.to_h { [position_at(it), value_at(it)] }
16
+ end
17
+
18
+ def position_at(index)
19
+ return if index > tick_count
20
+
21
+ ((index * (100.0 / (tick_count.nonzero? || 1))) + tick_offset.to_f) * (100 / (100 + (2 * tick_offset.to_f)))
22
+ end
23
+
24
+ def value_at(index)
25
+ return if index > tick_count || index.negative? || values.empty?
26
+ return values.dig(index, 0) if discrete || values.dig(0, 0).is_a?(String)
27
+
28
+ adjusted_minimum + (index * tick_interval)
29
+ end
30
+
31
+ def length_between(min, max)
32
+ return position_for(0 - max) - position_for(0 - min) if max.negative?
33
+
34
+ (position_for(max) - position_for(min))
35
+ end
36
+
37
+ def position_for(value)
38
+ return values.flatten.index(value).try { position_at(it) } if discrete == :categorical
39
+ return 0 if adjusted_maximum == adjusted_minimum
40
+
41
+ downcasted_position_for(value)
42
+ end
43
+
44
+ private
45
+
46
+ def downcasted_position_for(value)
47
+ ((Caster.new(value).downcast.to_f - casted_adjusted_minimum) / casted_range) * 100
48
+ end
49
+
50
+ def casted_range
51
+ casted_adjusted_maximum - casted_adjusted_minimum
52
+ end
53
+
54
+ def casted_adjusted_maximum
55
+ Caster.new(adjusted_maximum).downcast
56
+ end
57
+
58
+ def casted_adjusted_minimum
59
+ Caster.new(adjusted_minimum).downcast
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Graph
6
+ class Axis
7
+ module Ticks # :nodoc:
8
+ INTERVAL_FACTORS = [1, 2, 5, 10, 30].freeze
9
+ INTERVAL_BASES = ActiveSupport::Duration::PARTS_IN_SECONDS
10
+ ALL_INTERVALS = INTERVAL_BASES.values.flat_map { |base| INTERVAL_FACTORS.collect { base * it } }
11
+
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ def tick_count
16
+ return values_count.pred if discrete?
17
+
18
+ ((maximum - adjusted_minimum) / tick_interval).ceil
19
+ end
20
+
21
+ def adjusted_minimum
22
+ @adjusted_minimum ||= Caster.new(minimum).casting { (it / tick_interval).floor * tick_interval }
23
+ end
24
+
25
+ private
26
+
27
+ def interval
28
+ ((maximum + adjustment_for_empty_interval) - (minimum - adjustment_for_empty_interval)) / 10.0
29
+ end
30
+
31
+ def adjustment_for_empty_interval
32
+ return 0 if minimum != maximum
33
+
34
+ (maximum.nil? || maximum.zero? ? 1.0 : (maximum.abs * 0.1)) * 0.5
35
+ end
36
+
37
+ def tick_interval
38
+ return 0 if interval <= 0
39
+ return ALL_INTERVALS.min_by { (it - interval).abs } if minimum.is_a?(Time)
40
+
41
+ tick_base * case (interval / tick_base)
42
+ when ..1 then 1
43
+ when 1..2 then 2
44
+ when 2..2.5 then 2.5
45
+ when 2.5..5 then 5
46
+ else 10
47
+ end
48
+ end
49
+
50
+ def tick_base
51
+ 10**BigDecimal(Math.log10(interval).floor)
52
+ end
53
+
54
+ def tick_offset
55
+ (100.0 / values_count) / 2 if categorical?
56
+ end
57
+
58
+ def adjusted_maximum
59
+ @adjusted_maximum ||= adjusted_minimum + (tick_count * tick_interval)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Graph
6
+ class Axis # :nodoc:
7
+ include ActiveModel::API
8
+ include ActiveModel::Attributes
9
+
10
+ include Ticks
11
+ include Positioning
12
+
13
+ attribute :name, :'rcharts/symbol'
14
+ attribute :index, :integer
15
+ attribute :graphable
16
+ attribute :values_method, default: :values
17
+ attribute :minimum
18
+ attribute :maximum
19
+ attribute :discrete
20
+ attribute :stacked, :boolean, default: false
21
+
22
+ alias stacked? stacked
23
+
24
+ def discrete?
25
+ discrete.present?
26
+ end
27
+
28
+ def categorical?
29
+ discrete == :categorical
30
+ end
31
+
32
+ def horizontal?
33
+ name == :x
34
+ end
35
+
36
+ def vertical?
37
+ name == :y
38
+ end
39
+
40
+ private
41
+
42
+ delegate :count, to: :values, prefix: true, private: true
43
+
44
+ def minimum
45
+ super || (self.minimum = minima.min) || 0
46
+ end
47
+
48
+ def maximum
49
+ super || (self.maximum = maxima.max) || 0
50
+ end
51
+
52
+ def minmax
53
+ [minimum, maximum]
54
+ end
55
+
56
+ def keys
57
+ graphable.keys
58
+ end
59
+
60
+ def values
61
+ @values ||= graphable.then(&values_method).collect { it.is_a?(Hash) ? it.values : Array.wrap(it) }
62
+ end
63
+
64
+ def minima
65
+ values.filter_map do |value|
66
+ value.reject { it.try(:positive?) }
67
+ .compact
68
+ .presence
69
+ .try(&(stacked? ? :sum : :min))
70
+ .then { it || value.compact.min }
71
+ end
72
+ end
73
+
74
+ def maxima
75
+ values.filter_map do |value|
76
+ value.reject { it.try(:negative?) }
77
+ .compact
78
+ .presence
79
+ .try(&(stacked? ? :sum : :max))
80
+ .then { it || value.compact.max }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Graph
6
+ class Calculator # :nodoc:
7
+ delegate :[], :dig, to: :to_h
8
+
9
+ def initialize(values = {})
10
+ @values = values
11
+ @chain = []
12
+ @selections = {}
13
+ end
14
+
15
+ def sum_complete
16
+ values.transform_values do |value|
17
+ case value
18
+ when Hash then value.values.any?(nil) ? nil : value.values.sum
19
+ when Array then value.any?(nil) ? nil : value.sum
20
+ else value
21
+ end
22
+ end
23
+ end
24
+
25
+ def signed(sign = nil)
26
+ return self unless sign
27
+
28
+ apply sign do
29
+ values_for_predicate sign == :positive ? :positive? : :negative?
30
+ end
31
+ end
32
+
33
+ def stacked(exclude_current: false)
34
+ apply :stacked, exclude_current do
35
+ collect_sum inclusive: !exclude_current
36
+ end
37
+ end
38
+
39
+ def to_h
40
+ keys.index_with do |key|
41
+ next values if key.nil?
42
+
43
+ values.transform_values { it[key] }
44
+ end
45
+ end
46
+
47
+ protected
48
+
49
+ attr_accessor :values, :chain
50
+
51
+ def apply!(*operation)
52
+ selections[[*chain, *operation]] ||= yield if block_given?
53
+ self.chain = chain.dup
54
+ chain.append(*operation)
55
+ self.values = selections[chain]
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :selections
61
+
62
+ def apply(*operation, &)
63
+ selections[[*chain, *operation]] ||= yield if block_given?
64
+ dup.tap { it.apply!(*operation) }
65
+ end
66
+
67
+ def keys
68
+ case values.values.first
69
+ in Hash => value then value.keys
70
+ in Array => value then (0...value.size).to_a
71
+ else [nil]
72
+ end
73
+ end
74
+
75
+ def values_for_predicate(predicate)
76
+ values.deep_transform_values { it.try(predicate) == false ? nil : it }
77
+ end
78
+
79
+ def collect_sum(inclusive: true)
80
+ values.transform_values do |value|
81
+ value.keys.index_with do |key|
82
+ value.keys.index(key).then do |limit|
83
+ value.values.slice(inclusive ? 0..limit : 0...limit).compact.presence&.sum
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Graph
6
+ class Composition # :nodoc:
7
+ attr_reader :axes
8
+
9
+ delegate :sum_complete, :signed, :stacked, to: :calculator
10
+ delegate :keys, to: :data
11
+
12
+ def initialize(graphable = {}, axis_options = {})
13
+ @data = graphable
14
+ @axes = Axes.new(graphable, axis_options)
15
+ end
16
+
17
+ def values
18
+ @values ||= case data.values.first
19
+ in Hash then data
20
+ in Array then data.transform_values { it.index_with.with_index { |_, index| index } }
21
+ else data.transform_values { { nil => it } }
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :data
28
+
29
+ def calculator
30
+ @calculator ||= Calculator.new(data)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Graph
6
+ class Options # :nodoc:
7
+ DEFAULTS = { x: { 0 => { values_method: :keys } }, y: { 0 => { values_method: :values } } }.freeze
8
+
9
+ def initialize(keys, options)
10
+ @keys = keys
11
+ @options = options
12
+ end
13
+
14
+ def to_h(&)
15
+ DEFAULTS.deep_merge(normalized_options).to_h(&)
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :keys, :options
21
+
22
+ def normalized_options
23
+ options.transform_values do |value|
24
+ next value.each_with_index.to_h { |item, index| [index, item] } if value.is_a?(Array)
25
+ next value if value.is_a?(Hash) && value.keys.first.is_a?(Integer)
26
+
27
+ { 0 => value }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Graph # :nodoc:
6
+ end
7
+ end
8
+ end