chartjs-zoomable 1.0.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 +90 -0
- data/Rakefile +32 -0
- data/app/assets/config/chartjs_zoomable_manifest.js +2 -0
- data/app/assets/javascripts/chartjs/zoomable/application.js +14 -0
- data/app/assets/javascripts/chartjs/zoomable/chartjs-plugin-zoom.js +580 -0
- data/app/assets/stylesheets/chartjs/zoomable/application.css +15 -0
- data/app/controllers/chartjs/zoomable/application_controller.rb +7 -0
- data/app/helpers/chartjs/zoomable/application_helper.rb +6 -0
- data/app/jobs/chartjs/zoomable/application_job.rb +6 -0
- data/app/mailers/chartjs/zoomable/application_mailer.rb +8 -0
- data/app/models/chartjs/zoomable/application_record.rb +7 -0
- data/app/views/layouts/chartjs/zoomable/application.html.erb +16 -0
- data/config/routes.rb +2 -0
- data/lib/chartjs/zoomable.rb +7 -0
- data/lib/chartjs/zoomable/engine.rb +7 -0
- data/lib/chartjs/zoomable/version.rb +5 -0
- data/lib/tasks/chartjs/zoomable_tasks.rake +4 -0
- metadata +105 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a1774f680deadbe485abb972e0cd29e92d68d3e9
|
4
|
+
data.tar.gz: 271068cca5ba6fc54127ae5885f2509a9e87641f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b68ff04421cd69b5825cc5653d682c247547518e1b5b3ea200ae026fd616ade435e33aede74dd3220f453622ad3aa93a6a9c1888d10f3fa16e0abe640bc04a00
|
7
|
+
data.tar.gz: 1f1c2bb5da1f4f0e690c7305eb1e5eb90fbe09d36f0616500cb5d55822a32b7a2d824ddf616a06e42643b72c96f7e9a43c78e2903d2f9d73065b8b83b9b69698
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2018 Mateusz Michalski
|
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,90 @@
|
|
1
|
+
# chartjs-zoomable
|
2
|
+
Simplifies usage of Chart.js in Rails views with zoom and pan plugin!
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
Add this line to your application's Gemfile:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem 'chartjs-zoomable'
|
9
|
+
```
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
```bash
|
13
|
+
$ bundle
|
14
|
+
```
|
15
|
+
Add the following to your `application.js`:
|
16
|
+
```javascript
|
17
|
+
$ //= require chartjs-zoomable
|
18
|
+
```
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
Each chart type has a corresponding helper for your views. The helper methods take the same arguments as their Javascript counterparts. The `options` are optional. They allow you to make a graph zoomable:
|
22
|
+
|
23
|
+
```erb
|
24
|
+
<%= line_chart data, options %>
|
25
|
+
<%= bar_chart data, options %>
|
26
|
+
<%= horizontal_bar_chart data, options %>
|
27
|
+
<%= radar_chart data, options %>
|
28
|
+
<%= polar_area_chart data, options %>
|
29
|
+
<%= pie_chart data, options %>
|
30
|
+
<%= doughnut_chart data, options %>
|
31
|
+
<%= bubble_chart data, options %>
|
32
|
+
<%= scatter_chart data, options %>
|
33
|
+
```
|
34
|
+
|
35
|
+
If you don't want these helpers – perhaps they clash with other methods in your views – add this initializer to your app:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
# config/initializers/chartjs.rb
|
39
|
+
Chartjs.no_conflict!
|
40
|
+
```
|
41
|
+
|
42
|
+
Then you can use these helpers instead:
|
43
|
+
|
44
|
+
```erb
|
45
|
+
<%= chartjs_line_chart data, options %>
|
46
|
+
<%= chartjs_bar_chart data, options %>
|
47
|
+
<%= chartjs_horizontal_bar_chart data, options %>
|
48
|
+
<%= chartjs_radar_chart data, options %>
|
49
|
+
<%= chartjs_polar_area_chart data, options %>
|
50
|
+
<%= chartjs_pie_chart data, options %>
|
51
|
+
<%= chartjs_doughnut_chart data, options %>
|
52
|
+
<%= chartjs_bubble_chart data, options %>
|
53
|
+
<%= chartjs_scatter_chart data, options %>
|
54
|
+
```
|
55
|
+
|
56
|
+
For example, to render a [line chart][linechart] with zoom:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
data = {
|
60
|
+
labels: ["March", "April", "May", "June", "July", "August", "September"],
|
61
|
+
datasets: [
|
62
|
+
{
|
63
|
+
label: "My First dataset",
|
64
|
+
backgroundColor: "rgba(220,220,220,0.2)",
|
65
|
+
borderColor: "rgba(220,220,220,1)",
|
66
|
+
data: [67, 58, 80, 81, 56, 55, 40]
|
67
|
+
},
|
68
|
+
{
|
69
|
+
label: "My Second dataset",
|
70
|
+
backgroundColor: "rgba(151,187,205,0.2)",
|
71
|
+
borderColor: "rgba(151,187,205,1)",
|
72
|
+
data: [28, 48, 40, 19, 86, 27, 91]
|
73
|
+
}
|
74
|
+
]
|
75
|
+
}
|
76
|
+
options = {
|
77
|
+
id: "my_first_graph",
|
78
|
+
zoom: {
|
79
|
+
enabled: true,
|
80
|
+
mode: 'x',
|
81
|
+
},
|
82
|
+
}
|
83
|
+
<%= line_chart data, options %>
|
84
|
+
```
|
85
|
+
|
86
|
+
## Contributing
|
87
|
+
Contribution directions go here.
|
88
|
+
|
89
|
+
## License
|
90
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Chartjs::Zoomable'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,14 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require Chart.bundle.min
|
14
|
+
//= require_tree .
|
@@ -0,0 +1,580 @@
|
|
1
|
+
/*!
|
2
|
+
* chartjs-plugin-zoom
|
3
|
+
* http://chartjs.org/
|
4
|
+
* Version: 0.6.5
|
5
|
+
*
|
6
|
+
* Copyright 2016 Evert Timberg
|
7
|
+
* Released under the MIT license
|
8
|
+
* https://github.com/chartjs/chartjs-plugin-zoom/blob/master/LICENSE.md
|
9
|
+
*/
|
10
|
+
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
11
|
+
|
12
|
+
},{}],2:[function(require,module,exports){
|
13
|
+
/*jslint browser:true, devel:true, white:true, vars:true */
|
14
|
+
/*global require*/
|
15
|
+
|
16
|
+
// hammer JS for touch support
|
17
|
+
var Hammer = require('hammerjs');
|
18
|
+
Hammer = typeof(Hammer) === 'function' ? Hammer : window.Hammer;
|
19
|
+
|
20
|
+
// Get the chart variable
|
21
|
+
var Chart = require('chart.js');
|
22
|
+
Chart = typeof(Chart) === 'function' ? Chart : window.Chart;
|
23
|
+
var helpers = Chart.helpers;
|
24
|
+
|
25
|
+
// Take the zoom namespace of Chart
|
26
|
+
var zoomNS = Chart.Zoom = Chart.Zoom || {};
|
27
|
+
|
28
|
+
// Where we store functions to handle different scale types
|
29
|
+
var zoomFunctions = zoomNS.zoomFunctions = zoomNS.zoomFunctions || {};
|
30
|
+
var panFunctions = zoomNS.panFunctions = zoomNS.panFunctions || {};
|
31
|
+
|
32
|
+
// Default options if none are provided
|
33
|
+
var defaultOptions = zoomNS.defaults = {
|
34
|
+
pan: {
|
35
|
+
enabled: true,
|
36
|
+
mode: 'xy',
|
37
|
+
speed: 20,
|
38
|
+
threshold: 10
|
39
|
+
},
|
40
|
+
zoom: {
|
41
|
+
enabled: true,
|
42
|
+
mode: 'xy',
|
43
|
+
sensitivity: 3
|
44
|
+
}
|
45
|
+
};
|
46
|
+
|
47
|
+
function directionEnabled(mode, dir) {
|
48
|
+
if (mode === undefined) {
|
49
|
+
return true;
|
50
|
+
} else if (typeof mode === 'string') {
|
51
|
+
return mode.indexOf(dir) !== -1;
|
52
|
+
}
|
53
|
+
|
54
|
+
return false;
|
55
|
+
}
|
56
|
+
|
57
|
+
function rangeMaxLimiter(zoomPanOptions, newMax) {
|
58
|
+
if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMax &&
|
59
|
+
!helpers.isNullOrUndef(zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes])) {
|
60
|
+
var rangeMax = zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes];
|
61
|
+
if (newMax > rangeMax) {
|
62
|
+
newMax = rangeMax;
|
63
|
+
}
|
64
|
+
}
|
65
|
+
return newMax;
|
66
|
+
}
|
67
|
+
|
68
|
+
function rangeMinLimiter(zoomPanOptions, newMin) {
|
69
|
+
if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMin &&
|
70
|
+
!helpers.isNullOrUndef(zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes])) {
|
71
|
+
var rangeMin = zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes];
|
72
|
+
if (newMin < rangeMin) {
|
73
|
+
newMin = rangeMin;
|
74
|
+
}
|
75
|
+
}
|
76
|
+
return newMin;
|
77
|
+
}
|
78
|
+
|
79
|
+
function zoomIndexScale(scale, zoom, center, zoomOptions) {
|
80
|
+
var labels = scale.chart.data.labels;
|
81
|
+
var minIndex = scale.minIndex;
|
82
|
+
var lastLabelIndex = labels.length - 1;
|
83
|
+
var maxIndex = scale.maxIndex;
|
84
|
+
var sensitivity = zoomOptions.sensitivity;
|
85
|
+
var chartCenter = scale.isHorizontal() ? scale.left + (scale.width/2) : scale.top + (scale.height/2);
|
86
|
+
var centerPointer = scale.isHorizontal() ? center.x : center.y;
|
87
|
+
|
88
|
+
zoomNS.zoomCumulativeDelta = zoom > 1 ? zoomNS.zoomCumulativeDelta + 1 : zoomNS.zoomCumulativeDelta - 1;
|
89
|
+
|
90
|
+
if (Math.abs(zoomNS.zoomCumulativeDelta) > sensitivity){
|
91
|
+
if(zoomNS.zoomCumulativeDelta < 0){
|
92
|
+
if(centerPointer >= chartCenter){
|
93
|
+
if (minIndex <= 0){
|
94
|
+
maxIndex = Math.min(lastLabelIndex, maxIndex + 1);
|
95
|
+
} else{
|
96
|
+
minIndex = Math.max(0, minIndex - 1);
|
97
|
+
}
|
98
|
+
} else if(centerPointer < chartCenter){
|
99
|
+
if (maxIndex >= lastLabelIndex){
|
100
|
+
minIndex = Math.max(0, minIndex - 1);
|
101
|
+
} else{
|
102
|
+
maxIndex = Math.min(lastLabelIndex, maxIndex + 1);
|
103
|
+
}
|
104
|
+
}
|
105
|
+
zoomNS.zoomCumulativeDelta = 0;
|
106
|
+
}
|
107
|
+
else if(zoomNS.zoomCumulativeDelta > 0){
|
108
|
+
if(centerPointer >= chartCenter){
|
109
|
+
minIndex = minIndex < maxIndex ? minIndex = Math.min(maxIndex, minIndex + 1) : minIndex;
|
110
|
+
} else if(centerPointer < chartCenter){
|
111
|
+
maxIndex = maxIndex > minIndex ? maxIndex = Math.max(minIndex, maxIndex - 1) : maxIndex;
|
112
|
+
}
|
113
|
+
zoomNS.zoomCumulativeDelta = 0;
|
114
|
+
}
|
115
|
+
scale.options.ticks.min = rangeMinLimiter(zoomOptions, labels[minIndex]);
|
116
|
+
scale.options.ticks.max = rangeMaxLimiter(zoomOptions, labels[maxIndex]);
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
function zoomTimeScale(scale, zoom, center, zoomOptions) {
|
121
|
+
var options = scale.options;
|
122
|
+
|
123
|
+
var range;
|
124
|
+
var min_percent;
|
125
|
+
if (scale.isHorizontal()) {
|
126
|
+
range = scale.right - scale.left;
|
127
|
+
min_percent = (center.x - scale.left) / range;
|
128
|
+
} else {
|
129
|
+
range = scale.bottom - scale.top;
|
130
|
+
min_percent = (center.y - scale.top) / range;
|
131
|
+
}
|
132
|
+
|
133
|
+
var max_percent = 1 - min_percent;
|
134
|
+
var newDiff = range * (zoom - 1);
|
135
|
+
|
136
|
+
var minDelta = newDiff * min_percent;
|
137
|
+
var maxDelta = newDiff * max_percent;
|
138
|
+
|
139
|
+
var newMin = scale.getValueForPixel(scale.getPixelForValue(scale.min) + minDelta);
|
140
|
+
var newMax = scale.getValueForPixel(scale.getPixelForValue(scale.max) - maxDelta);
|
141
|
+
|
142
|
+
var diffMinMax = newMax.diff(newMin);
|
143
|
+
var minLimitExceeded = rangeMinLimiter(zoomOptions, diffMinMax) != diffMinMax;
|
144
|
+
var maxLimitExceeded = rangeMaxLimiter(zoomOptions, diffMinMax) != diffMinMax;
|
145
|
+
|
146
|
+
if (!minLimitExceeded && !maxLimitExceeded) {
|
147
|
+
options.time.min = newMin;
|
148
|
+
options.time.max = newMax;
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
function zoomNumericalScale(scale, zoom, center, zoomOptions) {
|
153
|
+
var range = scale.max - scale.min;
|
154
|
+
var newDiff = range * (zoom - 1);
|
155
|
+
|
156
|
+
var cursorPixel = scale.isHorizontal() ? center.x : center.y;
|
157
|
+
var min_percent = (scale.getValueForPixel(cursorPixel) - scale.min) / range;
|
158
|
+
var max_percent = 1 - min_percent;
|
159
|
+
|
160
|
+
var minDelta = newDiff * min_percent;
|
161
|
+
var maxDelta = newDiff * max_percent;
|
162
|
+
|
163
|
+
scale.options.ticks.min = rangeMinLimiter(zoomOptions, scale.min + minDelta);
|
164
|
+
scale.options.ticks.max = rangeMaxLimiter(zoomOptions, scale.max - maxDelta);
|
165
|
+
}
|
166
|
+
|
167
|
+
function zoomScale(scale, zoom, center, zoomOptions) {
|
168
|
+
var fn = zoomFunctions[scale.options.type];
|
169
|
+
if (fn) {
|
170
|
+
fn(scale, zoom, center, zoomOptions);
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
function doZoom(chartInstance, zoom, center, whichAxes) {
|
175
|
+
var ca = chartInstance.chartArea;
|
176
|
+
if (!center) {
|
177
|
+
center = {
|
178
|
+
x: (ca.left + ca.right) / 2,
|
179
|
+
y: (ca.top + ca.bottom) / 2,
|
180
|
+
};
|
181
|
+
}
|
182
|
+
|
183
|
+
var zoomOptions = chartInstance.options.zoom;
|
184
|
+
|
185
|
+
if (zoomOptions && helpers.getValueOrDefault(zoomOptions.enabled, defaultOptions.zoom.enabled)) {
|
186
|
+
// Do the zoom here
|
187
|
+
var zoomMode = helpers.getValueOrDefault(chartInstance.options.zoom.mode, defaultOptions.zoom.mode);
|
188
|
+
zoomOptions.sensitivity = helpers.getValueOrDefault(chartInstance.options.zoom.sensitivity, defaultOptions.zoom.sensitivity);
|
189
|
+
|
190
|
+
// Which axe should be modified when figers were used.
|
191
|
+
var _whichAxes;
|
192
|
+
if (zoomMode == 'xy' && whichAxes !== undefined) {
|
193
|
+
// based on fingers positions
|
194
|
+
_whichAxes = whichAxes;
|
195
|
+
} else {
|
196
|
+
// no effect
|
197
|
+
_whichAxes = 'xy';
|
198
|
+
}
|
199
|
+
|
200
|
+
helpers.each(chartInstance.scales, function(scale, id) {
|
201
|
+
if (scale.isHorizontal() && directionEnabled(zoomMode, 'x') && directionEnabled(_whichAxes, 'x')) {
|
202
|
+
zoomOptions.scaleAxes = "x";
|
203
|
+
zoomScale(scale, zoom, center, zoomOptions);
|
204
|
+
} else if (!scale.isHorizontal() && directionEnabled(zoomMode, 'y') && directionEnabled(_whichAxes, 'y')) {
|
205
|
+
// Do Y zoom
|
206
|
+
zoomOptions.scaleAxes = "y";
|
207
|
+
zoomScale(scale, zoom, center, zoomOptions);
|
208
|
+
}
|
209
|
+
});
|
210
|
+
|
211
|
+
chartInstance.update(0);
|
212
|
+
}
|
213
|
+
}
|
214
|
+
|
215
|
+
function panIndexScale(scale, delta, panOptions) {
|
216
|
+
var labels = scale.chart.data.labels;
|
217
|
+
var lastLabelIndex = labels.length - 1;
|
218
|
+
var offsetAmt = Math.max((scale.ticks.length - ((scale.options.gridLines.offsetGridLines) ? 0 : 1)), 1);
|
219
|
+
var panSpeed = panOptions.speed;
|
220
|
+
var minIndex = scale.minIndex;
|
221
|
+
var step = Math.round(scale.width / (offsetAmt * panSpeed));
|
222
|
+
var maxIndex;
|
223
|
+
|
224
|
+
zoomNS.panCumulativeDelta += delta;
|
225
|
+
|
226
|
+
minIndex = zoomNS.panCumulativeDelta > step ? Math.max(0, minIndex -1) : zoomNS.panCumulativeDelta < -step ? Math.min(lastLabelIndex - offsetAmt + 1, minIndex + 1) : minIndex;
|
227
|
+
zoomNS.panCumulativeDelta = minIndex !== scale.minIndex ? 0 : zoomNS.panCumulativeDelta;
|
228
|
+
|
229
|
+
maxIndex = Math.min(lastLabelIndex, minIndex + offsetAmt - 1);
|
230
|
+
|
231
|
+
scale.options.ticks.min = rangeMinLimiter(panOptions, labels[minIndex]);
|
232
|
+
scale.options.ticks.max = rangeMaxLimiter(panOptions, labels[maxIndex]);
|
233
|
+
}
|
234
|
+
|
235
|
+
function panTimeScale(scale, delta, panOptions) {
|
236
|
+
var options = scale.options;
|
237
|
+
var limitedMax = rangeMaxLimiter(panOptions, scale.getValueForPixel(scale.getPixelForValue(scale.max) - delta));
|
238
|
+
var limitedMin = rangeMinLimiter(panOptions, scale.getValueForPixel(scale.getPixelForValue(scale.min) - delta));
|
239
|
+
|
240
|
+
var limitedTimeDelta = delta < 0 ? limitedMax - scale.max : limitedMin - scale.min;
|
241
|
+
|
242
|
+
options.time.max = scale.max + limitedTimeDelta;
|
243
|
+
options.time.min = scale.min + limitedTimeDelta;
|
244
|
+
}
|
245
|
+
|
246
|
+
function panNumericalScale(scale, delta, panOptions) {
|
247
|
+
var tickOpts = scale.options.ticks;
|
248
|
+
var start = scale.start,
|
249
|
+
end = scale.end;
|
250
|
+
|
251
|
+
if (tickOpts.reverse) {
|
252
|
+
tickOpts.max = scale.getValueForPixel(scale.getPixelForValue(start) - delta);
|
253
|
+
tickOpts.min = scale.getValueForPixel(scale.getPixelForValue(end) - delta);
|
254
|
+
} else {
|
255
|
+
tickOpts.min = scale.getValueForPixel(scale.getPixelForValue(start) - delta);
|
256
|
+
tickOpts.max = scale.getValueForPixel(scale.getPixelForValue(end) - delta);
|
257
|
+
}
|
258
|
+
tickOpts.min = rangeMinLimiter(panOptions, tickOpts.min);
|
259
|
+
tickOpts.max = rangeMaxLimiter(panOptions, tickOpts.max);
|
260
|
+
}
|
261
|
+
|
262
|
+
function panScale(scale, delta, panOptions) {
|
263
|
+
var fn = panFunctions[scale.options.type];
|
264
|
+
if (fn) {
|
265
|
+
fn(scale, delta, panOptions);
|
266
|
+
}
|
267
|
+
}
|
268
|
+
|
269
|
+
function doPan(chartInstance, deltaX, deltaY) {
|
270
|
+
var panOptions = chartInstance.options.pan;
|
271
|
+
if (panOptions && helpers.getValueOrDefault(panOptions.enabled, defaultOptions.pan.enabled)) {
|
272
|
+
var panMode = helpers.getValueOrDefault(chartInstance.options.pan.mode, defaultOptions.pan.mode);
|
273
|
+
panOptions.speed = helpers.getValueOrDefault(chartInstance.options.pan.speed, defaultOptions.pan.speed);
|
274
|
+
|
275
|
+
helpers.each(chartInstance.scales, function(scale, id) {
|
276
|
+
if (scale.isHorizontal() && directionEnabled(panMode, 'x') && deltaX !== 0) {
|
277
|
+
panOptions.scaleAxes = "x";
|
278
|
+
panScale(scale, deltaX, panOptions);
|
279
|
+
} else if (!scale.isHorizontal() && directionEnabled(panMode, 'y') && deltaY !== 0) {
|
280
|
+
panOptions.scaleAxes = "y";
|
281
|
+
panScale(scale, deltaY, panOptions);
|
282
|
+
}
|
283
|
+
});
|
284
|
+
|
285
|
+
chartInstance.update(0);
|
286
|
+
}
|
287
|
+
}
|
288
|
+
|
289
|
+
function positionInChartArea(chartInstance, position) {
|
290
|
+
return (position.x >= chartInstance.chartArea.left && position.x <= chartInstance.chartArea.right) &&
|
291
|
+
(position.y >= chartInstance.chartArea.top && position.y <= chartInstance.chartArea.bottom);
|
292
|
+
}
|
293
|
+
|
294
|
+
function getYAxis(chartInstance) {
|
295
|
+
var scales = chartInstance.scales;
|
296
|
+
|
297
|
+
for (var scaleId in scales) {
|
298
|
+
var scale = scales[scaleId];
|
299
|
+
|
300
|
+
if (!scale.isHorizontal()) {
|
301
|
+
return scale;
|
302
|
+
}
|
303
|
+
}
|
304
|
+
}
|
305
|
+
|
306
|
+
// Store these for later
|
307
|
+
zoomNS.zoomFunctions.category = zoomIndexScale;
|
308
|
+
zoomNS.zoomFunctions.time = zoomTimeScale;
|
309
|
+
zoomNS.zoomFunctions.linear = zoomNumericalScale;
|
310
|
+
zoomNS.zoomFunctions.logarithmic = zoomNumericalScale;
|
311
|
+
zoomNS.panFunctions.category = panIndexScale;
|
312
|
+
zoomNS.panFunctions.time = panTimeScale;
|
313
|
+
zoomNS.panFunctions.linear = panNumericalScale;
|
314
|
+
zoomNS.panFunctions.logarithmic = panNumericalScale;
|
315
|
+
// Globals for catergory pan and zoom
|
316
|
+
zoomNS.panCumulativeDelta = 0;
|
317
|
+
zoomNS.zoomCumulativeDelta = 0;
|
318
|
+
|
319
|
+
// Chartjs Zoom Plugin
|
320
|
+
var zoomPlugin = {
|
321
|
+
afterInit: function(chartInstance) {
|
322
|
+
helpers.each(chartInstance.scales, function(scale) {
|
323
|
+
scale.originalOptions = helpers.clone(scale.options);
|
324
|
+
});
|
325
|
+
|
326
|
+
chartInstance.resetZoom = function() {
|
327
|
+
helpers.each(chartInstance.scales, function(scale, id) {
|
328
|
+
var timeOptions = scale.options.time;
|
329
|
+
var tickOptions = scale.options.ticks;
|
330
|
+
|
331
|
+
if (timeOptions) {
|
332
|
+
timeOptions.min = scale.originalOptions.time.min;
|
333
|
+
timeOptions.max = scale.originalOptions.time.max;
|
334
|
+
}
|
335
|
+
|
336
|
+
if (tickOptions) {
|
337
|
+
tickOptions.min = scale.originalOptions.ticks.min;
|
338
|
+
tickOptions.max = scale.originalOptions.ticks.max;
|
339
|
+
}
|
340
|
+
});
|
341
|
+
|
342
|
+
helpers.each(chartInstance.data.datasets, function(dataset, id) {
|
343
|
+
dataset._meta = null;
|
344
|
+
});
|
345
|
+
|
346
|
+
chartInstance.update();
|
347
|
+
};
|
348
|
+
|
349
|
+
},
|
350
|
+
beforeInit: function(chartInstance) {
|
351
|
+
chartInstance.zoom = {};
|
352
|
+
|
353
|
+
var node = chartInstance.zoom.node = chartInstance.chart.ctx.canvas;
|
354
|
+
|
355
|
+
var options = chartInstance.options;
|
356
|
+
var panThreshold = helpers.getValueOrDefault(options.pan ? options.pan.threshold : undefined, zoomNS.defaults.pan.threshold);
|
357
|
+
if (!options.zoom || !options.zoom.enabled) {
|
358
|
+
return;
|
359
|
+
}
|
360
|
+
if (options.zoom.drag) {
|
361
|
+
// Only want to zoom horizontal axis
|
362
|
+
options.zoom.mode = 'x';
|
363
|
+
|
364
|
+
chartInstance.zoom._mouseDownHandler = function(event) {
|
365
|
+
chartInstance.zoom._dragZoomStart = event;
|
366
|
+
};
|
367
|
+
node.addEventListener('mousedown', chartInstance.zoom._mouseDownHandler);
|
368
|
+
|
369
|
+
chartInstance.zoom._mouseMoveHandler = function(event){
|
370
|
+
if (chartInstance.zoom._dragZoomStart) {
|
371
|
+
chartInstance.zoom._dragZoomEnd = event;
|
372
|
+
chartInstance.update(0);
|
373
|
+
}
|
374
|
+
};
|
375
|
+
node.addEventListener('mousemove', chartInstance.zoom._mouseMoveHandler);
|
376
|
+
|
377
|
+
chartInstance.zoom._mouseUpHandler = function(event){
|
378
|
+
if (chartInstance.zoom._dragZoomStart) {
|
379
|
+
var chartArea = chartInstance.chartArea;
|
380
|
+
var yAxis = getYAxis(chartInstance);
|
381
|
+
var beginPoint = chartInstance.zoom._dragZoomStart;
|
382
|
+
var offsetX = beginPoint.target.getBoundingClientRect().left;
|
383
|
+
var startX = Math.min(beginPoint.clientX, event.clientX) - offsetX;
|
384
|
+
var endX = Math.max(beginPoint.clientX, event.clientX) - offsetX;
|
385
|
+
var dragDistance = endX - startX;
|
386
|
+
var chartDistance = chartArea.right - chartArea.left;
|
387
|
+
var zoom = 1 + ((chartDistance - dragDistance) / chartDistance );
|
388
|
+
|
389
|
+
// Remove drag start and end before chart update to stop drawing selected area
|
390
|
+
chartInstance.zoom._dragZoomStart = null;
|
391
|
+
chartInstance.zoom._dragZoomEnd = null;
|
392
|
+
|
393
|
+
if (dragDistance > 0) {
|
394
|
+
doZoom(chartInstance, zoom, {
|
395
|
+
x: (dragDistance / 2) + startX,
|
396
|
+
y: (yAxis.bottom - yAxis.top) / 2,
|
397
|
+
});
|
398
|
+
}
|
399
|
+
}
|
400
|
+
};
|
401
|
+
node.addEventListener('mouseup', chartInstance.zoom._mouseUpHandler);
|
402
|
+
} else {
|
403
|
+
chartInstance.zoom._wheelHandler = function(event) {
|
404
|
+
var rect = event.target.getBoundingClientRect();
|
405
|
+
var offsetX = event.clientX - rect.left;
|
406
|
+
var offsetY = event.clientY - rect.top;
|
407
|
+
|
408
|
+
var center = {
|
409
|
+
x : offsetX,
|
410
|
+
y : offsetY
|
411
|
+
};
|
412
|
+
|
413
|
+
if (event.deltaY < 0) {
|
414
|
+
doZoom(chartInstance, 1.1, center);
|
415
|
+
} else {
|
416
|
+
doZoom(chartInstance, 0.909, center);
|
417
|
+
}
|
418
|
+
// Prevent the event from triggering the default behavior (eg. Content scrolling).
|
419
|
+
event.preventDefault();
|
420
|
+
};
|
421
|
+
|
422
|
+
node.addEventListener('wheel', chartInstance.zoom._wheelHandler);
|
423
|
+
}
|
424
|
+
|
425
|
+
if (Hammer) {
|
426
|
+
var mc = new Hammer.Manager(node);
|
427
|
+
mc.add(new Hammer.Pinch());
|
428
|
+
mc.add(new Hammer.Pan({
|
429
|
+
threshold: panThreshold
|
430
|
+
}));
|
431
|
+
|
432
|
+
// Hammer reports the total scaling. We need the incremental amount
|
433
|
+
var currentPinchScaling;
|
434
|
+
var handlePinch = function handlePinch(e) {
|
435
|
+
var diff = 1 / (currentPinchScaling) * e.scale;
|
436
|
+
var rect = e.target.getBoundingClientRect();
|
437
|
+
var offsetX = e.center.x - rect.left;
|
438
|
+
var offsetY = e.center.y - rect.top;
|
439
|
+
var center = {
|
440
|
+
x : offsetX,
|
441
|
+
y : offsetY
|
442
|
+
};
|
443
|
+
|
444
|
+
// fingers position difference
|
445
|
+
var x = Math.abs(e.pointers[0].clientX - e.pointers[1].clientX);
|
446
|
+
var y = Math.abs(e.pointers[0].clientY - e.pointers[1].clientY);
|
447
|
+
|
448
|
+
// diagonal fingers will change both (xy) axes
|
449
|
+
var p = x / y;
|
450
|
+
var xy;
|
451
|
+
if (p > 0.3 && p < 1.7) {
|
452
|
+
xy = 'xy';
|
453
|
+
}
|
454
|
+
// x axis
|
455
|
+
else if (x > y) {
|
456
|
+
xy = 'x';
|
457
|
+
}
|
458
|
+
// y axis
|
459
|
+
else {
|
460
|
+
xy = 'y';
|
461
|
+
}
|
462
|
+
|
463
|
+
doZoom(chartInstance, diff, center, xy);
|
464
|
+
|
465
|
+
// Keep track of overall scale
|
466
|
+
currentPinchScaling = e.scale;
|
467
|
+
};
|
468
|
+
|
469
|
+
mc.on('pinchstart', function(e) {
|
470
|
+
currentPinchScaling = 1; // reset tracker
|
471
|
+
});
|
472
|
+
mc.on('pinch', handlePinch);
|
473
|
+
mc.on('pinchend', function(e) {
|
474
|
+
handlePinch(e);
|
475
|
+
currentPinchScaling = null; // reset
|
476
|
+
zoomNS.zoomCumulativeDelta = 0;
|
477
|
+
});
|
478
|
+
|
479
|
+
var currentDeltaX = null, currentDeltaY = null, panning = false;
|
480
|
+
var handlePan = function handlePan(e) {
|
481
|
+
if (currentDeltaX !== null && currentDeltaY !== null) {
|
482
|
+
panning = true;
|
483
|
+
var deltaX = e.deltaX - currentDeltaX;
|
484
|
+
var deltaY = e.deltaY - currentDeltaY;
|
485
|
+
currentDeltaX = e.deltaX;
|
486
|
+
currentDeltaY = e.deltaY;
|
487
|
+
doPan(chartInstance, deltaX, deltaY);
|
488
|
+
}
|
489
|
+
};
|
490
|
+
|
491
|
+
mc.on('panstart', function(e) {
|
492
|
+
currentDeltaX = 0;
|
493
|
+
currentDeltaY = 0;
|
494
|
+
handlePan(e);
|
495
|
+
});
|
496
|
+
mc.on('panmove', handlePan);
|
497
|
+
mc.on('panend', function(e) {
|
498
|
+
currentDeltaX = null;
|
499
|
+
currentDeltaY = null;
|
500
|
+
zoomNS.panCumulativeDelta = 0;
|
501
|
+
setTimeout(function() { panning = false; }, 500);
|
502
|
+
});
|
503
|
+
|
504
|
+
chartInstance.zoom._ghostClickHandler = function(e) {
|
505
|
+
if (panning) {
|
506
|
+
e.stopImmediatePropagation();
|
507
|
+
e.preventDefault();
|
508
|
+
}
|
509
|
+
};
|
510
|
+
node.addEventListener('click', chartInstance.zoom._ghostClickHandler);
|
511
|
+
|
512
|
+
chartInstance._mc = mc;
|
513
|
+
}
|
514
|
+
},
|
515
|
+
|
516
|
+
beforeDatasetsDraw: function(chartInstance) {
|
517
|
+
var ctx = chartInstance.chart.ctx;
|
518
|
+
var chartArea = chartInstance.chartArea;
|
519
|
+
ctx.save();
|
520
|
+
ctx.beginPath();
|
521
|
+
|
522
|
+
if (chartInstance.zoom._dragZoomEnd) {
|
523
|
+
var yAxis = getYAxis(chartInstance);
|
524
|
+
var beginPoint = chartInstance.zoom._dragZoomStart;
|
525
|
+
var endPoint = chartInstance.zoom._dragZoomEnd;
|
526
|
+
var offsetX = beginPoint.target.getBoundingClientRect().left;
|
527
|
+
var startX = Math.min(beginPoint.clientX, endPoint.clientX) - offsetX;
|
528
|
+
var endX = Math.max(beginPoint.clientX, endPoint.clientX) - offsetX;
|
529
|
+
var rectWidth = endX - startX;
|
530
|
+
|
531
|
+
|
532
|
+
ctx.fillStyle = 'rgba(225,225,225,0.3)';
|
533
|
+
ctx.lineWidth = 5;
|
534
|
+
ctx.fillRect(startX, yAxis.top, rectWidth, yAxis.bottom - yAxis.top);
|
535
|
+
}
|
536
|
+
|
537
|
+
ctx.rect(chartArea.left, chartArea.top, chartArea.right - chartArea.left, chartArea.bottom - chartArea.top);
|
538
|
+
ctx.clip();
|
539
|
+
},
|
540
|
+
|
541
|
+
afterDatasetsDraw: function(chartInstance) {
|
542
|
+
chartInstance.chart.ctx.restore();
|
543
|
+
},
|
544
|
+
|
545
|
+
destroy: function(chartInstance) {
|
546
|
+
if (chartInstance.zoom) {
|
547
|
+
var options = chartInstance.options;
|
548
|
+
var node = chartInstance.zoom.node;
|
549
|
+
|
550
|
+
if (options.zoom && options.zoom.drag) {
|
551
|
+
node.removeEventListener('mousedown', chartInstance.zoom._mouseDownHandler);
|
552
|
+
node.removeEventListener('mousemove', chartInstance.zoom._mouseMoveHandler);
|
553
|
+
node.removeEventListener('mouseup', chartInstance.zoom._mouseUpHandler);
|
554
|
+
} else {
|
555
|
+
node.removeEventListener('wheel', chartInstance.zoom._wheelHandler);
|
556
|
+
}
|
557
|
+
|
558
|
+
if (Hammer) {
|
559
|
+
node.removeEventListener('click', chartInstance.zoom._ghostClickHandler);
|
560
|
+
}
|
561
|
+
|
562
|
+
delete chartInstance.zoom;
|
563
|
+
|
564
|
+
var mc = chartInstance._mc;
|
565
|
+
if (mc) {
|
566
|
+
mc.remove('pinchstart');
|
567
|
+
mc.remove('pinch');
|
568
|
+
mc.remove('pinchend');
|
569
|
+
mc.remove('panstart');
|
570
|
+
mc.remove('pan');
|
571
|
+
mc.remove('panend');
|
572
|
+
}
|
573
|
+
}
|
574
|
+
}
|
575
|
+
};
|
576
|
+
|
577
|
+
module.exports = zoomPlugin;
|
578
|
+
Chart.pluginService.register(zoomPlugin);
|
579
|
+
|
580
|
+
},{"chart.js":1,"hammerjs":1}]},{},[2]);
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Chartjs zoomable</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "chartjs/zoomable/application", media: "all" %>
|
9
|
+
<%= javascript_include_tag "chartjs/zoomable/application" %>
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
|
13
|
+
<%= yield %>
|
14
|
+
|
15
|
+
</body>
|
16
|
+
</html>
|
data/config/routes.rb
ADDED
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: chartjs-zoomable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mateusz Michalski
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-11-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.2.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.2.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: chartjs-ror
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.6.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.6.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: chartjs-zoomable lets you use Chartjs graphs using helper methods in
|
56
|
+
your views with zoom and pan functionality
|
57
|
+
email:
|
58
|
+
- nytorian@icloud.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- MIT-LICENSE
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- app/assets/config/chartjs_zoomable_manifest.js
|
67
|
+
- app/assets/javascripts/chartjs/zoomable/application.js
|
68
|
+
- app/assets/javascripts/chartjs/zoomable/chartjs-plugin-zoom.js
|
69
|
+
- app/assets/stylesheets/chartjs/zoomable/application.css
|
70
|
+
- app/controllers/chartjs/zoomable/application_controller.rb
|
71
|
+
- app/helpers/chartjs/zoomable/application_helper.rb
|
72
|
+
- app/jobs/chartjs/zoomable/application_job.rb
|
73
|
+
- app/mailers/chartjs/zoomable/application_mailer.rb
|
74
|
+
- app/models/chartjs/zoomable/application_record.rb
|
75
|
+
- app/views/layouts/chartjs/zoomable/application.html.erb
|
76
|
+
- config/routes.rb
|
77
|
+
- lib/chartjs/zoomable.rb
|
78
|
+
- lib/chartjs/zoomable/engine.rb
|
79
|
+
- lib/chartjs/zoomable/version.rb
|
80
|
+
- lib/tasks/chartjs/zoomable_tasks.rake
|
81
|
+
homepage: https://github.com/Nytorian/chartjs-zoomable
|
82
|
+
licenses:
|
83
|
+
- MIT
|
84
|
+
metadata: {}
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 2.5.1
|
102
|
+
signing_key:
|
103
|
+
specification_version: 4
|
104
|
+
summary: Simplifies usage of Chart.js in Rails views with zoom and pan plugin!
|
105
|
+
test_files: []
|