ahoy_captain 0.10.1 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +23 -2
  3. data/app/assets/javascript/ahoy_captain/controllers/application_controller.js +12 -0
  4. data/app/assets/javascript/ahoy_captain/controllers/combobox_controller.js +50 -20
  5. data/app/assets/javascript/ahoy_captain/controllers/frame_link_controller.js +20 -0
  6. data/app/assets/javascript/ahoy_captain/controllers/line_chart_controller.js +67 -4
  7. data/app/assets/javascript/ahoy_captain/controllers/map_controller.js +47 -0
  8. data/app/assets/javascript/ahoy_captain/controllers/predicate_select_controller.js +1 -0
  9. data/app/assets/javascript/ahoy_captain/controllers/tile_controller.js +25 -1
  10. data/app/assets/javascript/ahoy_captain/helpers/countries.js +2261 -0
  11. data/app/components/ahoy_captain/combobox_component.html.erb +2 -2
  12. data/app/components/ahoy_captain/combobox_component.rb +1 -1
  13. data/app/components/ahoy_captain/comparison_link_component.html.erb +17 -0
  14. data/app/components/ahoy_captain/comparison_link_component.rb +10 -6
  15. data/app/components/ahoy_captain/dropdown_link_component.html.erb +2 -4
  16. data/app/components/ahoy_captain/dropdown_link_component.rb +4 -0
  17. data/app/components/ahoy_captain/previous_next_component.html.erb +8 -0
  18. data/app/components/ahoy_captain/previous_next_component.rb +11 -0
  19. data/app/components/ahoy_captain/stats/comparable_container_component.html.erb +1 -1
  20. data/app/components/ahoy_captain/stats/container_component.html.erb +1 -1
  21. data/app/components/ahoy_captain/sticky_nav_component.html.erb +26 -22
  22. data/app/components/ahoy_captain/sticky_nav_component.rb +8 -0
  23. data/app/components/ahoy_captain/tile_component.rb +7 -0
  24. data/app/controllers/ahoy_captain/locations/cities_controller.rb +22 -0
  25. data/app/controllers/ahoy_captain/locations/countries_controller.rb +22 -0
  26. data/app/controllers/ahoy_captain/locations/maps_controller.rb +24 -0
  27. data/app/controllers/ahoy_captain/locations/regions_controller.rb +22 -0
  28. data/app/helpers/ahoy_captain/application_helper.rb +0 -2
  29. data/app/models/ahoy_captain/range_from_params.rb +4 -1
  30. data/app/views/ahoy_captain/layouts/shared/_tile_loader.html.erb +12 -0
  31. data/app/views/ahoy_captain/locations/maps/show.html.erb +3 -0
  32. data/app/views/ahoy_captain/properties/_form.html.erb +1 -1
  33. data/app/views/ahoy_captain/roots/show.html.erb +89 -57
  34. data/app/views/ahoy_captain/stats/base/index.html.erb +1 -0
  35. data/app/views/ahoy_captain/stats/show.html.erb +13 -8
  36. data/config/routes.rb +7 -3
  37. data/lib/ahoy_captain/filters_configuration.rb +5 -5
  38. data/lib/ahoy_captain/version.rb +1 -1
  39. data/lib/ahoy_captain.rb +1 -0
  40. metadata +104 -12
  41. data/app/assets/javascript/ahoy_captain/controllers/active_links_controller.js +0 -30
  42. data/app/controllers/ahoy_captain/cities_controller.rb +0 -20
  43. data/app/controllers/ahoy_captain/countries_controller.rb +0 -20
  44. data/app/controllers/ahoy_captain/regions_controller.rb +0 -20
  45. /data/app/views/ahoy_captain/{cities → locations/cities}/index.html+details.erb +0 -0
  46. /data/app/views/ahoy_captain/{cities → locations/cities}/index.html.erb +0 -0
  47. /data/app/views/ahoy_captain/{countries → locations/countries}/index.html+details.erb +0 -0
  48. /data/app/views/ahoy_captain/{countries → locations/countries}/index.html.erb +0 -0
  49. /data/app/views/ahoy_captain/{regions → locations/regions}/index.html+details.erb +0 -0
  50. /data/app/views/ahoy_captain/{regions → locations/regions}/index.html.erb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b2500018e9b54eebbe3265041ed28c4a545698362738c9e2dfe34db83edc221
4
- data.tar.gz: dc8b920328f271d5ff63047360b6e5ef1b06ba12b1598346005063bc3548d102
3
+ metadata.gz: 550735e026fe60170552c09149ee5dceb1d874020c131f49c9a79a6d1c04982f
4
+ data.tar.gz: 9a782cddeb96ba6726980ae8f0c5b332f51756b8114b88dc63af91a2739776ea
5
5
  SHA512:
