rcharts 0.1.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 256aeff9d84f681b225b1a35cd613367c65e2406caf8ba1815214e690dc462a1
4
- data.tar.gz: a2de1b9873a98beaa5e7d0cec25042a8a59d36ed6998d9f8005f48f9eb20e4c3
3
+ metadata.gz: 15185c259bd39b4d6a998939a98356bf1513693e521ed693cb80907f8aab593e
4
+ data.tar.gz: acdb9c9f5f0d2c8336b22e78f7f49f3868485b112bb652e854f34bfeacdcd3f3
5
5
  SHA512:
6
- metadata.gz: 531cd70794162b29f5b87c79ef64ea87ceb43f6b60d5f2967dfff5cbb723d9315084615bb353324fdc80c25d3f6dffea42913ac1254dd48ff89aba0b49cd80c2
7
- data.tar.gz: 907ab23d72f376f6c8f15584ce4e54bf0386d7e2d85affb9fa0576ec01374866c04db8bd0a1c1afad98ede4b94f7ddfc6f698bad1e65889fdc866d38b9786aff
6
+ metadata.gz: 5a8e3de34d0ec122623157e9f6334c46185403b93949d541e7184841e0ded9e7479dd7b09961c26c51e71c0eceed39e72cd24974252ab42bb937c03c11dac8a7
7
+ data.tar.gz: 4c83d7996aedaab2a5442c1b299ed020d5c8ff2e5ebdf16646e5d0304a12591ff2f9d6c2c245e917cd1dd4141599e1cdbeed567efcb050d65244c65d5c7cbfe2
@@ -5,6 +5,8 @@ module RCharts
5
5
  module Graph
6
6
  class Axis
7
7
  class Caster # :nodoc:
8
+ EPOCH_OFFSET = Time.at(0, in: 'Z').to_date.jd
9
+
8
10
  def initialize(value)
9
11
  @value = value
10
12
  end
@@ -16,7 +18,7 @@ module RCharts
16
18
  def downcast
17
19
  case value
18
20
  when Time then value.to_i
19
- when Date then value.jd + value.day_fraction
21
+ when Date then julian_days.to_i
20
22
  else value
21
23
  end
22
24
  end
@@ -28,13 +30,26 @@ module RCharts
28
30
  def upcast(raw)
29
31
  case value
30
32
  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)
33
+ when Time then value.class.at(raw, in: value_zone)
34
+ when Date then upcast_date(raw)
34
35
  else raw
35
36
  end
36
37
  end
37
38
 
39
+ def upcast_date(raw)
40
+ raw.seconds.in_days.divmod(1).then do |days, fraction|
41
+ value.class.jd(EPOCH_OFFSET + days, *datetime_args, value.start) + fraction
42
+ end
43
+ end
44
+
45
+ def datetime_args
46
+ [0, 0, 0, value.offset] if value.is_a?(DateTime)
47
+ end
48
+
49
+ def julian_days
50
+ ((value.jd - EPOCH_OFFSET) + (value.day_fraction - value.try(:offset).to_f)).days
51
+ end
52
+
38
53
  def value_zone
39
54
  value.zone.try { ActiveSupport::TimeZone[it] } || value.utc_offset
