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,121 @@
|
|
1
|
+
require 'action_view/helpers/number_helper'
|
2
|
+
require 'action_view/helpers/capture_helper'
|
3
|
+
require 'action_view/helpers/output_safety_helper'
|
4
|
+
require 'action_view/helpers/tag_helper'
|
5
|
+
require 'active_support/inflector'
|
6
|
+
require 'active_charts/util'
|
7
|
+
|
8
|
+
module ActiveCharts
|
9
|
+
class Chart
|
10
|
+
include ActionView::Helpers::NumberHelper
|
11
|
+
include ActionView::Helpers::CaptureHelper
|
12
|
+
include ActionView::Helpers::OutputSafetyHelper
|
13
|
+
include ActionView::Helpers::TagHelper
|
14
|
+
include ActiveSupport::Inflector
|
15
|
+
|
16
|
+
MARGIN = 20
|
17
|
+
CSS_CLASSES = %w[a b c d e f g h i j k l m n o].map { |letter| 'series-' + letter }.freeze
|
18
|
+
|
19
|
+
def initialize(collection, options = {})
|
20
|
+
@collection = Util.array_of_arrays?(collection) ? collection : [[]]
|
21
|
+
@rows_count = @collection.count
|
22
|
+
@columns_count = @collection.map(&:count).max
|
23
|
+
process_options(options)
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :collection, :rows_count, :columns_count, :title, :extra_css_classes,
|
27
|
+
:max_values, :data_formatters, :label_height, :series_labels
|
28
|
+
|
29
|
+
def to_html
|
30
|
+
inner_html = [tag.figcaption(title, class: 'ac-chart-title'), chart_svg_tag, legend_list_tag].join('
|
31
|
+
')
|
32
|
+
|
33
|
+
tag.figure(inner_html.html_safe, class: container_classes)
|
34
|
+
end
|
35
|
+
|
36
|
+
def chart_svg_tag; end
|
37
|
+
|
38
|
+
def legend_list_tag
|
39
|
+
list_items = series_labels.map.with_index do |label, index|
|
40
|
+
tag.li(label, class: series_class(index))
|
41
|
+
end
|
42
|
+
|
43
|
+
tag.ul(list_items.join.html_safe, class: 'ac-chart ac-series-legend')
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def process_options(options)
|
49
|
+
@title = options[:title] || ''
|
50
|
+
@extra_css_classes = options[:class] || ''
|
51
|
+
@data_formatters = options[:data_formatters] || []
|
52
|
+
@max_values = valid_max_values(options[:max_values], options[:single_y_scale])
|
53
|
+
@label_height = options[:label_height] || MARGIN / 2
|
54
|
+
@series_labels = options[:series_labels] || []
|
55
|
+
end
|
56
|
+
|
57
|
+
def valid_max_values(custom_max_values = nil, single_y_scale = false)
|
58
|
+
return custom_max_values if valid_max_values?(custom_max_values)
|
59
|
+
|
60
|
+
computed_max_values = Util.max_values(@collection)
|
61
|
+
|
62
|
+
return computed_max_values unless single_y_scale
|
63
|
+
|
64
|
+
Array.new(columns_count, computed_max_values.max)
|
65
|
+
end
|
66
|
+
|
67
|
+
def valid_max_values?(max_values)
|
68
|
+
return false unless max_values.is_a?(Array)
|
69
|
+
return false unless max_values.count.eql?(columns_count)
|
70
|
+
|
71
|
+
Util.max_values(@collection).map.with_index do |computed_val, index|
|
72
|
+
computed_val <= max_values[index]
|
73
|
+
end.all?
|
74
|
+
end
|
75
|
+
|
76
|
+
def container_classes
|
77
|
+
['ac-chart-container', 'ac-clearfix', extra_css_classes].join(' ')
|
78
|
+
end
|
79
|
+
|
80
|
+
def series_class(index)
|
81
|
+
CSS_CLASSES[index % CSS_CLASSES.size]
|
82
|
+
end
|
83
|
+
|
84
|
+
def tag_options(opts, whitelist = nil)
|
85
|
+
opts = opts.select { |k, _v| whitelist.include? k.to_s } if whitelist
|
86
|
+
tag_builder = TagBuilder.new(self)
|
87
|
+
opts.map { |k, v| tag_builder.tag_option(k, v, true) }.join(' ')
|
88
|
+
end
|
89
|
+
|
90
|
+
def formatted_val(val, formatter = nil)
|
91
|
+
case formatter
|
92
|
+
when :percent
|
93
|
+
number_to_percentage(Util.safe_to_dec(val) * 100, precision: 1)
|
94
|
+
when :date
|
95
|
+
Util.date_label(val)
|
96
|
+
when :rounded
|
97
|
+
Util.safe_to_dec(val).round
|
98
|
+
when :currency
|
99
|
+
number_to_currency(val, precision: 0)
|
100
|
+
when :year
|
101
|
+
val.to_i.to_s
|
102
|
+
else
|
103
|
+
number_with_delimiter(val)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def svg_options
|
108
|
+
{
|
109
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
110
|
+
style: "width: #{svg_width}px; height: auto;",
|
111
|
+
viewBox: "0 0 #{svg_width} #{svg_height}",
|
112
|
+
class: ['ac-chart', css_class].join(' ')
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def css_class
|
117
|
+
class_name = self.class.to_s.gsub('ActiveCharts::', '')
|
118
|
+
'ac-' + underscore(class_name).tr('_', '-')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'active_charts/generators/assets'
|
3
|
+
|
4
|
+
module ActiveCharts
|
5
|
+
class CLI < Thor
|
6
|
+
desc 'install', 'Installs Active Record and generates the default asset files'
|
7
|
+
|
8
|
+
def install
|
9
|
+
ActiveCharts::Generators::Assets.start
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'thor/group'
|
2
|
+
|
3
|
+
module ActiveCharts
|
4
|
+
module Generators
|
5
|
+
class Assets < Thor::Group
|
6
|
+
include Thor::Actions
|
7
|
+
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
|
10
|
+
def copy_stylesheets
|
11
|
+
template 'active_charts.css.scss', 'app/assets/stylesheets/active_charts.css.scss'
|
12
|
+
say ' - make sure to require or import it if you have customized your application.css file'
|
13
|
+
end
|
14
|
+
|
15
|
+
def copy_javascript
|
16
|
+
template 'active_charts.js', 'app/assets/javascripts/active_charts.js'
|
17
|
+
say ' - bundle active_charts.js in application.js by adding:
|
18
|
+
//= require active_charts'
|
19
|
+
say ' - if your application.js is loaded in document head, precompile the active_charts script separately by adding to config/initializers/assets.rb:
|
20
|
+
Rails.application.config.assets.precompile += %w[active_charts.js]'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,136 @@
|
|
1
|
+
$ac-light-gray: #dedede;
|
2
|
+
$ac-red: #fe4a5d;
|
3
|
+
$ac-orange: #ff9016;
|
4
|
+
$ac-yellow: #fee123;
|
5
|
+
$ac-green: #008476;
|
6
|
+
$ac-blue: #4542ca;
|
7
|
+
$ac-purple: #702277;
|
8
|
+
$ac-pink: #ffb3cb;
|
9
|
+
$ac-lime: #a5e100;
|
10
|
+
$ac-teal: #00a0cc;
|
11
|
+
$ac-lavendar: #c19bff;
|
12
|
+
$ac-gold: #d39f10;
|
13
|
+
$ac-brown: #7d4a32;
|
14
|
+
$ac-gray: #aaa;
|
15
|
+
$ac-black: #000;
|
16
|
+
$ac-olive: #a09f29;
|
17
|
+
|
18
|
+
$ac-border: $ac-light-gray;
|
19
|
+
|
20
|
+
$ac-series: (
|
21
|
+
series-a: $ac-red,
|
22
|
+
series-b: $ac-orange,
|
23
|
+
series-c: $ac-yellow,
|
24
|
+
series-d: $ac-green,
|
25
|
+
series-e: $ac-blue,
|
26
|
+
series-f: $ac-purple,
|
27
|
+
series-g: $ac-pink,
|
28
|
+
series-h: $ac-lime,
|
29
|
+
series-i: $ac-teal,
|
30
|
+
series-j: $ac-lavendar,
|
31
|
+
series-k: $ac-gold,
|
32
|
+
series-l: $ac-brown,
|
33
|
+
series-m: $ac-gray,
|
34
|
+
series-n: $ac-black,
|
35
|
+
series-o: $ac-olive,
|
36
|
+
);
|
37
|
+
|
38
|
+
@mixin smart-box {
|
39
|
+
-webkit-box-sizing: border-box;
|
40
|
+
-moz-box-sizing: border-box;
|
41
|
+
-ms-box-sizing: border-box;
|
42
|
+
box-sizing: border-box;
|
43
|
+
}
|
44
|
+
|
45
|
+
@mixin border-radius($radius) {
|
46
|
+
-webkit-border-radius: $radius;
|
47
|
+
-moz-border-radius: $radius;
|
48
|
+
-ms-border-radius: $radius;
|
49
|
+
border-radius: $radius;
|
50
|
+
}
|
51
|
+
|
52
|
+
@mixin slow-transitions {
|
53
|
+
-webkit-transition-property: opacity background color fill stroke;
|
54
|
+
-webkit-transition-duration: 0.20s;
|
55
|
+
-webkit-transition-timing-function: ease-out;
|
56
|
+
-moz-transition-property: opacity background color fill stroke;
|
57
|
+
-moz-transition-duration: 0.20s;
|
58
|
+
-moz-transition-timing-function: ease-out;
|
59
|
+
-ms-transition-property: opacity background color fill stroke;
|
60
|
+
-ms-transition-duration: 0.20s;
|
61
|
+
-ms-transition-timing-function: ease-out;
|
62
|
+
transition-property: opacity background color fill stroke;
|
63
|
+
transition-duration: 0.20s;
|
64
|
+
transition-timing-function: ease-out;
|
65
|
+
}
|
66
|
+
|
67
|
+
text, circle { @include slow-transitions; }
|
68
|
+
|
69
|
+
.ac-clearfix:before,
|
70
|
+
.ac-clearfix:after {
|
71
|
+
content: " "; /* 1 */
|
72
|
+
display: table; /* 2 */
|
73
|
+
}
|
74
|
+
.ac-clearfix:after {
|
75
|
+
clear: both;
|
76
|
+
}
|
77
|
+
|
78
|
+
figure.ac-chart-container {
|
79
|
+
margin-bottom: 1em;
|
80
|
+
}
|
81
|
+
|
82
|
+
figcaption.ac-chart-title {
|
83
|
+
text-align: center;
|
84
|
+
font-weight: 700;
|
85
|
+
font-size: 1em;
|
86
|
+
text-transform: capitalize;
|
87
|
+
margin: 0 auto 0.5em auto;
|
88
|
+
}
|
89
|
+
|
90
|
+
svg.ac-chart {
|
91
|
+
background: #fff;
|
92
|
+
max-width: 100%;
|
93
|
+
display: block;
|
94
|
+
margin: 0 auto 1em auto;
|
95
|
+
|
96
|
+
line.ac-grid-line, rect { stroke-width: 1px; stroke: $ac-border; }
|
97
|
+
path.ac-line-chart-line { stroke-width: 1px; fill: transparent; }
|
98
|
+
rect.grid { fill: #fff; }
|
99
|
+
text { text-anchor: middle; font-size: 14px; height: 12px; }
|
100
|
+
text.anchor_start, text.ac-y-label { text-anchor: start; }
|
101
|
+
text.ac-toggleable { text-anchor: start; opacity: 0; cursor: pointer; z: 2; }
|
102
|
+
text.ac-toggleable:hover { opacity: 0.2; }
|
103
|
+
.ac-triggerable { cursor: pointer; z: 1; }
|
104
|
+
}
|
105
|
+
|
106
|
+
svg.ac-chart.ac-scatter-plot {
|
107
|
+
circle.ac-scatter-plot-dot { r: 3px; stroke-width: 3px; fill: transparent; }
|
108
|
+
}
|
109
|
+
|
110
|
+
ul.ac-chart.ac-series-legend {
|
111
|
+
@include smart-box;
|
112
|
+
|
113
|
+
width: 360px;
|
114
|
+
max-width: 100%;
|
115
|
+
margin: 0 auto;
|
116
|
+
padding: 0.25em;
|
117
|
+
|
118
|
+
li {
|
119
|
+
list-style: none;
|
120
|
+
text-align: left;
|
121
|
+
padding-left: 0.5em;
|
122
|
+
margin-bottom: 0.2em;
|
123
|
+
border-left-width: 1em;
|
124
|
+
border-left-style: solid;
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
@each $name, $color in $ac-series {
|
129
|
+
rect.ac-bar-chart-bar.#{$name} { fill: $color; }
|
130
|
+
path.ac-line-chart-line.#{$name},
|
131
|
+
circle.ac-scatter-plot-dot.#{$name} { stroke: $color; }
|
132
|
+
li.#{$name} { border-color: $color; }
|
133
|
+
}
|
134
|
+
|
135
|
+
.ac-highlight { stroke: $ac-black !important; z: 1 !important; }
|
136
|
+
.ac-visible { opacity: 1 !important; z: 2 !important; }
|
@@ -0,0 +1,86 @@
|
|
1
|
+
/*
|
2
|
+
* ActiveCharts
|
3
|
+
* ============
|
4
|
+
* package for chart interactivity
|
5
|
+
* no dependencies
|
6
|
+
* browser support: IE 9+, Firefox, Chrome, Safari
|
7
|
+
*/
|
8
|
+
var ActiveCharts = (function() {
|
9
|
+
|
10
|
+
var tooltip_triggers = [];
|
11
|
+
var tooltips = [];
|
12
|
+
|
13
|
+
function setTriggers(classNames) {
|
14
|
+
for (var i = 0; i < classNames.length; i++) {
|
15
|
+
tooltip_triggers.push.apply(tooltip_triggers, document.getElementsByClassName(classNames[i]));
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
function setTooltips(classNames) {
|
20
|
+
for (var i = 0; i < classNames.length; i++) {
|
21
|
+
tooltips.push.apply(tooltips, document.getElementsByClassName(classNames[i]));
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
function toggleTooltip(triggerEl, tooltipEl) {
|
26
|
+
if (hasClass(triggerEl, 'ac-highlight')) {
|
27
|
+
removeClass(triggerEl, 'ac-highlight');
|
28
|
+
removeClass(tooltipEl, 'ac-visible');
|
29
|
+
} else {
|
30
|
+
addClass(triggerEl, 'ac-highlight');
|
31
|
+
addClass(tooltipEl, 'ac-visible');
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
function addClass(el, className) {
|
36
|
+
if (el.classList)
|
37
|
+
el.classList.add(className);
|
38
|
+
else
|
39
|
+
el.className += ' ' + className;
|
40
|
+
}
|
41
|
+
|
42
|
+
function removeClass(el, className) {
|
43
|
+
if (el.classList)
|
44
|
+
el.classList.remove(className);
|
45
|
+
else
|
46
|
+
el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
47
|
+
}
|
48
|
+
|
49
|
+
function hasClass(el, className) {
|
50
|
+
if (el.classList)
|
51
|
+
return el.classList.contains(className);
|
52
|
+
else
|
53
|
+
return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
|
54
|
+
}
|
55
|
+
|
56
|
+
return {
|
57
|
+
|
58
|
+
activateTooltips: function(triggerClassNames, tooltipClassNames, eventName) {
|
59
|
+
setTriggers(triggerClassNames);
|
60
|
+
setTooltips(tooltipClassNames)
|
61
|
+
|
62
|
+
for (var i = 0; i < tooltip_triggers.length; i++) {
|
63
|
+
tooltip_triggers[i].addEventListener(eventName, function(e){
|
64
|
+
toggleTooltip(e.target, e.target.nextElementSibling);
|
65
|
+
});
|
66
|
+
}
|
67
|
+
|
68
|
+
for (i = 0; i < tooltips.length; i++) {
|
69
|
+
tooltips[i].addEventListener(eventName, function(e){
|
70
|
+
toggleTooltip(e.target.previousElementSibling, e.target);
|
71
|
+
});
|
72
|
+
}
|
73
|
+
},
|
74
|
+
|
75
|
+
// expose some private methods
|
76
|
+
toggleTooltip: toggleTooltip,
|
77
|
+
addClass: addClass,
|
78
|
+
removeClass: removeClass,
|
79
|
+
hasClass: hasClass
|
80
|
+
|
81
|
+
};
|
82
|
+
|
83
|
+
})();
|
84
|
+
|
85
|
+
ActiveCharts.activateTooltips(['ac-triggerable'], ['ac-toggleable'], 'click');
|
86
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'active_charts/helpers/collection_parser'
|
2
|
+
|
3
|
+
module ActiveCharts
|
4
|
+
module Helpers
|
5
|
+
autoload :BarChartHelper, 'active_charts/helpers/bar_chart_helper'
|
6
|
+
autoload :ScatterPlotHelper, 'active_charts/helpers/scatter_plot_helper'
|
7
|
+
autoload :LineChartHelper, 'active_charts/helpers/line_chart_helper'
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_charts/util'
|
4
|
+
|
5
|
+
module ActiveCharts
|
6
|
+
module Helpers #:nodoc:
|
7
|
+
module BarChartHelper
|
8
|
+
def bar_chart(collection, options = {})
|
9
|
+
BarChart.new(collection, options).to_html
|
10
|
+
end
|
11
|
+
|
12
|
+
def bar_chart_for(resource_collection, columns = [], options = {})
|
13
|
+
return bar_chart([[]], options) unless Util.valid_collection?(resource_collection)
|
14
|
+
|
15
|
+
parser = CollectionParser.new(resource_collection, columns, options[:label_column])
|
16
|
+
bar_chart(parser.collection, options.merge(series_labels: parser.series_labels, rows: parser.rows))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveCharts
|
4
|
+
module Helpers #:nodoc:
|
5
|
+
class CollectionParser
|
6
|
+
def initialize(resource_collection, columns, label_column)
|
7
|
+
resource = resource_collection.first.class
|
8
|
+
|
9
|
+
@label_column = label_column || auto_label_column(resource)
|
10
|
+
@columns = valid_columns(resource, columns)
|
11
|
+
@rows = resource_collection.pluck(@label_column)
|
12
|
+
@collection = resource_collection.pluck(*@columns)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :collection, :columns, :rows, :label_column
|
16
|
+
|
17
|
+
def series_labels
|
18
|
+
columns.map(&:to_s).map(&:titleize)
|
19
|
+
end
|
20
|
+
|
21
|
+
def xy_series_labels
|
22
|
+
x_label = series_labels.first
|
23
|
+
|
24
|
+
series_labels[1..-1].map do |y_label|
|
25
|
+
"#{x_label} vs. #{y_label}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def xy_collection
|
30
|
+
collection.map do |row|
|
31
|
+
x_val = row.first
|
32
|
+
|
33
|
+
row[1..-1].map do |y_val|
|
34
|
+
[x_val, y_val]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def valid_columns(resource, columns)
|
42
|
+
attribute_names = resource.new.attribute_names.map(&:to_sym)
|
43
|
+
|
44
|
+
return attribute_names if columns.eql?([])
|
45
|
+
|
46
|
+
attribute_names & columns
|
47
|
+
end
|
48
|
+
|
49
|
+
def auto_label_column(resource)
|
50
|
+
attribute_names = resource.new.attribute_names
|
51
|
+
|
52
|
+
%w[name title id].each do |attribute_name|
|
53
|
+
return attribute_name.to_sym if attribute_names.include?(attribute_name)
|
54
|
+
end
|
55
|
+
|
56
|
+
attribute_names.first.to_sym
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private_constant :CollectionParser
|
61
|
+
end
|
62
|
+
end
|