Skip to content
This repository was archived by the owner on Jul 25, 2025. It is now read-only.

Commit 3a687a0

Browse files
committed
Add responsive resize handling to chart controllers and update HTML structure
- Implemented resize listeners in various chart controllers to enhance responsiveness on window resize. - Added debounce functionality to optimize performance during resize events. - Updated SVG attributes in chart rendering for better responsiveness. - Refactored HTML structure in retirement calculator views for improved layout and styling.
1 parent 9a65d38 commit 3a687a0

20 files changed

Lines changed: 662 additions & 16 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,4 @@ scripts/
7575
tasks.json
7676
/tasks/
7777
.cursor/mcp.json
78+
CLAUDE.md

app/avo/resources/redirect.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class Avo::Resources::Redirect < Avo::BaseResource
2+
self.title = :source_path
3+
self.includes = []
4+
self.search = {
5+
query: -> { query.ransack(source_path_cont: params[:q], destination_path_cont: params[:q], m: "or").result(distinct: false) }
6+
}
7+
8+
def fields
9+
field :id, as: :id, hide_on: [ :new, :edit ]
10+
11+
field :source_path, as: :text, required: true, help: "Examples: /old-page, /blog/*, ^/article/(\\d+)"
12+
field :destination_path, as: :text, required: true, help: "Examples: /new-page, /posts, /posts/\\1 (for regex capture)"
13+
14+
field :redirect_type, as: :select, required: true, options: Redirect::REDIRECT_TYPES.map { |type| [ type.humanize, type ] }
15+
16+
field :pattern_type, as: :select, required: true, options: Redirect::PATTERN_TYPES.map { |type| [ type.humanize, type ] }
17+
18+
field :active, as: :boolean, required: true
19+
field :priority, as: :number, required: true, help: "Lower numbers have higher priority"
20+
21+
field :created_at, as: :date_time, hide_on: [ :new, :edit ]
22+
field :updated_at, as: :date_time, hide_on: [ :new, :edit ]
23+
end
24+
25+
end

app/controllers/application_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
class ApplicationController < ActionController::Base
22
include SentryContext
3+
include Redirectable
34

45
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
56
# allow_browser versions: :modern
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# This controller has been generated to enable Rails' resource routes.
2+
# More information on https://docs.avohq.io/3.0/controllers.html
3+
class Avo::RedirectsController < Avo::ResourcesController
4+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module Redirectable
2+
extend ActiveSupport::Concern
3+
4+
included do
5+
before_action :check_for_redirects
6+
end
7+
8+
private
9+
10+
def check_for_redirects
11+
return if request.post? || request.patch? || request.put? || request.delete?
12+
13+
redirect_rule = find_matching_redirect(request.path)
14+
15+
if redirect_rule
16+
destination = redirect_rule.process_destination(request.path)
17+
redirect_to destination, status: redirect_rule.status_code
18+
end
19+
end
20+
21+
def find_matching_redirect(request_path)
22+
Redirect.active.by_priority.find do |redirect|
23+
redirect.matches_path?(request_path)
24+
end
25+
end
26+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class RedirectsController < ApplicationController
2+
skip_before_action :check_for_redirects
3+
4+
def catch_all
5+
redirect_rule = find_matching_redirect(request.path)
6+
7+
if redirect_rule
8+
destination = redirect_rule.process_destination(request.path)
9+
redirect_to destination, status: redirect_rule.status_code
10+
else
11+
render file: Rails.root.join("public", "404.html"), status: :not_found, layout: false
12+
end
13+
end
14+
15+
private
16+
17+
def find_matching_redirect(request_path)
18+
Redirect.active.by_priority.find do |redirect|
19+
redirect.matches_path?(request_path)
20+
end
21+
end
22+
end

app/javascript/controllers/stack_bar_chart_controller.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default class extends Controller {
88

99
#data = [];
1010
#isMobile = window.innerWidth <= 768;
11+
#resizeHandler = null;
1112

1213
dataValueChanged(value) {
1314
this.#data = value;
@@ -16,21 +17,57 @@ export default class extends Controller {
1617

1718
connect() {
1819
this.#drawChart();
20+
this.#addResizeListener();
21+
}
22+
23+
disconnect() {
24+
this.#removeResizeListener();
25+
}
26+
27+
#addResizeListener() {
28+
this.#resizeHandler = this.#debounce(() => {
29+
this.#isMobile = window.innerWidth <= 768;
30+
this.#drawChart();
31+
}, 250);
32+
window.addEventListener('resize', this.#resizeHandler);
33+
}
34+
35+
#removeResizeListener() {
36+
if (this.#resizeHandler) {
37+
window.removeEventListener('resize', this.#resizeHandler);
38+
}
39+
}
40+
41+
#debounce(func, wait) {
42+
let timeout;
43+
return function executedFunction(...args) {
44+
const later = () => {
45+
clearTimeout(timeout);
46+
func(...args);
47+
};
48+
clearTimeout(timeout);
49+
timeout = setTimeout(later, wait);
50+
};
1951
}
2052