40
55
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCharts
4
+ module GraphHelper
5
+ module Graph
6
+ class Axis
7
+ class Domain # :nodoc:
8
+ FLOOR_ALIGNMENT = { ...(2.days.to_i) => -> { it.beginning_of_day },
9
+ (2.days.to_i)...(2.weeks.to_i) => -> { it.beginning_of_week },
10
+ (2.weeks.to_i)...(2.months.to_i) => -> { it.beginning_of_month },
11
+ (2.months.to_i)...(4.months.to_i) => -> { it.beginning_of_quarter },
12
+ (4.months.to_i)...(10.months.to_i) => -> { it.beginning_of_year.advance(months: it.month < 7 ? 0 : 6) },
13
+ (10.months.to_i)... => -> { it.beginning_of_year } }.freeze
14
+ MODES = %i[exact rounded].freeze
15
+
16
+ def initialize(minimum:, maximum:, tick_interval:, mode:)
17
+ @minimum = minimum
18
+ @maximum = maximum
19
+ @tick_interval = tick_interval
20
+ @mode = mode.presence_in(MODES) || raise(ArgumentError, "mode must be one of #{MODES.collect(&:inspect).join(', ')}")
21
+ end
22
+
23
+ def domain_minimum
24
+ return minimum if exact?
25
+
26
+ adjusted_minimum
27
+ end
28
+
29
+ def domain_maximum
30
+ return maximum if exact?
31
+
32
+ adjusted_maximum
33
+ end
34
+
35
+ def exact?
36
+ mode == :exact
37
+ end
38
+
39
+ def rounded?
40
+ mode == :rounded
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :minimum, :maximum, :tick_interval, :mode
46
+
47
+ def adjusted_minimum
48
+ @adjusted_minimum ||= calculate_adjusted_minimum
49
+ end
50
+
51
+ def calculate_adjusted_minimum
52
+ return minimum if tick_interval.zero?
53
+ return calendar_boundary_for(minimum, tick_interval) if temporal_data?
54
+
55
+ Caster.new(minimum).casting { (it / tick_interval).floor * tick_interval }
56
+ end
57
+
58
+ def adjusted_maximum
59
+ @adjusted_maximum ||= adjusted_minimum + (tick_count * tick_interval)
60
+ end
61
+
62
+ def tick_count
63
+ @tick_count ||= tick_interval.zero? ? 0 : (tick_count_range / tick_interval).ceil
64
+ end
65
+
66
+ def tick_count_range
67
+ temporal_data? ? (maximum.to_time - adjusted_minimum.to_time).abs : (maximum - adjusted_minimum).to_f
68
+ end
69
+
70
+ def temporal_data?
71
+ minimum.acts_like?(:time) || minimum.acts_like?(:date)
72
+ end
73
+
74
+ def calendar_boundary_for(value, interval)
75
+ FLOOR_ALIGNMENT.detect { |range, _| range.cover?(interval) }
76
+ .last
77
+ .call(value.to_time)
78
+ .then { value.acts_like?(:date) ? it.to_date : it }
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -5,6 +5,18 @@ module RCharts
5
5
  module Graph
6
6
  class Axis
7
7
  module Positioning # :nodoc:
8
+ QUANTIZATION = { ...(5.days) => :adjusted_minimum,
9
+ (5.days)...(2.weeks) => :week_start,
10
+ (2.weeks)...(2.months) => :month_start,
11
+ (2.months)...(4.months) => :quarter_start,
12
+ (4.months)...(10.months) => :half_year_start,
13
+ (10.months)... => :year_start }.freeze
14
+ ROUNDING = { ...(1.day) => -> { it },
15
+ (1.day)...(7.days) => -> { it.in_days.round.days },
16
+ (7.days)...(1.month) => -> { it.in_weeks.round.weeks },
17
+ (1.month)...(1.year) => -> { it.in_months.round.months },
18
+ (1.year)... => -> { it.in_years.round.years } }.freeze
19
+
8
20
  extend ActiveSupport::Concern
9
21
  include Ticks
10
22
 
@@ -12,20 +24,16 @@ module RCharts
12
24
  def ticks
13
25
  return {} unless values.any?
14
26
 
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)))
27
+ @ticks ||= Array.new(tick_count.succ) { value_at(it) }
28
+ .reject { it.nil? || it > adjusted_maximum }
29
+ .index_by { position_for(it) }
22
30
  end
23
31
 
24
32
  def value_at(index)
25
33
  return if index > tick_count || index.negative? || values.empty?
26
- return values.dig(index, 0) if discrete || values.dig(0, 0).is_a?(String)
34
+ return values.dig(index, 0) if discrete
27
35
 
28
- adjusted_minimum + (index * tick_interval)
36
+ quantized_adjusted_minimum + (index * rounded_tick_interval)
29
37
  end
