ahoy_captain 0.10.1 → 0.11.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.
Files changed (46) 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/helpers/countries.js +2261 -0
  10. data/app/components/ahoy_captain/combobox_component.html.erb +2 -2
  11. data/app/components/ahoy_captain/combobox_component.rb +1 -1
  12. data/app/components/ahoy_captain/dropdown_link_component.html.erb +2 -4
  13. data/app/components/ahoy_captain/dropdown_link_component.rb +4 -0
  14. data/app/components/ahoy_captain/previous_next_component.html.erb +8 -0
  15. data/app/components/ahoy_captain/previous_next_component.rb +11 -0
  16. data/app/components/ahoy_captain/stats/comparable_container_component.html.erb +1 -1
  17. data/app/components/ahoy_captain/stats/container_component.html.erb +1 -1
  18. data/app/components/ahoy_captain/sticky_nav_component.html.erb +26 -22
  19. data/app/components/ahoy_captain/sticky_nav_component.rb +8 -0
  20. data/app/components/ahoy_captain/tile_component.rb +7 -0
  21. data/app/controllers/ahoy_captain/locations/cities_controller.rb +22 -0
  22. data/app/controllers/ahoy_captain/locations/countries_controller.rb +22 -0
  23. data/app/controllers/ahoy_captain/locations/maps_controller.rb +24 -0
  24. data/app/controllers/ahoy_captain/locations/regions_controller.rb +22 -0
  25. data/app/helpers/ahoy_captain/application_helper.rb +0 -2
  26. data/app/models/ahoy_captain/range_from_params.rb +4 -0
  27. data/app/views/ahoy_captain/locations/maps/show.html.erb +3 -0
  28. data/app/views/ahoy_captain/properties/_form.html.erb +1 -1
  29. data/app/views/ahoy_captain/roots/show.html.erb +46 -54
  30. data/app/views/ahoy_captain/stats/base/index.html.erb +1 -0
  31. data/app/views/ahoy_captain/stats/show.html.erb +11 -6
  32. data/config/routes.rb +7 -3
  33. data/lib/ahoy_captain/filters_configuration.rb +5 -5
  34. data/lib/ahoy_captain/version.rb +1 -1
  35. data/lib/ahoy_captain.rb +1 -0
  36. metadata +102 -12
  37. data/app/assets/javascript/ahoy_captain/controllers/active_links_controller.js +0 -30
  38. data/app/controllers/ahoy_captain/cities_controller.rb +0 -20
  39. data/app/controllers/ahoy_captain/countries_controller.rb +0 -20
  40. data/app/controllers/ahoy_captain/regions_controller.rb +0 -20
  41. /data/app/views/ahoy_captain/{cities → locations/cities}/index.html+details.erb +0 -0
  42. /data/app/views/ahoy_captain/{cities → locations/cities}/index.html.erb +0 -0
  43. /data/app/views/ahoy_captain/{countries → locations/countries}/index.html+details.erb +0 -0
  44. /data/app/views/ahoy_captain/{countries → locations/countries}/index.html.erb +0 -0
  45. /data/app/views/ahoy_captain/{regions → locations/regions}/index.html+details.erb +0 -0
  46. /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: a8ee0971d696dceab133146c4f8a53a0e7f687c474497bc49ee8bf914bda2430
4
+ data.tar.gz: 3fc99cc0c51f8abbb07acbbf6a39e9f77a5b2adcabc6bd8e5e0583c0bd77b1af
5
5
  SHA512:
6
- metadata.gz: 823d78198fc0b83ed2535fa9482a5b468fee0aa7a8668db13a625b4b10b68b1910c345790b1ade4e3a9eea7a96f86f5042721dae6a162877f3ac78260437973c
7
- data.tar.gz: c0969c25e1bb0170eb49b310e48b7a4a5b5cde951d927d535af261f76a8c6c49f7bde837978e23e1e50042f0e706e39d90605d159860aefe179ac27842c3a179
6
+ metadata.gz: 99352a390c1f4641d5664882e5807909f4948f77f8b3ab3de89c4e5bad2773b78f6ff4ced0433d490ffa8db1050e6f5b284ad43086b4521328ab5e9af2f01a2c
7
+ data.tar.gz: 7b13a9571dcc60c6d4a8ab4677cdd6f0fd77123a192a5833e1e837a2f3117e5beed71ab4cb3ab00b66de3b8dafa134f30eeb72f9da98cddf5d3040b7c2dfb963
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
  }