2153
#drawChart() {
2254
const data = this.#data.segments;
2355
const desiredHomePrice = this.#data.desiredHomePrice;
2456

57+
// Use container width instead of fixed widths
58+
const containerWidth = this.element.clientWidth || 320;
2559
const margin = this.#isMobile ? { top: 20, right: 10, bottom: 40, left: 10 } : { top: 20, right: 30, bottom: 40, left: 50 };
26-
const width = (this.#isMobile ? 350 : 600) - margin.left - margin.right;
60+
const width = Math.max(280, containerWidth - margin.left - margin.right);
2761
const height = 400 - margin.top - margin.bottom;
2862

2963
const svg = d3.select(this.element)
3064
.html("")
3165
.append("svg")
3266
.attr("width", width + margin.left + margin.right)
3367
.attr("height", height + margin.top + margin.bottom)
68+
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
69+
.style("max-width", "100%")
70+
.style("height", "auto")
3471
.append("g")
3572
.attr("transform", `translate(${margin.left},${margin.top})`);
3673

app/javascript/controllers/time_series_bar_chart_controller.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ export default class extends Controller {
1111
#d3GroupMemo = null;
1212
#d3SvgMemo = null;
1313
#data = [];
14+
#resizeHandler = null;
1415

1516
connect() {
1617
this.#install();
18+
this.#addResizeListener();
1719
document.addEventListener("turbo:load", this.#reinstall);
1820
}
1921

2022
disconnect() {
2123
this.#teardown();
24+
this.#removeResizeListener();
2225
document.removeEventListener("turbo:load", this.#reinstall);
2326
}
2427

@@ -46,6 +49,31 @@ export default class extends Controller {
4649
this.#installTooltip();
4750
}
4851

52+
#addResizeListener() {
53+
this.#resizeHandler = this.#debounce(() => {
54+
this.#reinstall();
55+
}, 250);
56+
window.addEventListener('resize', this.#resizeHandler);
57+
}
58+
59+
#removeResizeListener() {
60+
if (this.#resizeHandler) {
61+
window.removeEventListener('resize', this.#resizeHandler);
62+
}
63+
}
64+
65+
#debounce(func, wait) {
66+
let timeout;
67+
return function executedFunction(...args) {
68+
const later = () => {
69+
clearTimeout(timeout);
70+
func(...args);
71+
};
72+
clearTimeout(timeout);
73+
timeout = setTimeout(later, wait);
74+
};
75+
}
76+
4977
// Normalize data when it is set
5078
dataValueChanged(value) {
5179
this.#data = value.map(d => ({
@@ -311,7 +339,9 @@ export default class extends Controller {
311339
.append("svg")
312340
.attr("width", this.#initialElementWidth)
313341
.attr("height", this.#initialElementHeight)
314-
.attr("viewBox", [ 0, 0, this.#initialElementWidth, this.#initialElementHeight ]);
342+
.attr("viewBox", [ 0, 0, this.#initialElementWidth, this.#initialElementHeight ])
343+
.style("max-width", "100%")
344+
.style("height", "auto");
315345

316346
this.#d3SvgMemo.append("defs")
317347
.append("clipPath")

app/javascript/controllers/time_series_bogleheads_growth_chart_controller.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default class extends Controller {
1212

1313
#initialElementWidth = 0;
1414
#initialElementHeight = 0;
15+
#resizeHandler = null;
1516

1617
connect() {
1718
console.log(this.showLegendValue, this.values);
@@ -25,6 +26,57 @@ export default class extends Controller {
2526
this.#drawLegend();
2627
}
2728
this.#installTooltip();
29+
this.#addResizeListener();
30+
}
31+
32+
disconnect() {
33+
this.#removeResizeListener();
34+
}
35+
36+
#redrawChart() {
37+
this.#clearChart();
38+
this.#rememberInitialElementSize();
39+
this.#drawGridlines();
40+
this.#drawBogleheadsGrowthChart();
41+
if (this.useLabelsValue) {
42+
this.#drawXAxis();
43+
}
44+
if (this.showLegendValue) {
45+
this.#drawLegend();
46+
}
47+
this.#installTooltip();
48+
}
49+
50+
#clearChart() {
51+
this.#d3TooltipMemo = null;
52+
this.#d3GroupMemo = null;
53+
this.#d3SvgMemo = null;
54+
this.#d3Element.selectAll("*").remove();
55+
}
56+
57+
#addResizeListener() {
58+
this.#resizeHandler = this.#debounce(() => {
59+
this.#redrawChart();
60+
}, 250);
61+
window.addEventListener('resize', this.#resizeHandler);
62+
}
63+
64+
#removeResizeListener() {
65+
if (this.#resizeHandler) {
66+
window.removeEventListener('resize', this.#resizeHandler);
67+
}
68+
}
69+
70+
#debounce(func, wait) {
71+
let timeout;
72+
return function executedFunction(...args) {
73+
const later = () => {
74+
clearTimeout(timeout);
75+
func(...args);
76+
};
77+
clearTimeout(timeout);
78+
timeout = setTimeout(later, wait);
79+
};
2880
}
2981

3082
// Normalize data when it is set
@@ -352,7 +404,9 @@ export default class extends Controller {
352404
.append("svg")
353405
.attr("width", this.#initialElementWidth)
354406
.attr("height", this.#initialElementHeight)
355-
.attr("viewBox", [0, 0, this.#initialElementWidth, this.#initialElementHeight]);
407+
.attr("viewBox", [0, 0, this.#initialElementWidth, this.#initialElementHeight])
408+
.style("max-width", "100%")
409+
.style("height", "auto");
356410

357411
this.#d3SvgMemo.append("defs")
358412
.append("clipPath")

app/javascript/controllers/time_series_exchange_rate_chart_controller.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default class extends Controller {
1111

1212
#initialElementWidth = 0;
1313
#initialElementHeight = 0;
14+
#resizeHandler = null;
1415

1516
connect() {
1617
this.#rememberInitialElementSize();
@@ -20,6 +21,54 @@ export default class extends Controller {
2021
this.#drawXAxis();
2122
this.#installTooltip();
2223
this.hideLoading();
24+
this.#addResizeListener();
25+
}
26+
27+
disconnect() {
28+
this.#removeResizeListener();
29+
}
30+
31+
#redrawChart() {
32+
this.#clearChart();
33+
this.#rememberInitialElementSize();
34+
this.showLoading();
35+
this.#drawGridlines();
36+
this.#drawChart();
37+
this.#drawXAxis();
38+
this.#installTooltip();
39+
this.hideLoading();
40+
}
41+
42+
#clearChart() {
43+
this.#d3TooltipMemo = null;
44+
this.#d3GroupMemo = null;
45+
this.#d3SvgMemo = null;
46+
this.#d3Element.selectAll("*").remove();
47+
}
48+
49+
#addResizeListener() {
50+
this.#resizeHandler = this.#debounce(() => {
51+
this.#redrawChart();
52+
}, 250);
53+
window.addEventListener('resize', this.#resizeHandler);
54+
}
55+
56+
#removeResizeListener() {
57+
if (this.#resizeHandler) {
58+
window.removeEventListener('resize', this.#resizeHandler);
59+
}
60+
}
61+
62+
#debounce(func, wait) {
63+
let timeout;
64+
return function executedFunction(...args) {
65+
const later = () => {
66+
clearTimeout(timeout);
67+
func(...args);
68+
};
69+
clearTimeout(timeout);
70+
timeout = setTimeout(later, wait);
71+
};
2372
}
2473

2574
showLoading() {
@@ -254,7 +303,9 @@ export default class extends Controller {
254303
.append("svg")
255304
.attr("width", this.#initialElementWidth)
256305
.attr("height", this.#initialElementHeight)
257-
.attr("viewBox", [0, 0, this.#initialElementWidth, this.#initialElementHeight]);
306+
.attr("viewBox", [0, 0, this.#initialElementWidth, this.#initialElementHeight])
307+
.style("max-width", "100%")
308+
.style("height", "auto");
258309
}
259310

260311
get #d3Element() {

0 commit comments

Comments
 (0)