30
38
 
31
39
  def length_between(min, max)
@@ -41,22 +49,67 @@ module RCharts
41
49
  downcasted_position_for(value)
42
50
  end
43
51
 
52
+ def position_at(index)
53
+ return if index > tick_count
54
+
55
+ (tick_offset.to_f + raw_position_at(index))
56
+ end
57
+
44
58
  private
45
59
 
60
+ def raw_position_at(index)
61
+ index * (100.0 / ((categorical? ? values_count.nonzero? : tick_count.nonzero?) || 1))
62
+ end
63
+
46
64
  def downcasted_position_for(value)
47
- ((Caster.new(value).downcast.to_f - casted_adjusted_minimum) / casted_range) * 100
65
+ ((Caster.new(value).downcast.to_f - Caster.new(adjusted_minimum).downcast) / casted_range) * 100
48
66
  end
49
67
 
50
68
  def casted_range
51
- casted_adjusted_maximum - casted_adjusted_minimum
69
+ Caster.new(adjusted_maximum).downcast - Caster.new(adjusted_minimum).downcast
52
70
  end
53
71
 
54
- def casted_adjusted_maximum
55
- Caster.new(adjusted_maximum).downcast
72
+ def rounded_tick_interval
73
+ return tick_interval unless temporal_data?
74
+
75
+ ROUNDING.detect { |range, _value| range.cover?(tick_interval) }
76
+ .last
77
+ .call(tick_interval)
78
+ end
79
+
80
+ def quantized_adjusted_minimum
81
+ return adjusted_minimum unless temporal_data?
82
+
83
+ QUANTIZATION.detect { |range, _value| range.cover?(tick_interval) }
84
+ .last
85
+ .then { send(it) }
86
+ .then { tick_interval < 1.day ? it.to_time : it }
87
+ end
88
+
89
+ def week_start
90
+ adjusted_minimum.days_to_week_start.zero? ? adjusted_minimum.beginning_of_week : adjusted_minimum.next_week
91
+ end
92
+
93
+ def month_start
94
+ adjusted_minimum.mday == 1 ? adjusted_minimum.beginning_of_month : adjusted_minimum.next_month.beginning_of_month
95
+ end
96
+
97
+ def quarter_start
98
+ if adjusted_minimum.yday == adjusted_minimum.beginning_of_quarter.yday
99
+ adjusted_minimum.beginning_of_quarter
100
+ else
101
+ adjusted_minimum.next_quarter.beginning_of_quarter
102
+ end
103
+ end
104
+
105
+ def half_year_start
106
+ return adjusted_minimum if adjusted_minimum.month.modulo(6) == 1 && adjusted_minimum.mday == 1
107
+
108
+ adjusted_minimum.beginning_of_year.advance(months: adjusted_minimum.month < 7 ? 6 : 12)
56
109
  end
57
110
 
58
- def casted_adjusted_minimum
59
- Caster.new(adjusted_minimum).downcast
111
+ def year_start
112
+ adjusted_minimum.yday == 1 ? adjusted_minimum.beginning_of_year : adjusted_minimum.next_year.beginning_of_year
60
113
  end
61
114
  end
62
115
  end
@@ -5,27 +5,56 @@ module RCharts
5
5
  module Graph
6
6
  class Axis
7
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 } }
8
+ DECIMAL_INTERVALS = [1, 2, 2.5, 5, 10].freeze
9
+ TEMPORAL_INTERVALS = [1.second, 2.seconds, 5.seconds, 10.seconds, 15.seconds, 30.seconds,
10
+ 1.minute, 2.minutes, 5.minutes, 10.minutes, 15.minutes, 30.minutes,
11
+ 1.hour, 2.hours, 3.hours, 6.hours, 12.hours,
12
+ 1.day, 2.days,
13
+ 1.week, 2.weeks,
14
+ 1.month, 2.months, 3.months, 6.months,
15
+ 1.year, 2.years, 5.years, 10.years, 20.years, 50.years, 100.years].freeze
16
+ TARGET_TICK_COUNT = 10.0
11
17
 
