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.
@@ -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