rails_pulse 0.2.3 → 0.2.4
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 +4 -4
- data/README.md +1 -1
- data/app/controllers/concerns/chart_table_concern.rb +2 -3
- data/app/controllers/rails_pulse/application_controller.rb +10 -3
- data/app/controllers/rails_pulse/queries_controller.rb +1 -1
- data/app/controllers/rails_pulse/requests_controller.rb +2 -1
- data/app/controllers/rails_pulse/routes_controller.rb +1 -1
- data/app/helpers/rails_pulse/application_helper.rb +47 -2
- data/app/helpers/rails_pulse/chart_helper.rb +32 -2
- data/app/javascript/rails_pulse/application.js +3 -54
- data/app/javascript/rails_pulse/controllers/chart_controller.js +229 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +9 -14
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +27 -33
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +0 -2
- data/app/jobs/rails_pulse/cleanup_job.rb +0 -2
- data/app/jobs/rails_pulse/summary_job.rb +0 -2
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +1 -1
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +1 -1
- data/app/views/rails_pulse/components/_metric_card.html.erb +2 -2
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +1 -1
- data/app/views/rails_pulse/components/_table_pagination.html.erb +8 -6
- data/app/views/rails_pulse/csp_test/show.html.erb +1 -1
- data/app/views/rails_pulse/dashboard/charts/_bar_chart.html.erb +1 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +4 -3
- data/app/views/rails_pulse/queries/index.html.erb +2 -1
- data/app/views/rails_pulse/queries/show.html.erb +2 -1
- data/app/views/rails_pulse/routes/index.html.erb +2 -1
- data/app/views/rails_pulse/routes/show.html.erb +2 -1
- data/config/importmap.rb +1 -1
- data/lib/rails_pulse/engine.rb +0 -30
- data/lib/rails_pulse/version.rb +1 -1
- data/public/rails-pulse-assets/csp-test.js +10 -10
- data/public/rails-pulse-assets/rails-pulse.js +48 -48
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- metadata +5 -25
- data/config/initializers/rails_charts_csp_patch.rb +0 -75
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 943ab1e08e81dbe5ede71b7d3c9ca526a65c9a0ce1767c7f9c37cee8883925cf
|
|
4
|
+
data.tar.gz: b3661c728215dc4759e6a3e04dd49dc113d13155972ed30f6ff35d0a2a07b10c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db5d711b9d8931c6aa489b9069f05d16514a997b9798c743f2c2998611797dbc3dbdcfdcd12e69f65b2b3459292d1e7558075c02805aa0f49a398b85716736ea
|
|
7
|
+
data.tar.gz: 55ccd02b5a9bb13ba46e36e0051c2440d8026956743e02660968209c66e61fe25f4ed8c3dd04a77b08a6209df9e13c922f42e481ade7cf53447d89a93976e680
|
data/README.md
CHANGED
|
@@ -601,7 +601,7 @@ Rails Pulse is built using modern, battle-tested technologies that ensure reliab
|
|
|
601
601
|
- **[Turbo Frames](https://turbo.hotwired.dev/handbook/frames)** - Lazy loading and partial page updates for optimal performance
|
|
602
602
|
|
|
603
603
|
### **Data Visualization**
|
|
604
|
-
- **[
|
|
604
|
+
- **[Apache ECharts](https://echarts.apache.org/)** - Powerful, interactive charting library
|
|
605
605
|
- **[Lucide Icons](https://lucide.dev/)** - Beautiful, consistent iconography with pre-compiled SVG bundle
|
|
606
606
|
|
|
607
607
|
### **Asset Management**
|
|
@@ -2,7 +2,6 @@ module ChartTableConcern
|
|
|
2
2
|
extend ActiveSupport::Concern
|
|
3
3
|
|
|
4
4
|
included do
|
|
5
|
-
include Pagy::Backend
|
|
6
5
|
include TimeRangeConcern
|
|
7
6
|
include ResponseRangeConcern
|
|
8
7
|
include ZoomRangeConcern
|
|
@@ -41,7 +40,7 @@ module ChartTableConcern
|
|
|
41
40
|
disabled_tags: session_disabled_tags,
|
|
42
41
|
show_non_tagged: session[:show_non_tagged] != false,
|
|
43
42
|
**chart_options
|
|
44
|
-
).
|
|
43
|
+
).to_chart_data
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
def setup_table_data(ransack_params)
|
|
@@ -52,7 +51,7 @@ module ChartTableConcern
|
|
|
52
51
|
table_results = build_table_results
|
|
53
52
|
handle_pagination
|
|
54
53
|
|
|
55
|
-
@pagy, @table_data = pagy(table_results,
|
|
54
|
+
@pagy, @table_data = pagy(table_results, items: session_pagination_limit)
|
|
56
55
|
end
|
|
57
56
|
|
|
58
57
|
def setup_zoom_range_data
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
module RailsPulse
|
|
2
2
|
class ApplicationController < ActionController::Base
|
|
3
|
+
# Support both Pagy 8.x (Backend) and Pagy 9+ (Method)
|
|
4
|
+
if defined?(Pagy::Method)
|
|
5
|
+
include Pagy::Method
|
|
6
|
+
else
|
|
7
|
+
include Pagy::Backend
|
|
8
|
+
end
|
|
9
|
+
|
|
3
10
|
before_action :authenticate_rails_pulse_user!
|
|
4
11
|
before_action :set_show_non_tagged_default
|
|
5
12
|
helper_method :session_global_filters, :session_disabled_tags
|
|
@@ -8,10 +15,10 @@ module RailsPulse
|
|
|
8
15
|
limit = limit || params[:limit]
|
|
9
16
|
session[:pagination_limit] = limit.to_i if limit.present?
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
# Render JSON for direct API calls or AJAX requests (but not turbo frame requests)
|
|
19
|
+
if (request.xhr? && !turbo_frame_request?) || (request.patch? && action_name == "set_pagination_limit")
|
|
13
20
|
render json: { status: "ok" }
|
|
14
|
-
|
|
21
|
+
end
|
|
15
22
|
end
|
|
16
23
|
|
|
17
24
|
def set_global_filters
|
|
@@ -163,7 +163,7 @@ module RailsPulse
|
|
|
163
163
|
table_results = build_table_results
|
|
164
164
|
handle_pagination
|
|
165
165
|
|
|
166
|
-
@pagy, @table_data = pagy(table_results,
|
|
166
|
+
@pagy, @table_data = pagy(table_results, items: session_pagination_limit)
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
def handle_pagination
|
|
@@ -128,7 +128,8 @@ module RailsPulse
|
|
|
128
128
|
table_results = build_table_results
|
|
129
129
|
handle_pagination
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
# Use 'items:' for Pagy 8.x compatibility ('limit:' is for Pagy 43+)
|
|
132
|
+
@pagy, @table_data = pagy(table_results, items: session_pagination_limit)
|
|
132
133
|
end
|
|
133
134
|
|
|
134
135
|
def handle_pagination
|
|
@@ -142,7 +142,7 @@ module RailsPulse
|
|
|
142
142
|
table_results = build_table_results
|
|
143
143
|
handle_pagination
|
|
144
144
|
|
|
145
|
-
@pagy, @table_data = pagy(table_results,
|
|
145
|
+
@pagy, @table_data = pagy(table_results, items: session_pagination_limit)
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
def handle_pagination
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
module RailsPulse
|
|
2
2
|
module ApplicationHelper
|
|
3
|
-
include Pagy::Frontend
|
|
4
|
-
|
|
5
3
|
include BreadcrumbsHelper
|
|
6
4
|
include ChartHelper
|
|
7
5
|
include FormattingHelper
|
|
@@ -10,6 +8,10 @@ module RailsPulse
|
|
|
10
8
|
include FormHelper
|
|
11
9
|
include TagsHelper
|
|
12
10
|
|
|
11
|
+
# Include Pagy frontend helpers for Pagy 8.x compatibility
|
|
12
|
+
# Pagy 43+ doesn't need this, but it doesn't hurt to include it
|
|
13
|
+
include Pagy::Frontend if defined?(Pagy::Frontend)
|
|
14
|
+
|
|
13
15
|
# Replacement for lucide_icon helper that works with pre-compiled assets
|
|
14
16
|
# Outputs a custom element that will be hydrated by Stimulus
|
|
15
17
|
def rails_pulse_icon(name, options = {})
|
|
@@ -36,6 +38,49 @@ module RailsPulse
|
|
|
36
38
|
# Backward compatibility alias - can be removed after migration
|
|
37
39
|
alias_method :lucide_icon, :rails_pulse_icon
|
|
38
40
|
|
|
41
|
+
# Get items per page from Pagy instance (compatible with Pagy 8.x and 43+)
|
|
42
|
+
def pagy_items(pagy)
|
|
43
|
+
# Pagy 43+ uses options[:items] or has a limit method
|
|
44
|
+
if pagy.respond_to?(:options) && pagy.options.is_a?(Hash)
|
|
45
|
+
pagy.options[:items]
|
|
46
|
+
# Pagy 8.x uses vars[:items]
|
|
47
|
+
elsif pagy.respond_to?(:vars)
|
|
48
|
+
pagy.vars[:items]
|
|
49
|
+
# Fallback
|
|
50
|
+
else
|
|
51
|
+
pagy.limit || 10
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get page URL from Pagy instance (compatible with Pagy 8.x and 43+)
|
|
56
|
+
def pagy_page_url(pagy, page_number)
|
|
57
|
+
# Pagy 43+ has page_url method
|
|
58
|
+
if pagy.respond_to?(:page_url)
|
|
59
|
+
pagy.page_url(page_number)
|
|
60
|
+
# Pagy 8.x requires using pagy_url_for helper
|
|
61
|
+
else
|
|
62
|
+
pagy_url_for(pagy, page_number)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get previous page number (compatible with Pagy 8.x and 43+)
|
|
67
|
+
def pagy_previous(pagy)
|
|
68
|
+
# Pagy 43+ uses 'previous'
|
|
69
|
+
if pagy.respond_to?(:previous)
|
|
70
|
+
pagy.previous
|
|
71
|
+
# Pagy 8.x uses 'prev'
|
|
72
|
+
elsif pagy.respond_to?(:prev)
|
|
73
|
+
pagy.prev
|
|
74
|
+
else
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get next page number (compatible with Pagy 8.x and 43+)
|
|
80
|
+
def pagy_next(pagy)
|
|
81
|
+
pagy.respond_to?(:next) ? pagy.next : nil
|
|
82
|
+
end
|
|
83
|
+
|
|
39
84
|
# Make Rails Pulse routes available as rails_pulse in views
|
|
40
85
|
def rails_pulse
|
|
41
86
|
@rails_pulse_helper ||= RailsPulseHelper.new(self)
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
module RailsPulse
|
|
2
2
|
module ChartHelper
|
|
3
|
+
# Main chart rendering method - unified API for all chart types
|
|
4
|
+
# Uses Stimulus controller to handle chart initialization
|
|
5
|
+
def render_stimulus_chart(data, type:, **options)
|
|
6
|
+
chart_id = options[:id] || "rails-pulse-chart-#{SecureRandom.hex(8)}"
|
|
7
|
+
height = options[:height] || "400px"
|
|
8
|
+
width = options[:width] || "100%"
|
|
9
|
+
theme = options[:theme] || "railspulse"
|
|
10
|
+
chart_options = options[:options] || {}
|
|
11
|
+
|
|
12
|
+
# Build data attributes for Stimulus
|
|
13
|
+
stimulus_data = {
|
|
14
|
+
controller: "rails-pulse--chart",
|
|
15
|
+
rails_pulse__chart_type_value: type,
|
|
16
|
+
rails_pulse__chart_data_value: data.to_json,
|
|
17
|
+
rails_pulse__chart_options_value: chart_options.to_json,
|
|
18
|
+
rails_pulse__chart_theme_value: theme
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
content_tag(:div, "",
|
|
22
|
+
id: chart_id,
|
|
23
|
+
style: "height: #{height}; width: #{width};",
|
|
24
|
+
data: stimulus_data
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
3
28
|
# Base chart options shared across all chart types
|
|
4
29
|
def base_chart_options(units: nil, zoom: false)
|
|
5
30
|
{
|
|
@@ -97,16 +122,21 @@ module RailsPulse
|
|
|
97
122
|
|
|
98
123
|
private
|
|
99
124
|
|
|
125
|
+
# Wraps JavaScript function strings for later processing
|
|
126
|
+
def js_function(func_string)
|
|
127
|
+
"__FUNCTION_START__#{func_string}__FUNCTION_END__"
|
|
128
|
+
end
|
|
129
|
+
|
|
100
130
|
def apply_tooltip_formatter(options, tooltip_formatter)
|
|
101
131
|
return unless tooltip_formatter.present?
|
|
102
132
|
|
|
103
|
-
options[:tooltip][:formatter] =
|
|
133
|
+
options[:tooltip][:formatter] = js_function(tooltip_formatter)
|
|
104
134
|
end
|
|
105
135
|
|
|
106
136
|
def apply_xaxis_formatter(options, xaxis_formatter)
|
|
107
137
|
return unless xaxis_formatter.present?
|
|
108
138
|
|
|
109
|
-
options[:xAxis][:axisLabel] ||= { formatter:
|
|
139
|
+
options[:xAxis][:axisLabel] ||= { formatter: js_function(xaxis_formatter) }
|
|
110
140
|
end
|
|
111
141
|
|
|
112
142
|
def apply_zoom_configuration(options, zoom, zoom_start, zoom_end, chart_data)
|
|
@@ -12,6 +12,7 @@ import PopoverController from "./controllers/popover_controller";
|
|
|
12
12
|
import FormController from "./controllers/form_controller";
|
|
13
13
|
|
|
14
14
|
// Rails Pulse Controllers
|
|
15
|
+
import ChartController from "./controllers/chart_controller";
|
|
15
16
|
import IndexController from "./controllers/index_controller";
|
|
16
17
|
import ColorSchemeController from "./controllers/color_scheme_controller";
|
|
17
18
|
import PaginationController from "./controllers/pagination_controller";
|
|
@@ -29,7 +30,7 @@ const application = Application.start();
|
|
|
29
30
|
application.debug = false;
|
|
30
31
|
window.Stimulus = application;
|
|
31
32
|
|
|
32
|
-
// Make ECharts available globally for
|
|
33
|
+
// Make ECharts available globally for chart rendering
|
|
33
34
|
window.echarts = echarts;
|
|
34
35
|
|
|
35
36
|
// Make Turbo available globally
|
|
@@ -42,6 +43,7 @@ application.register("rails-pulse--menu", MenuController);
|
|
|
42
43
|
application.register("rails-pulse--popover", PopoverController);
|
|
43
44
|
application.register("rails-pulse--form", FormController);
|
|
44
45
|
|
|
46
|
+
application.register("rails-pulse--chart", ChartController);
|
|
45
47
|
application.register("rails-pulse--index", IndexController);
|
|
46
48
|
application.register("rails-pulse--color-scheme", ColorSchemeController);
|
|
47
49
|
application.register("rails-pulse--pagination", PaginationController);
|
|
@@ -96,59 +98,6 @@ echarts.registerTheme('railspulse', {
|
|
|
96
98
|
"bar": { "itemStyle": { "barBorderWidth": 0 } }
|
|
97
99
|
});
|
|
98
100
|
|
|
99
|
-
// Chart resize functionality (moved from inline script for CSP compliance)
|
|
100
|
-
window.addEventListener('resize', function() {
|
|
101
|
-
if (window.RailsCharts && window.RailsCharts.charts) {
|
|
102
|
-
Object.keys(window.RailsCharts.charts).forEach(function(chartID) {
|
|
103
|
-
window.RailsCharts.charts[chartID].resize();
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Apply axis label colors based on current color scheme
|
|
109
|
-
function applyChartAxisLabelColors() {
|
|
110
|
-
if (!window.RailsCharts || !window.RailsCharts.charts) return;
|
|
111
|
-
const scheme = document.documentElement.getAttribute('data-color-scheme');
|
|
112
|
-
const isDark = scheme === 'dark';
|
|
113
|
-
const axisColor = isDark ? '#ffffff' : '#999999';
|
|
114
|
-
Object.keys(window.RailsCharts.charts).forEach(function(chartID) {
|
|
115
|
-
const chart = window.RailsCharts.charts[chartID];
|
|
116
|
-
try {
|
|
117
|
-
chart.setOption({
|
|
118
|
-
xAxis: { axisLabel: { color: axisColor } },
|
|
119
|
-
yAxis: { axisLabel: { color: axisColor } }
|
|
120
|
-
});
|
|
121
|
-
} catch (e) {
|
|
122
|
-
// noop
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Initial apply after charts initialize and on scheme changes
|
|
128
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
129
|
-
// run shortly after load to allow charts to initialize
|
|
130
|
-
setTimeout(applyChartAxisLabelColors, 50);
|
|
131
|
-
});
|
|
132
|
-
document.addEventListener('rails-pulse:color-scheme-changed', applyChartAxisLabelColors);
|
|
133
|
-
|
|
134
|
-
// Global function to initialize Rails Charts in any container.
|
|
135
|
-
// This is needed as we render Rails Charts in Turbo Frames.
|
|
136
|
-
window.initializeChartsInContainer = function(containerId) {
|
|
137
|
-
requestAnimationFrame(() => {
|
|
138
|
-
const container = containerId ? document.getElementById(containerId) : document;
|
|
139
|
-
const scripts = container.querySelectorAll('script');
|
|
140
|
-
scripts.forEach(script => {
|
|
141
|
-
const content = script.textContent;
|
|
142
|
-
const match = content.match(/function\s+(init_rails_charts_[a-f0-9]+)/);
|
|
143
|
-
if (match && window[match[1]]) {
|
|
144
|
-
window[match[1]]();
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
// ensure colors are correct for any charts initialized in this container
|
|
148
|
-
setTimeout(applyChartAxisLabelColors, 10);
|
|
149
|
-
});
|
|
150
|
-
};
|
|
151
|
-
|
|
152
101
|
// Export for global access
|
|
153
102
|
window.RailsPulse = {
|
|
154
103
|
application,
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = {
|
|
5
|
+
type: String, // "bar", "line", "area", "sparkline"
|
|
6
|
+
data: Object, // Chart data
|
|
7
|
+
options: Object, // ECharts configuration
|
|
8
|
+
theme: String // ECharts theme
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
this.initializeChart()
|
|
13
|
+
this.handleColorSchemeChange = this.onColorSchemeChange.bind(this)
|
|
14
|
+
document.addEventListener('rails-pulse:color-scheme-changed', this.handleColorSchemeChange)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnect() {
|
|
18
|
+
document.removeEventListener('rails-pulse:color-scheme-changed', this.handleColorSchemeChange)
|
|
19
|
+
this.disposeChart()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Main initialization with retry logic
|
|
23
|
+
initializeChart() {
|
|
24
|
+
this.retryCount = 0
|
|
25
|
+
this.maxRetries = 100 // 5 seconds
|
|
26
|
+
this.attemptInit()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
attemptInit() {
|
|
30
|
+
if (typeof echarts === 'undefined') {
|
|
31
|
+
this.retryCount++
|
|
32
|
+
if (this.retryCount >= this.maxRetries) {
|
|
33
|
+
console.error('[RailsPulse] echarts not loaded after 5 seconds for', this.element.id)
|
|
34
|
+
this.showError()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
setTimeout(() => this.attemptInit(), 50)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.renderChart()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
renderChart() {
|
|
45
|
+
try {
|
|
46
|
+
// Initialize chart
|
|
47
|
+
this.chart = echarts.init(this.element, this.themeValue || 'railspulse')
|
|
48
|
+
|
|
49
|
+
// Build and set options
|
|
50
|
+
const config = this.buildChartConfig()
|
|
51
|
+
this.chart.setOption(config)
|
|
52
|
+
|
|
53
|
+
// Apply current color scheme
|
|
54
|
+
this.applyColorScheme()
|
|
55
|
+
|
|
56
|
+
// Dispatch event for other controllers (event-based communication)
|
|
57
|
+
document.dispatchEvent(new CustomEvent('stimulus:echarts:rendered', {
|
|
58
|
+
detail: {
|
|
59
|
+
containerId: this.element.id,
|
|
60
|
+
chart: this.chart,
|
|
61
|
+
controller: this
|
|
62
|
+
}
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
// Responsive resize
|
|
66
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
67
|
+
if (this.chart) {
|
|
68
|
+
this.chart.resize()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
this.resizeObserver.observe(this.element)
|
|
72
|
+
|
|
73
|
+
// Mark as rendered for tests
|
|
74
|
+
this.element.setAttribute('data-chart-rendered', 'true')
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('[RailsPulse] Error initializing chart:', error)
|
|
78
|
+
this.showError()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
buildChartConfig() {
|
|
83
|
+
// Start with provided options
|
|
84
|
+
const config = { ...this.optionsValue }
|
|
85
|
+
|
|
86
|
+
// Process formatters (convert function strings to actual functions)
|
|
87
|
+
this.processFormatters(config)
|
|
88
|
+
|
|
89
|
+
// Set data (xAxis and series)
|
|
90
|
+
this.setChartData(config)
|
|
91
|
+
|
|
92
|
+
return config
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setChartData(config) {
|
|
96
|
+
const data = this.dataValue
|
|
97
|
+
|
|
98
|
+
// Extract labels and values
|
|
99
|
+
const labels = Object.keys(data).map(k => {
|
|
100
|
+
const num = Number(k)
|
|
101
|
+
return isNaN(num) ? k : num
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const values = Object.values(data).map(v => {
|
|
105
|
+
if (typeof v === 'object' && v !== null) {
|
|
106
|
+
return v.value !== undefined ? v.value : v
|
|
107
|
+
}
|
|
108
|
+
return v
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Set xAxis data
|
|
112
|
+
config.xAxis = config.xAxis || {}
|
|
113
|
+
config.xAxis.type = 'category'
|
|
114
|
+
config.xAxis.data = labels
|
|
115
|
+
|
|
116
|
+
// Set yAxis
|
|
117
|
+
config.yAxis = config.yAxis || {}
|
|
118
|
+
config.yAxis.type = 'value'
|
|
119
|
+
|
|
120
|
+
// Set series data
|
|
121
|
+
if (Array.isArray(config.series)) {
|
|
122
|
+
// If series is already an array, update first series
|
|
123
|
+
config.series[0] = config.series[0] || {}
|
|
124
|
+
config.series[0].type = this.typeValue
|
|
125
|
+
config.series[0].data = values
|
|
126
|
+
} else if (config.series && typeof config.series === 'object') {
|
|
127
|
+
// If series is a single object (from helper), convert to array
|
|
128
|
+
const seriesConfig = { ...config.series }
|
|
129
|
+
config.series = [{
|
|
130
|
+
type: this.typeValue,
|
|
131
|
+
data: values,
|
|
132
|
+
...seriesConfig
|
|
133
|
+
}]
|
|
134
|
+
} else {
|
|
135
|
+
// No series provided, create default
|
|
136
|
+
config.series = [{
|
|
137
|
+
type: this.typeValue,
|
|
138
|
+
data: values
|
|
139
|
+
}]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
processFormatters(config) {
|
|
144
|
+
// Process tooltip formatter
|
|
145
|
+
if (config.tooltip?.formatter && typeof config.tooltip.formatter === 'string') {
|
|
146
|
+
config.tooltip.formatter = this.parseFormatter(config.tooltip.formatter)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Process xAxis formatter
|
|
150
|
+
if (config.xAxis?.axisLabel?.formatter && typeof config.xAxis.axisLabel.formatter === 'string') {
|
|
151
|
+
config.xAxis.axisLabel.formatter = this.parseFormatter(config.xAxis.axisLabel.formatter)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Process yAxis formatter
|
|
155
|
+
if (config.yAxis?.axisLabel?.formatter && typeof config.yAxis.axisLabel.formatter === 'string') {
|
|
156
|
+
config.yAxis.axisLabel.formatter = this.parseFormatter(config.yAxis.axisLabel.formatter)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
parseFormatter(formatterString) {
|
|
161
|
+
// Remove function markers if present
|
|
162
|
+
const cleanString = formatterString.replace(/__FUNCTION_START__|__FUNCTION_END__/g, '')
|
|
163
|
+
|
|
164
|
+
// If it's a function string, parse it
|
|
165
|
+
if (cleanString.trim().startsWith('function')) {
|
|
166
|
+
try {
|
|
167
|
+
// eslint-disable-next-line no-eval
|
|
168
|
+
return eval(`(${cleanString})`)
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('[RailsPulse] Error parsing formatter function:', error)
|
|
171
|
+
return cleanString
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return cleanString
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
showError() {
|
|
178
|
+
this.element.classList.add('chart-error')
|
|
179
|
+
this.element.innerHTML = '<p class="text-subtle p-4">Chart failed to load</p>'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Public accessor for chart instance
|
|
183
|
+
get chartInstance() {
|
|
184
|
+
return this.chart
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
disposeChart() {
|
|
188
|
+
if (this.resizeObserver) {
|
|
189
|
+
this.resizeObserver.disconnect()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.chart) {
|
|
193
|
+
this.chart.dispose()
|
|
194
|
+
this.chart = null
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Action for dynamic updates
|
|
199
|
+
update(event) {
|
|
200
|
+
if (event.detail?.data) {
|
|
201
|
+
this.dataValue = event.detail.data
|
|
202
|
+
}
|
|
203
|
+
if (event.detail?.options) {
|
|
204
|
+
this.optionsValue = event.detail.options
|
|
205
|
+
}
|
|
206
|
+
if (this.chart) {
|
|
207
|
+
const config = this.buildChartConfig()
|
|
208
|
+
this.chart.setOption(config, true) // true = not merge
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Color scheme management
|
|
213
|
+
onColorSchemeChange() {
|
|
214
|
+
this.applyColorScheme()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
applyColorScheme() {
|
|
218
|
+
if (!this.chart) return
|
|
219
|
+
|
|
220
|
+
const scheme = document.documentElement.getAttribute('data-color-scheme')
|
|
221
|
+
const isDark = scheme === 'dark'
|
|
222
|
+
const axisColor = isDark ? '#ffffff' : '#999999'
|
|
223
|
+
|
|
224
|
+
this.chart.setOption({
|
|
225
|
+
xAxis: { axisLabel: { color: axisColor } },
|
|
226
|
+
yAxis: { axisLabel: { color: axisColor } }
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -15,21 +15,16 @@ export default class extends Controller {
|
|
|
15
15
|
originalSeriesOption = null;
|
|
16
16
|
|
|
17
17
|
connect() {
|
|
18
|
-
// Listen for the custom event '
|
|
19
|
-
// This event is
|
|
18
|
+
// Listen for the custom event 'stimulus:echarts:rendered' to set up the chart.
|
|
19
|
+
// This event is dispatched by the chart controller when the chart is ready.
|
|
20
20
|
this.handleChartInitialized = this.onChartInitialized.bind(this);
|
|
21
21
|
|
|
22
|
-
document.addEventListener('
|
|
23
|
-
|
|
24
|
-
// If the chart is already initialized (e.g., on back navigation), set up immediately
|
|
25
|
-
if (window.RailsCharts?.charts?.[this.chartIdValue]) {
|
|
26
|
-
this.setup();
|
|
27
|
-
}
|
|
22
|
+
document.addEventListener('stimulus:echarts:rendered', this.handleChartInitialized);
|
|
28
23
|
}
|
|
29
24
|
|
|
30
25
|
disconnect() {
|
|
31
|
-
// Remove the event listener
|
|
32
|
-
document.removeEventListener('
|
|
26
|
+
// Remove the event listener when the controller is disconnected
|
|
27
|
+
document.removeEventListener('stimulus:echarts:rendered', this.handleChartInitialized);
|
|
33
28
|
|
|
34
29
|
// Remove chart event listeners if they exist
|
|
35
30
|
if (this.hasChartTarget && this.chartTarget) {
|
|
@@ -47,6 +42,8 @@ export default class extends Controller {
|
|
|
47
42
|
// After the chart is initialized, set up the event listeners and data tracking
|
|
48
43
|
onChartInitialized(event) {
|
|
49
44
|
if (event.detail.containerId === this.chartIdValue) {
|
|
45
|
+
// Store the chart instance from the event
|
|
46
|
+
this.chart = event.detail.chart;
|
|
50
47
|
this.setup();
|
|
51
48
|
}
|
|
52
49
|
}
|
|
@@ -56,7 +53,7 @@ export default class extends Controller {
|
|
|
56
53
|
return; // Prevent multiple setups
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
// We need both the chart target in DOM and the chart object from
|
|
56
|
+
// We need both the chart target in DOM and the chart object from the event
|
|
60
57
|
let hasTarget = false;
|
|
61
58
|
try {
|
|
62
59
|
hasTarget = !!this.chartTarget;
|
|
@@ -64,10 +61,8 @@ export default class extends Controller {
|
|
|
64
61
|
hasTarget = false;
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
// Get the chart element which the RailsCharts library has created
|
|
68
|
-
this.chart = window.RailsCharts.charts[this.chartIdValue];
|
|
69
|
-
|
|
70
64
|
// Only proceed if we have BOTH the DOM target and the chart object
|
|
65
|
+
// (chart is set by onChartInitialized from the event)
|
|
71
66
|
if (!hasTarget || !this.chart) {
|
|
72
67
|
return;
|
|
73
68
|
}
|