12
18
  extend ActiveSupport::Concern
13
19
 
14
20
  included do
15
21
  def tick_count
16
- return values_count.pred if discrete?
22
+ return values_count.pred if categorical?
23
+ return values_count if discrete?
17
24
 
18
- ((maximum - adjusted_minimum) / tick_interval).ceil
25
+ (range / tick_interval.to_f).ceil
19
26
  end
20
27
 
21
28
  def adjusted_minimum
22
- @adjusted_minimum ||= Caster.new(minimum).casting { (it / tick_interval).floor * tick_interval }
29
+ @adjusted_minimum ||= domain.domain_minimum
30
+ end
31
+
32
+ def adjusted_maximum
33
+ @adjusted_maximum ||= domain.domain_maximum
34
+ end
35
+
36
+ def domain
37
+ @domain ||= Domain.new(minimum:, maximum:, tick_interval: tick_interval.to_f,
38
+ mode: ((values_method == :keys && temporal_data?) || discrete? ? :exact : :rounded))
23
39
  end
24
40
 
25
41
  private
26
42
 
27
43
  def interval
28
- ((maximum + adjustment_for_empty_interval) - (minimum - adjustment_for_empty_interval)) / 10.0
44
+ interval_range / TARGET_TICK_COUNT
45
+ end
46
+
47
+ def range
48
+ return (maximum.to_time - adjusted_minimum.to_time).abs if temporal_data?
49
+
50
+ maximum - adjusted_minimum
51
+ end
52
+
53
+ def interval_range
54
+ return (maximum ? 1 : 0) if categorical?
55
+ return (maximum.to_time - minimum.to_time).abs if temporal_data?
56
+
57
+ (maximum + adjustment_for_empty_interval) - (minimum - adjustment_for_empty_interval)
29
58
  end
30
59
 
31
60
  def adjustment_for_empty_interval
@@ -36,15 +65,13 @@ module RCharts
36
65
 
37
66
  def tick_interval
38
67
  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
68
+ return TEMPORAL_INTERVALS.min_by { (it - interval).abs } if temporal_data?
69
+
70
+ tick_base * DECIMAL_INTERVALS.min_by { |n| n < (interval / tick_base) ? Float::INFINITY : n }
71
+ end
72
+
73
+ def temporal_data?
74
+ minimum.acts_like?(:time) || minimum.acts_like?(:date)
48
75
  end
49
76
 
50
77
  def tick_base
@@ -54,10 +81,6 @@ module RCharts
54
81
  def tick_offset
55
82
  (100.0 / values_count) / 2 if categorical?
56
83
  end
57
-
58
- def adjusted_maximum
59
- @adjusted_maximum ||= adjusted_minimum + (tick_count * tick_interval)
60
- end
61
84
  end
62
85
  end
63
86
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RCharts
4
- # Fake comment to trigger workflow
5
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
6
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rcharts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Malčić
@@ -49,6 +49,7 @@ files:
49
49
  - app/helpers/rcharts/graph_helper/graph/axes.rb
50
50
  - app/helpers/rcharts/graph_helper/graph/axis.rb
51
51
  - app/helpers/rcharts/graph_helper/graph/axis/caster.rb
52
+ - app/helpers/rcharts/graph_helper/graph/axis/domain.rb
52
53
  - app/helpers/rcharts/graph_helper/graph/axis/positioning.rb
53
54
  - app/helpers/rcharts/graph_helper/graph/axis/ticks.rb
54
55
  - app/helpers/rcharts/graph_helper/graph/calculator.rb
@@ -101,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
102
  - !ruby/object:Gem::Version
102
103
  version: '0'
103
104
  requirements: []
104
- rubygems_version: 3.6.9
105
+ rubygems_version: 4.0.3
105
106
  specification_version: 4
106
107
  summary: Responsive SVG charting for Action View.
107
108
  test_files: []