6
- metadata.gz: 823d78198fc0b83ed2535fa9482a5b468fee0aa7a8668db13a625b4b10b68b1910c345790b1ade4e3a9eea7a96f86f5042721dae6a162877f3ac78260437973c
7
- data.tar.gz: c0969c25e1bb0170eb49b310e48b7a4a5b5cde951d927d535af261f76a8c6c49f7bde837978e23e1e50042f0e706e39d90605d159860aefe179ac27842c3a179
6
+ metadata.gz: 021c101c8ff731e860b85baab37836fdb9f1dc5a038c90cd5411b9f0e628b8918823fdc7446164620414bce1a20560a7284dd6d63753ad4ab6c1f1541aed17d8
7
+ data.tar.gz: b0a5fc48678367faa56299f2462b92edf3331adc4df3f0cc685b68584cfea4604d76437528007ce27e5c3173312945d514e2399a65efad1d4b473e2a134b6690
data/Rakefile CHANGED
@@ -1,3 +1,24 @@
1
- require "bundler/setup"
1
+ # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'Caffeinate'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
20
+ load 'rails/tasks/engine.rake'
21
+
22
+ load 'rails/tasks/statistics.rake'
23
+
24
+ require 'bundler/gem_tasks'
@@ -10,6 +10,18 @@ export default class extends Controller {
10
10
  }, 1000 * 30);
11
11
  });
12
12
  }
13
+
14
+ document.querySelectorAll('a[data-turbo-frame]').forEach(link => {
15
+ const frameSelector = link.dataset.turboFrame;
16
+ const frame = document.querySelector(`turbo-frame#${frameSelector}`);
17
+ if(frame) {
18
+ const src = frame.src;
19
+ if(link.href.includes(src)) {
20
+ link.classList.add('text-primary', 'font-semibold')
21
+ }
22
+ }
23
+
24
+ })
13
25
  }
14
26
 
15
27
  comboboxInit(event) {
@@ -22,7 +22,7 @@ export default class extends Controller {
22
22
  input: String,
23
23
  highlightedIndex: Number,
24
24
  singleOption: Boolean,
25
- freeChoice: Boolean,
25
+ freeChoice: { type: Boolean, default: false },
26
26
  selected: Array,
27
27
  url: String,
28
28
  query: String
@@ -51,11 +51,33 @@ export default class extends Controller {
51
51
  value: this,
52
52
  });
53
53
 
54
+ const targetNode = this.selectTarget;
55
+ const config = { attributes: true };
56
+
57
+ const callback = (mutationList, observer) => {
58
+ for (const mutation of mutationList) {
59
+ if (mutation.type === "attributes") {
60
+ this.handleNameChange()
61
+ }
62
+ }
63
+ };
64
+
65
+ const observer = new MutationObserver(callback);
66
+ observer.observe(targetNode, config);
67
+
68
+
54
69
  window.dispatchEvent(new CustomEvent('combobox:init', { detail: { combobox: this } }))
55
70
  this.search = new URLSearchParams(window.location.search);
56
71
  this.search.delete(this.selectTarget.name)
57
72
  }
58
73
 
