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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +99 -0
- data/Rakefile +21 -0
- data/app/helpers/rcharts/graph_helper/axes/axis_element/styles.rb +91 -0
- data/app/helpers/rcharts/graph_helper/axes/axis_element.rb +89 -0
- data/app/helpers/rcharts/graph_helper/axes/label_element.rb +32 -0
- data/app/helpers/rcharts/graph_helper/axes/tick_element.rb +38 -0
- data/app/helpers/rcharts/graph_helper/axes.rb +8 -0
- data/app/helpers/rcharts/graph_helper/categories/bar_builder.rb +32 -0
- data/app/helpers/rcharts/graph_helper/categories/bar_segment_element.rb +49 -0
- data/app/helpers/rcharts/graph_helper/categories/bars_element.rb +52 -0
- data/app/helpers/rcharts/graph_helper/categories/category_builder.rb +115 -0
- data/app/helpers/rcharts/graph_helper/element.rb +65 -0
- data/app/helpers/rcharts/graph_helper/element_builder.rb +42 -0
- data/app/helpers/rcharts/graph_helper/graph/axes.rb +41 -0
- data/app/helpers/rcharts/graph_helper/graph/axis/caster.rb +45 -0
- data/app/helpers/rcharts/graph_helper/graph/axis/positioning.rb +66 -0
- data/app/helpers/rcharts/graph_helper/graph/axis/ticks.rb +66 -0
- data/app/helpers/rcharts/graph_helper/graph/axis.rb +86 -0
- data/app/helpers/rcharts/graph_helper/graph/calculator.rb +91 -0
- data/app/helpers/rcharts/graph_helper/graph/composition.rb +35 -0
- data/app/helpers/rcharts/graph_helper/graph/options.rb +33 -0
- data/app/helpers/rcharts/graph_helper/graph.rb +8 -0
- data/app/helpers/rcharts/graph_helper/graph_builder.rb +270 -0
- data/app/helpers/rcharts/graph_helper/legend_entry_builder.rb +46 -0
- data/app/helpers/rcharts/graph_helper/rule_element.rb +68 -0
- data/app/helpers/rcharts/graph_helper/series/area_element.rb +50 -0
- data/app/helpers/rcharts/graph_helper/series/path.rb +153 -0
- data/app/helpers/rcharts/graph_helper/series/path_element.rb +72 -0
- data/app/helpers/rcharts/graph_helper/series/point.rb +58 -0
- data/app/helpers/rcharts/graph_helper/series/scatter_element.rb +87 -0
- data/app/helpers/rcharts/graph_helper/series/series_builder.rb +145 -0
- data/app/helpers/rcharts/graph_helper/tooltips/entry_builder.rb +47 -0
- data/app/helpers/rcharts/graph_helper/tooltips/foreign_object_element.rb +84 -0
- data/app/helpers/rcharts/graph_helper/tooltips/hover_target_element.rb +39 -0
- data/app/helpers/rcharts/graph_helper/tooltips/marker_element.rb +38 -0
- data/app/helpers/rcharts/graph_helper/tooltips/tooltip_builder.rb +44 -0
- data/app/helpers/rcharts/graph_helper/tooltips/tooltip_element.rb +45 -0
- data/app/helpers/rcharts/graph_helper.rb +249 -0
- data/lib/generators/rcharts/install/install_generator.rb +13 -0
- data/lib/generators/rcharts/install/templates/rcharts.css +392 -0
- data/lib/rcharts/engine.rb +25 -0
- data/lib/rcharts/percentage.rb +36 -0
- data/lib/rcharts/type/percentage.rb +20 -0
- data/lib/rcharts/type/symbol.rb +29 -0
- data/lib/rcharts/type.rb +9 -0
- data/lib/rcharts/version.rb +6 -0
- data/lib/rcharts.rb +52 -0
- data/lib/tasks/rcharts_tasks.rake +6 -0
- metadata +107 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 256aeff9d84f681b225b1a35cd613367c65e2406caf8ba1815214e690dc462a1
|
|
4
|
+
data.tar.gz: a2de1b9873a98beaa5e7d0cec25042a8a59d36ed6998d9f8005f48f9eb20e4c3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 531cd70794162b29f5b87c79ef64ea87ceb43f6b60d5f2967dfff5cbb723d9315084615bb353324fdc80c25d3f6dffea42913ac1254dd48ff89aba0b49cd80c2
|
|
7
|
+
data.tar.gz: 907ab23d72f376f6c8f15584ce4e54bf0386d7e2d85affb9fa0576ec01374866c04db8bd0a1c1afad98ede4b94f7ddfc6f698bad1e65889fdc866d38b9786aff
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Justin Malčić
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# RCharts
|
|
2
|
+
|
|
3
|
+
RCharts is a charting library for Action View which produces JS-agnostic SVG charts.
|
|
4
|
+
|
|
5
|
+
In the time since popular JS charting libraries were created, significant advances in CSS have made it possible
|
|
6
|
+
to use the browser's rendering engine for more and more things where JS was previously unavoidable.
|
|
7
|
+
Rather than starting from the assumption that client-side rendering in JS is inevitable for charts, RCharts builds
|
|
8
|
+
on the premise that decent baseline functionality is now possible just with server-side rendering.
|
|
9
|
+
|
|
10
|
+
Being able to lean on the browser's rendering engine results in visibly improved client-side rendering performance
|
|
11
|
+
and eliminates the scope for undesirable interactions with other rendering in JS. This makes it possible to layer
|
|
12
|
+
interactivity on top of charts much more reliably, because the chart is no different from the rest of the page.
|
|
13
|
+
One possible progressive enhancement would be panning and zooming, using a Stimulus controller to send a request
|
|
14
|
+
to the server and reload the chart with different parameters.
|
|
15
|
+
|
|
16
|
+
RCharts was conceived specifically with Turbo morphing in mind. Because morphing is able to update pages and frames
|
|
17
|
+
at the level of individual attributes, when CSS transitions are added to SVG elements, properties determining placement
|
|
18
|
+
and size of elements can be animated like any other. Like other styling, you can control all aspects of the transitions
|
|
19
|
+
using CSS, by overriding variables in the stylesheet provided or by using your own styles.
|
|
20
|
+
|
|
21
|
+
To be useful to a wider audience, charting libraries need to be flexible enough to cover a wide range of use cases.
|
|
22
|
+
RCharts aims to provide a comprehensive set of features that cover the most common use cases, with more to come.
|
|
23
|
+
It currently supports:
|
|
24
|
+
|
|
25
|
+
* Different chart types
|
|
26
|
+
* Line charts
|
|
27
|
+
* Bar charts
|
|
28
|
+
* Horizontal bar charts
|
|
29
|
+
* Area charts
|
|
30
|
+
* Scatter plots
|
|
31
|
+
* Combinations of the above
|
|
32
|
+
* Proper value handling
|
|
33
|
+
* Stacked area and bar charts
|
|
34
|
+
* Smoothing for line and area charts (you can choose by how much but not the method)
|
|
35
|
+
* Negative values (e.g. with split series for stacked area charts)
|
|
36
|
+
* Masks for missing values
|
|
37
|
+
* Various axis options
|
|
38
|
+
* Automatic axis ticks based on the type and values of provided data
|
|
39
|
+
* Axis labels formatted however you want
|
|
40
|
+
* Axis labels rotation and hiding to avoid overlap
|
|
41
|
+
* Axis titles
|
|
42
|
+
* Multiple parallel axes based on potentially different data
|
|
43
|
+
* Tooltips and legends
|
|
44
|
+
* Tooltips containing whatever you want
|
|
45
|
+
* Legend with placement in all directions
|
|
46
|
+
* Custom series symbols and colours
|
|
47
|
+
* Sparklines, by only rendering the chart itself
|
|
48
|
+
* CSS that you can customise to your liking, both by setting variables and/or using your own styles
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
The API is inspired by the venerable `ActionView::Helpers::FormBuilder`.
|
|
53
|
+
|
|
54
|
+
To get started, take a look at the WIP docs for `RCharts::GraphHelper`, and also run the dummy app which shows
|
|
55
|
+
the different chart types and how to combine them with morphing.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
$ git clone "https://github.com/BuildingAtlas/rcharts.git"
|
|
59
|
+
$ cd rcharts/test/dummy
|
|
60
|
+
$ rails s
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
Add this line to your application's Gemfile:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
gem 'rcharts'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
And then execute:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
$ bundle
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Install the stylesheet:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
$ rails rcharts:install
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Finally, include the helper in `ApplicationHelper`:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# app/helpers/application_helper.rb
|
|
87
|
+
|
|
88
|
+
module ApplicationHelper
|
|
89
|
+
include RCharts::GraphHelper
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Contributing
|
|
94
|
+
|
|
95
|
+
Contributions are welcome, but please open an issue first to discuss what you would like to add or change.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
The gem is available as open source under the terms of the [MIT Licence](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
|
6
|
+
load 'rails/tasks/engine.rake'
|
|
7
|
+
|
|
8
|
+
require 'bundler/gem_tasks'
|
|
9
|
+
|
|
10
|
+
require 'sdoc'
|
|
11
|
+
require 'rdoc/task'
|
|
12
|
+
|
|
13
|
+
RDoc::Task.new do |rdoc|
|
|
14
|
+
rdoc.rdoc_files.include('README.md', 'app/**/*.rb', 'lib/**/*.rb')
|
|
15
|
+
rdoc.rdoc_dir = 'doc'
|
|
16
|
+
rdoc.options << '--format=sdoc'
|
|
17
|
+
rdoc.options << '--github'
|
|
18
|
+
rdoc.options << '--title=RCharts'
|
|
19
|
+
rdoc.options << '--main=README.md'
|
|
20
|
+
rdoc.template = 'rails'
|
|
21
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCharts
|
|
4
|
+
module GraphHelper
|
|
5
|
+
module Axes
|
|
6
|
+
class AxisElement
|
|
7
|
+
module Styles # :nodoc:
|
|
8
|
+
DEFAULT_BREAKPOINTS = { hiding: { even: 1.1, odd: 0.6 }, rotation: { half: 1.0, full: 0.9 } }.freeze
|
|
9
|
+
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
ROTATION_CSS = <<~CSS
|
|
13
|
+
@container (inline-size < calc(<%= min_row_characters %> * <%= half_rotation_breakpoint %>)) {
|
|
14
|
+
.rcharts-chart .axis[data-name="x"] {
|
|
15
|
+
.axis-ticks[data-axis="<%= axis_id %>"] .axis-tick-text {
|
|
16
|
+
dominant-baseline: middle;
|
|
17
|
+
rotate: -45deg;
|
|
18
|
+
text-anchor: end;
|
|
19
|
+
}
|
|
20
|
+
.axis-ticks[data-axis="<%= axis_id %>"] {
|
|
21
|
+
height: calc(<%= max_label_characters %> * sin(45deg));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@container (inline-size < calc(<%= min_row_characters %> * <%= full_rotation_breakpoint %>)) {
|
|
27
|
+
.rcharts-chart .axis[data-name="x"] {
|
|
28
|
+
.axis-ticks[data-axis="<%= axis_id %>"] .axis-tick-text {
|
|
29
|
+
rotate: -90deg;
|
|
30
|
+
}
|
|
31
|
+
.axis-ticks[data-axis="<%= axis_id %>"] {
|
|
32
|
+
height: <%= max_label_characters %>;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
CSS
|
|
37
|
+
|
|
38
|
+
HIDING_CSS = <<~CSS
|
|
39
|
+
@container (inline-size < calc(<%= tick_count %> * <%= even_hiding_breakpoint %>)) {
|
|
40
|
+
.rcharts-chart .axis[data-name="x"] {
|
|
41
|
+
.axis-ticks[data-axis="<%= axis_id %>"] .axis-tick:nth-child(even) .axis-tick-text {
|
|
42
|
+
opacity: 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@container (inline-size < calc(<%= tick_count %> * <%= odd_hiding_breakpoint %>)) {
|
|
48
|
+
.rcharts-chart .axis[data-name="x"] {
|
|
49
|
+
.axis-ticks[data-axis="<%= axis_id %>"] .axis-tick:nth-child(4n + 3) .axis-tick-text {
|
|
50
|
+
opacity: 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
CSS
|
|
55
|
+
|
|
56
|
+
included do
|
|
57
|
+
attribute :breakpoints, default: -> { {} }
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def rendered_css
|
|
62
|
+
return unless horizontal?
|
|
63
|
+
|
|
64
|
+
ERB.new(ROTATION_CSS + HIDING_CSS)
|
|
65
|
+
.result_with_hash(min_row_characters: "#{min_row_characters}ch",
|
|
66
|
+
max_label_characters: "#{max_label_characters}ch",
|
|
67
|
+
tick_count: "#{tick_count}lh",
|
|
68
|
+
**breakpoints,
|
|
69
|
+
axis_id:)
|
|
70
|
+
.html_safe # rubocop:disable Rails/OutputSafety
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def breakpoints
|
|
74
|
+
flatten_breakpoints(DEFAULT_BREAKPOINTS.deep_merge(super))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def flatten_breakpoints(hash)
|
|
78
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
79
|
+
next result[key] = value unless value.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
value.each do |nested_key, nested_value|
|
|
82
|
+
result[:"#{nested_key}_#{key}_breakpoint"] = nested_value
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCharts
|
|
4
|
+
module GraphHelper
|
|
5
|
+
module Axes
|
|
6
|
+
class AxisElement < Element # :nodoc:
|
|
7
|
+
include Styles
|
|
8
|
+
|
|
9
|
+
attribute :name
|
|
10
|
+
attribute :index, :integer, default: 0
|
|
11
|
+
attribute :ticks, default: -> { {} }
|
|
12
|
+
attribute :label
|
|
13
|
+
attribute :horizontal, :boolean, default: false
|
|
14
|
+
attribute :character_scaling_factor, :float, default: 1.0
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_accessor :max_label_characters
|
|
19
|
+
|
|
20
|
+
alias horizontal? horizontal
|
|
21
|
+
|
|
22
|
+
def tags(&block)
|
|
23
|
+
with_max_label_characters_from block do
|
|
24
|
+
style_tag + container_tag(&block)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def container_tag(&)
|
|
29
|
+
tag.div class: 'axis', data: { name:, index: } do
|
|
30
|
+
svg_tag(&) + label_tag
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def svg_tag(&)
|
|
35
|
+
tag.svg class: 'axis-ticks', width: max_column_characters.try { "#{it * character_scaling_factor}ch" },
|
|
36
|
+
data: { axis: axis_id } do
|
|
37
|
+
ticks.each do |position, value|
|
|
38
|
+
concat tick_tag_for(position, value, &)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def label_tag
|
|
44
|
+
return ''.html_safe unless label
|
|
45
|
+
|
|
46
|
+
render LabelElement.new(label:, horizontal: horizontal?)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def tick_tag_for(position, value, &)
|
|
50
|
+
render TickElement.new(inline_axis: name, inline_axis_index: index,
|
|
51
|
+
value:, position:), &
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def style_tag
|
|
55
|
+
tag.style rendered_css
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def max_column_characters
|
|
59
|
+
return if horizontal?
|
|
60
|
+
|
|
61
|
+
max_label_characters
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def min_row_characters
|
|
65
|
+
max_label_characters.to_i * tick_count
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def with_max_label_characters_from(proc, &block)
|
|
69
|
+
self.max_label_characters = ticks.values
|
|
70
|
+
.collect { |value| capture_length { proc&.call(value) || value.to_s } }
|
|
71
|
+
.max
|
|
72
|
+
block&.call.tap { self.max_label_characters = nil }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def capture_length(&)
|
|
76
|
+
strip_tags(capture(&)).squish.length
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def axis_id
|
|
80
|
+
hash.abs
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def tick_count
|
|
84
|
+
ticks.count + 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCharts
|
|
4
|
+
module GraphHelper
|
|
5
|
+
module Axes
|
|
6
|
+
class LabelElement < Element # :nodoc:
|
|
7
|
+
attribute :label
|
|
8
|
+
attribute :horizontal, :boolean
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
alias horizontal? horizontal
|
|
13
|
+
|
|
14
|
+
def tags
|
|
15
|
+
tag.svg class: 'axis-label', overflow: 'visible', width:, height: do
|
|
16
|
+
tag.svg x: '50%', y: '50%', overflow: 'visible' do
|
|
17
|
+
tag.text label, class: 'axis-label-text'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def width
|
|
23
|
+
horizontal? ? Percentage::MAX : '1lh'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def height
|
|
27
|
+
horizontal? ? '1lh' : Percentage::MAX
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCharts
|
|
4
|
+
module GraphHelper
|
|
5
|
+
module Axes
|
|
6
|
+
class TickElement < Element # :nodoc:
|
|
7
|
+
attribute :value
|
|
8
|
+
attribute :position, :'rcharts/percentage', default: Percentage::MIN
|
|
9
|
+
attribute :inline_axis, :'rcharts/symbol'
|
|
10
|
+
attribute :inline_axis_index, :integer, default: 0
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def tags(&block)
|
|
15
|
+
tag.svg class: 'axis-tick', x:, y: do
|
|
16
|
+
tag.text class: 'axis-tick-text', data: { inline_axis:, inline_axis_index: } do
|
|
17
|
+
block&.call(value) || value
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def x
|
|
23
|
+
case inline_axis
|
|
24
|
+
when :x then position
|
|
25
|
+
else inline_axis_index.zero? ? Percentage::MAX : Percentage::MIN
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def y
|
|
30
|
+
case inline_axis
|
|
31
|
+
when :x then Percentage::MIN
|
|
32
|
+
else Percentage::MAX - position
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCharts
|
|
4
|
+
module GraphHelper
|
|
5
|
+
module Categories
|
|
6
|
+
# = Bar Builder
|
|
7
|
+
class BarBuilder < ElementBuilder
|
|
8
|
+
attribute :name
|
|
9
|
+
attribute :inline_size, :'rcharts/percentage', default: Percentage::MAX
|
|
10
|
+
attribute :block_size, :'rcharts/percentage', default: Percentage::MAX
|
|
11
|
+
attribute :block_position, :'rcharts/percentage', default: Percentage::MIN
|
|
12
|
+
attribute :inline_index, :integer, default: 0
|
|
13
|
+
attribute :series_index, :integer, default: 0
|
|
14
|
+
attribute :horizontal, :boolean, default: false
|
|
15
|
+
attribute :series_options, default: -> { {} }
|
|
16
|
+
|
|
17
|
+
# Renders a bar segment. Passes through <tt>:data</tt>, and <tt>:aria</tt>, and <tt>:class</tt> options to the tag builder.
|
|
18
|
+
# <%= graph_for @annual_sales do |graph| %>
|
|
19
|
+
# <%= graph.category do |category| %>
|
|
20
|
+
# <%= category.series do |series| %>
|
|
21
|
+
# <%= series.bar %>
|
|
22
|
+
# <% end %>
|
|
23
|
+
# <% end %>
|
|
24
|
+
# <% end %>
|
|
25
|
+
def bar(**)
|
|
26
|
+
render BarSegmentElement.new(inline_size:, block_size:, block_position:, inline_index:, series_index:,
|
|
27
|
+
horizontal:, series_options:, **)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCharts
|
|
4
|
+
module GraphHelper
|
|
5
|
+
module Categories
|
|
6
|
+
class BarSegmentElement < Element # :nodoc:
|
|
7
|
+
attribute :horizontal, :boolean, default: false
|
|
8
|
+
attribute :inline_size, :'rcharts/percentage', default: Percentage::MAX
|
|
9
|
+
attribute :block_size, :'rcharts/percentage', default: Percentage::MAX
|
|
10
|
+
attribute :block_position, :'rcharts/percentage', default: Percentage::MIN
|
|
11
|
+
attribute :inline_index, :integer, default: 0
|
|
12
|
+
attribute :series_index, :integer, default: 0
|
|
13
|
+
attribute :series_options, default: -> { {} }
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
alias horizontal? horizontal
|
|
18
|
+
|
|
19
|
+
def tags
|
|
20
|
+
tag.rect x:, y:, height:, width:, class: ['series-shape', class_names, color_class], data:, aria:
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def x
|
|
24
|
+
horizontal? ? block_position : inline_position
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def y
|
|
28
|
+
horizontal? ? inline_position : Percentage::MAX - block_position
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def height
|
|
32
|
+
horizontal? ? inline_size : block_size
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def width
|
|
36
|
+
horizontal? ? block_size : inline_size
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inline_position
|
|
40
|
+
inline_size * inline_index
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def color_class
|
|
44
|
+
series_options.fetch(:color_class, RCharts.color_class_for(series_index))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCharts
|
|
4
|
+
module GraphHelper
|
|
5
|
+
module Categories
|
|
6
|
+
class BarsElement < Element # :nodoc:
|
|
7
|
+
SPACING_FACTOR = 0.6
|
|
8
|
+
|
|
9
|
+
attribute :index, :integer, default: 0
|
|
10
|
+
attribute :values_count, :integer, default: 1
|
|
11
|
+
attribute :inline_position, :'rcharts/percentage', default: Percentage::MIN
|
|
12
|
+
attribute :horizontal, :boolean, default: false
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
alias horizontal? horizontal
|
|
17
|
+
|
|
18
|
+
def tags(&)
|
|
19
|
+
tag.svg x:, y:, width:, height:, class: 'category', &
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def x
|
|
23
|
+
adjusted_inline_position if horizontal?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def y
|
|
27
|
+
Percentage::MAX - adjusted_inline_position unless horizontal?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def height
|
|
31
|
+
horizontal? ? Percentage::MAX : inline_size
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def width
|
|
35
|
+
horizontal? ? inline_size : Percentage::MAX
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def inline_size
|
|
39
|
+
(Percentage::MAX / values_count) * SPACING_FACTOR
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def adjusted_inline_position
|
|
43
|
+
inline_position + inline_adjustment
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def inline_adjustment
|
|
47
|
+
(inline_size / 2) * (horizontal? ? -1 : 1)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCharts
|
|
4
|
+
module GraphHelper
|
|
5
|
+
module Categories
|
|
6
|
+
# = Category Builder
|
|
7
|
+
class CategoryBuilder < ElementBuilder
|
|
8
|
+
##
|
|
9
|
+
# :attr_accessor:
|
|
10
|
+
attribute :name
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# :attr_accessor: index
|
|
14
|
+
attribute :index, :integer, default: 0
|
|
15
|
+
|
|
16
|
+
attribute :category, default: -> { {} }
|
|
17
|
+
attribute :values_count, :integer, default: 1
|
|
18
|
+
attribute :layout_axes, default: -> { Graph::Axes.new }
|
|
19
|
+
attribute :series_options, default: -> { {} }
|
|
20
|
+
|
|
21
|
+
# Shortcut to render a bar for every series. Useful when you don't need to differentiate between different
|
|
22
|
+
# series. Arguments and options are passed to #series, see there for more details.
|
|
23
|
+
def bar(*, **)
|
|
24
|
+
series(*, **, &:bar)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Renders one or more series present in the data. For each series yields a BarBuilder which
|
|
28
|
+
# contains the series:
|
|
29
|
+
# <%= graph_for @annual_sales do |graph| %>
|
|
30
|
+
# <%= graph.category do |category| %>
|
|
31
|
+
# <%= category.series do |series| %>
|
|
32
|
+
# <%= series.bar %>
|
|
33
|
+
# <% end %>
|
|
34
|
+
# <% end %>
|
|
35
|
+
# <% end %>
|
|
36
|
+
# The default is to iterate over all series, but you can specify a subset by passing the series names as
|
|
37
|
+
# arguments.
|
|
38
|
+
#
|
|
39
|
+
# ==== Options
|
|
40
|
+
# [<tt>:axis</tt>] The axis for the series. Defaults to the first continuous axis.
|
|
41
|
+
# [<tt>:inline_axis</tt>] The axis for the categories. Defaults to the first discrete axis.
|
|
42
|
+
def series(*names, axis: nil, inline_axis: nil, **, &)
|
|
43
|
+
bars_tag(axis: inline_axis) do
|
|
44
|
+
filtered_series(*names).each_with_index do |(name, value), index|
|
|
45
|
+
concat bar_for(name, value, index, filtered_series(*names)&.count, axis:, **, &)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def bars_tag(axis: nil, **, &)
|
|
53
|
+
resolve_axis :discrete, axis do |discrete_axis|
|
|
54
|
+
render BarsElement.new(index:, values_count:, inline_position: discrete_axis.position_at(index),
|
|
55
|
+
horizontal: discrete_axis.horizontal?, **), &
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def bar_for(name, value, index, series_count, axis: nil, **, &) # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
60
|
+
resolve_axis :continuous, axis do |continuous_axis|
|
|
61
|
+
render BarBuilder.new(name:, inline_index: (continuous_axis.stacked? ? 0 : index),
|
|
62
|
+
series_index: category.keys.index(name),
|
|
63
|
+
inline_size: inline_size_for(continuous_axis, series_count),
|
|
64
|
+
series_options: series_options.fetch(name, {}),
|
|
65
|
+
horizontal: continuous_axis.horizontal?,
|
|
66
|
+
block_size: continuous_axis.length_between(0, value.to_f),
|
|
67
|
+
block_position: block_position_for(continuous_axis, name, value),
|
|
68
|
+
**),
|
|
69
|
+
&
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def inline_size_for(continuous_axis, series_count = nil)
|
|
74
|
+
Percentage::MAX / (continuous_axis.stacked? ? 1 : series_count || category.keys.count)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def block_position_for(continuous_axis, name, value)
|
|
78
|
+
continuous_axis.position_for(adjusted_value_for(continuous_axis, name, value))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def adjusted_value_for(continuous_axis, name, value)
|
|
82
|
+
previous_value_for(name, continuous_axis).to_f + value_offset_for(continuous_axis, value)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def value_offset_for(axis, value)
|
|
86
|
+
(axis.horizontal? && value.to_f.positive?) || (!axis.horizontal? && value.to_f.negative?) ? 0 : value.to_f
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def previous_value_for(key, continuous_axis)
|
|
90
|
+
index_of(key).then do |index|
|
|
91
|
+
return 0 if !continuous_axis.stacked? || index.zero?
|
|
92
|
+
|
|
93
|
+
filtered_series.values
|
|
94
|
+
.slice(0...index)
|
|
95
|
+
.compact
|
|
96
|
+
.filter(&(filtered_series.values[index].to_f.positive? ? :positive? : :negative?))
|
|
97
|
+
.sum
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def index_of(key)
|
|
102
|
+
filtered_series.keys.index(key)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def filtered_series(*names)
|
|
106
|
+
category.reject { names.presence&.exclude?(it) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def resolve_axis(type, name = nil, &)
|
|
110
|
+
(name ? layout_axes.fetch(*name) : layout_axes.public_send(type)).then(&)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|