74
+ handleNameChange() {
75
+ if(this.selectTarget.name.includes("_cont]")) {
76
+ this.freeChoiceValue = true
77
+ } else {
78
+ this.freeChoiceValue = false
79
+ }
80
+ }
59
81
  checkDisabledState() {
60
82
  if (this.disabledValue) {
61
83
  this.element.classList.add('opacity-80', 'cursor-default', 'pointer-events-none');
@@ -71,29 +93,30 @@ export default class extends Controller {
71
93
 
72
94
  fetchOptions(query) {
73
95
  if(this.disabledValue) { return }
74
- this.isLoadingValue = true;
75
- this.isOpenValue = true;
76
96
 
77
- const searchParams = new URLSearchParams(this.search.toString());
78
- const formData = new FormData(this.element.form);
97
+ if(this.freeChoiceValue) {
98
+ this.isLoadingValue = false;
99
+ this.highlightedIndexValue = 0;
100
+ this.optionsValue = [{ text: query, value: query }];
101
+ this.isOpenValue = true;
102
+
103
+ } else {
104
+ this.isLoadingValue = true;
105
+ this.isOpenValue = true;
79
106
 
80
- let deleted = [];
81
- for (const [key, value] of formData) {
82
- if(!deleted.includes(key)) {
83
- searchParams.delete(key)
84
- deleted.push(key)
85
- }
86
- }
107
+ const formData = new FormData(this.selectTarget.form);
108
+ const searchParams = new URLSearchParams([...formData.entries()]);
87
109
 
88
- searchParams.delete(this.element.name);
89
- searchParams.delete(this.queryValue);
90
- searchParams.set(this.queryValue, query);
110
+ searchParams.delete(this.selectTarget.name);
111
+ searchParams.delete(this.queryValue);
112
+ searchParams.set(this.queryValue, query);
91
113
 
92
- fetch(`${this.urlValue}?${searchParams.toString()}`).then(resp => resp.json()).then(loadedOptions => {
93
- this.isLoadingValue = false;
94
- this.highlightedIndexValue = 0;
95
- this.optionsValue = loadedOptions.map(option => ({ text: option.text, value: option.value }));
96
- });
114
+ fetch(`${this.urlValue}?${searchParams.toString()}`).then(resp => resp.json()).then(loadedOptions => {
115
+ this.isLoadingValue = false;
116
+ this.highlightedIndexValue = 0;
117
+ this.optionsValue = loadedOptions.map(option => ({ text: option.text, value: option.value }));
118
+ });
119
+ }
97
120
  }
98
121
 
99
122
  highlight(element) {
@@ -328,6 +351,13 @@ export default class extends Controller {
328
351
  }
329
352
  }
330
353
 
354
+ freeChoiceValueChanged(current, prev) {
355
+ if(this.selectedValue.filter(value => value.freeChoice).length) {
356
+ console.log("free choice changed")
357
+ this.setSelected([])
358
+ }
359
+
360
+ }
331
361
  disabledValueChanged(current) {
332
362
  if(current) {
333
363
  this.isOpenValue = false
@@ -0,0 +1,20 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static targets = ["link", "alt"]
5
+ static values = {
6
+ classes: { type: Array, default: ["text-primary", "font-semibold"] }
7
+ }
8
+
9
+ connect() {
10
+ this.element.addEventListener('click', () => {
11
+ const frame = this.element.dataset.turboFrame;
12
+ const otherLinks = document.querySelectorAll(`[data-turbo-frame="${frame}"]`);
13
+ otherLinks.forEach(link => {
14
+ link.classList.remove('text-primary', 'font-semibold');
15
+ })
16
+
17
+ this.element.classList.add("text-primary", "font-semibold")
18
+ })
19
+ }
20
+ }
@@ -33,6 +33,7 @@ export default class extends Controller {
33
33
 
34
34
  connect() {
35
35
  const onClick = (e) => {
36
+ if(drag) { return }
36
37
  const element = this.chart.getElementsAtEventForMode(e, 'index', { intersect: false })[0];
37
38
  const searchParams = new URLSearchParams(window.location.search);
38
39
 
@@ -76,8 +77,7 @@ export default class extends Controller {
76
77
  }
77
78
 
78
79
  const typeForDate = this.comparisonValue === 'year' ? 'long' : "short"
79
- this.chart = new Chart(this.element,
80
- {
80
+ const options = {
81
81
  type: 'line',
82
82
  data: {
83
83
  labels: Object.keys(this.currentValue),
@@ -139,8 +139,71 @@ export default class extends Controller {
139
139
  }
140
140
  }
141
141
  }
142
- },
143
- );
142
+ }
143
+ this.chart = new Chart(this.element, options);
144
+
145
+
146
+ var canvas = this.element;
147
+ var overlay = document.getElementById('overlay');
148
+ var startIndex = 0;
149
+ overlay.width = canvas.width;
150
+ overlay.height = canvas.height;
151
+ var selectionContext = overlay.getContext('2d');
152
+ var selectionRect = {
153
+ w: 0,
154
+ startX: 0,
155
+ startY: 0
156
+ };
157
+ var drag = false;
158
+ canvas.addEventListener('pointerdown', evt => {
159
+ const points = this.chart.getElementsAtEventForMode(evt, 'index', {
160
+ intersect: false
161
+ });
162
+
163
+ startIndex = points[0].index;
164
+ const rect = canvas.getBoundingClientRect();
165
+ selectionRect.startX = evt.clientX - rect.left;
166
+ selectionRect.startY = this.chart.chartArea.top;
167
+ drag = true;
168
+ });
169
+ canvas.addEventListener('pointermove', evt => {
170
+ const rect = canvas.getBoundingClientRect();
171
+ if (drag) {
172
+
173
+ const rect = canvas.getBoundingClientRect();
174
+ selectionContext.fillStyle = getCSS('--p')
175
+ selectionRect.w = (evt.clientX - rect.left) - selectionRect.startX;
176
+ selectionContext.globalAlpha = 0.5;
177
+ selectionContext.clearRect(0, 0, canvas.width, canvas.height);
178
+ selectionContext.fillRect(selectionRect.startX,
179
+ selectionRect.startY,
180
+ selectionRect.w,
181
+ this.chart.chartArea.bottom - this.chart.chartArea.top);
182
+ } else {
183
+ selectionContext.clearRect(0, 0, canvas.width, canvas.height);
184
+ var x = evt.clientX - rect.left;
185
+ if (x > this.chart.chartArea.left) {
186
+ selectionContext.fillStyle = getCSS('--p')
187
+ selectionContext.fillRect(x,
188
+ this.chart.chartArea.top,
189
+ 1,
190
+ this.chart.chartArea.bottom - this.chart.chartArea.top);
191
+ }
192
+ }
193
+ });
194
+ canvas.addEventListener('pointerup', evt => {
195
+ const points = this.chart.getElementsAtEventForMode(evt, 'index', {
196
+ intersect: false
197
+ });
198
+ const dates = [options.data.labels[startIndex], options.data.labels[points[0].index]].sort()
199
+ const searchParams = new URLSearchParams(window.location.search);
200
+ searchParams.set('start_date', dates[0])
201
+ searchParams.set('end_date', dates[1])
202
+ searchParams.delete('period')
203
+ searchParams.delete('compare_to_start_date')
204
+ searchParams.delete('compare_to_end_date')
205
+ Turbo.visit(window.location.pathname + "?" + searchParams.toString())
206
+ });
144
207
  }
145
208
 
146
209
  formatLabel(label) {
@@ -0,0 +1,47 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import "chartjs-chart-geo";
3
+ import CountryMap from "helpers/countries"
4
+
5
+ export default class extends Controller {
6
+ static values = {
7
+ data: Object
8
+ }
9
+ connect() {
10
+ fetch('https://unpkg.com/world-atlas/countries-50m.json').then((r) => r.json()).then((data) => {
11
+ const countries = ChartGeo.topojson.feature(data, data.objects.countries).features;
12
+ const numericToCode = {};
13
+ Object.keys(CountryMap).forEach(key => { numericToCode[CountryMap[key]['Numeric code']] = key })
14
+
15
+ countries.forEach(country => {
16
+ const abbrev = numericToCode[country.id];
17
+ country.value = this.dataValue[abbrev]
18
+ })
19
+
20
+ const chart = new Chart(this.element.getContext("2d"), {
21
+ type: 'choropleth',
22
+ data: {
23
+ labels: countries.map((d) => d.properties.name),
24
+ datasets: [{
25
+ label: 'Countries',
26
+ data: countries.map((d) => ({feature: d, value: d.value || 0 })),
27
+ }]
28
+ },
29
+ options: {
30
+ showOutline: false,
31
+ showGraticule: false,
32
+ plugins: {
33
+ legend: {
34
+ display: false
35
+ },
36
+ },
37
+ scales: {
38
+ projection: {
39
+ axis: 'x',
40
+ projection: 'equalEarth'
41
+ }
42
+ }
43
+ }
44
+ });
45
+ });
46
+ }
47
+ }
@@ -4,6 +4,7 @@ export default class extends Controller {
4
4
  static targets = ["select"]
5
5
 
6
6
  handleChange(event) {
7
+ this.selectTarget.dataset.predicate = event.target.value
7
8
  this.selectTarget.name = event.target.value
8
9
  }
9
10
  }
@@ -2,8 +2,32 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
4
  static targets = ["title"]
5
+ connect() {
6
+ this.frame = this.element.querySelector('turbo-frame');
7
+ const targetNode = this.frame;
8
+ const config = { attributes: true };
9
+
10
+ const callback = (mutationList, observer) => {
11
+ for (const mutation of mutationList) {
12
+ if (mutation.type === "attributes") {
13
+ this.handleFrameLoad(mutation.attributeName)
14
+ }
15
+ }
16
+ };
17
+
18
+ const observer = new MutationObserver(callback);
19
+ observer.observe(targetNode, config);
20
+ }
21
+
22
+ handleFrameLoad(status) {
23
+ if(!this.frame.hasAttribute('skeleton')) {
24
+ if(status === 'busy') {
25
+ this.frame.innerHTML = document.querySelector('#tile-loader-template').innerHTML;
26
+ }
27
+ }
28
+ }
29
+
5
30
  setTitle(event) {
6
31
  this.titleTarget.innerHTML = event.target.title || event.target.text
7
32
  }
8
-
9
33
  }