Adds a button to explain charts in simple terms with a copy-to-email feature
Size
12.5 KB
Version
1.1.3
Created
Oct 16, 2025
Updated
7 days ago
1// ==UserScript==
2// @name YCharts Simple Chart Explainer
3// @description Adds a button to explain charts in simple terms with a copy-to-email feature
4// @version 1.1.3
5// @match https://*.ycharts.com/*
6// @icon https://static.ycharts.com/images/icons/favicon.637225eac278.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('YCharts Simple Chart Explainer loaded');
12
13 // Debounce function to avoid multiple rapid calls
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // Function to extract chart data
27 function extractChartData() {
28 const chartTitle = document.querySelector('.page-name-text')?.textContent?.trim() || 'Chart';
29
30 const securities = Array.from(document.querySelectorAll('#securities-list .options-list-item')).map(item => {
31 const name = item.querySelector('.custom-control-description')?.textContent?.trim();
32 return name;
33 }).filter(Boolean);
34
35 const legendItems = Array.from(document.querySelectorAll('.chart-legend-item')).slice(1).map(item => {
36 const name = item.querySelector('.chart-legend-item-title')?.textContent?.trim();
37 const date = item.querySelector('.chart-legend-item-date')?.textContent?.trim();
38 const values = Array.from(item.querySelectorAll('[data-test-id^="chart-legend-item-value"]')).map(v => v.textContent.trim());
39 return { name, date, values };
40 }).filter(item => item.name);
41
42 // Extract the selected timeframe
43 const timeframeElement = document.querySelector('ycn-fund-chart-date-zoom .chart-control-item.current .chart-control-link');
44 const timeframe = timeframeElement?.textContent?.trim() || 'unknown';
45
46 return { chartTitle, securities, legendItems, timeframe };
47 }
48
49 // Function to create and show the explanation popup
50 function showExplanationPopup(explanation) {
51 // Remove existing popup if any
52 const existingPopup = document.getElementById('chart-explainer-popup');
53 if (existingPopup) {
54 existingPopup.remove();
55 }
56
57 // Create popup overlay
58 const overlay = document.createElement('div');
59 overlay.id = 'chart-explainer-popup';
60 overlay.style.cssText = `
61 position: fixed;
62 top: 0;
63 left: 0;
64 width: 100%;
65 height: 100%;
66 background: rgba(0, 0, 0, 0.5);
67 display: flex;
68 align-items: center;
69 justify-content: center;
70 z-index: 10000;
71 `;
72
73 // Create popup content
74 const popup = document.createElement('div');
75 popup.style.cssText = `
76 background: white;
77 border-radius: 8px;
78 padding: 24px;
79 max-width: 600px;
80 max-height: 80vh;
81 overflow-y: auto;
82 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
83 position: relative;
84 `;
85
86 // Create header
87 const header = document.createElement('div');
88 header.style.cssText = `
89 display: flex;
90 justify-content: space-between;
91 align-items: center;
92 margin-bottom: 16px;
93 padding-bottom: 12px;
94 border-bottom: 2px solid #e0e0e0;
95 `;
96
97 const title = document.createElement('h3');
98 title.textContent = 'Chart Explanation';
99 title.style.cssText = `
100 margin: 0;
101 font-size: 20px;
102 font-weight: 600;
103 color: #333;
104 `;
105
106 const closeButton = document.createElement('button');
107 closeButton.textContent = '×';
108 closeButton.style.cssText = `
109 background: none;
110 border: none;
111 font-size: 32px;
112 cursor: pointer;
113 color: #666;
114 padding: 0;
115 width: 32px;
116 height: 32px;
117 line-height: 32px;
118 text-align: center;
119 `;
120 closeButton.onclick = () => overlay.remove();
121
122 header.appendChild(title);
123 header.appendChild(closeButton);
124
125 // Create explanation text area
126 const explanationText = document.createElement('div');
127 explanationText.textContent = explanation;
128 explanationText.style.cssText = `
129 font-size: 16px;
130 line-height: 1.6;
131 color: #333;
132 margin-bottom: 20px;
133 white-space: pre-wrap;
134 `;
135
136 // Create copy button
137 const copyButton = document.createElement('button');
138 copyButton.textContent = 'Copy to Clipboard';
139 copyButton.style.cssText = `
140 background: #007bff;
141 color: white;
142 border: none;
143 padding: 10px 20px;
144 border-radius: 5px;
145 cursor: pointer;
146 font-size: 14px;
147 font-weight: 500;
148 transition: background 0.2s;
149 `;
150 copyButton.onmouseover = () => copyButton.style.background = '#0056b3';
151 copyButton.onmouseout = () => copyButton.style.background = '#007bff';
152 copyButton.onclick = async () => {
153 try {
154 await navigator.clipboard.writeText(explanation);
155 copyButton.textContent = '✓ Copied!';
156 copyButton.style.background = '#28a745';
157 setTimeout(() => {
158 copyButton.textContent = 'Copy to Clipboard';
159 copyButton.style.background = '#007bff';
160 }, 2000);
161 } catch (err) {
162 console.error('Failed to copy:', err);
163 copyButton.textContent = 'Failed to copy';
164 copyButton.style.background = '#dc3545';
165 setTimeout(() => {
166 copyButton.textContent = 'Copy to Clipboard';
167 copyButton.style.background = '#007bff';
168 }, 2000);
169 }
170 };
171
172 // Assemble popup
173 popup.appendChild(header);
174 popup.appendChild(explanationText);
175 popup.appendChild(copyButton);
176 overlay.appendChild(popup);
177
178 // Close on overlay click
179 overlay.onclick = (e) => {
180 if (e.target === overlay) {
181 overlay.remove();
182 }
183 };
184
185 document.body.appendChild(overlay);
186 }
187
188 // Function to generate explanation using AI
189 async function generateExplanation() {
190 const button = document.getElementById('explain-chart-btn');
191 if (!button) return;
192
193 // Show loading state
194 const originalText = button.textContent;
195 button.textContent = 'Generating...';
196 button.disabled = true;
197 button.style.opacity = '0.6';
198 button.style.cursor = 'wait';
199
200 try {
201 const chartData = extractChartData();
202 console.log('Chart data extracted:', chartData);
203
204 // Check if RM.aiCall is available
205 if (typeof RM === 'undefined' || typeof RM.aiCall !== 'function') {
206 throw new Error('AI service is not available. Please refresh the page and try again.');
207 }
208
209 // Create prompt for AI
210 const prompt = `You are explaining a financial chart to a 70-year-old person who may not be familiar with investment terminology.
211
212Chart Title: ${chartData.chartTitle}
213Timeframe: ${chartData.timeframe}
214
215Securities being compared:
216${chartData.securities.join('\n')}
217
218Current performance data (as of ${chartData.legendItems[0]?.date || 'latest'}):
219${chartData.legendItems.map(item => `- ${item.name}: ${item.values.join(' (Annualized: ')})`).join('\n')}
220
221Please provide a clear, simple explanation that:
2221. Explains what this chart is showing in plain English, specifically mentioning the timeframe (${chartData.timeframe})
2232. Provides context about what the timeframe means (e.g., "over the past 3 years" or "year-to-date" or "over the past month")
2243. Highlights which investments are performing best and worst over this specific time period
2254. Explains what the percentages mean in practical terms for this timeframe
2265. Avoids jargon and uses everyday language
2276. Keeps it concise (3-4 paragraphs maximum)
2287. Is suitable for pasting into an email
229
230Write in a warm, conversational tone as if you're explaining this to a friend or family member. Make sure to naturally incorporate the timeframe context throughout the explanation.`;
231
232 console.log('Calling AI with prompt...');
233 const explanation = await RM.aiCall(prompt);
234 console.log('AI explanation received');
235
236 // Show popup with explanation
237 showExplanationPopup(explanation);
238
239 } catch (error) {
240 console.error('Error generating explanation:', error);
241 const errorMessage = error.message || 'Unknown error occurred';
242 alert('Sorry, there was an error generating the explanation:\n\n' + errorMessage + '\n\nThis appears to be a platform issue. Please try:\n1. Refreshing the page\n2. Restarting your browser\n3. Opening the chart in a new tab');
243 } finally {
244 // Restore button state
245 button.textContent = originalText;
246 button.disabled = false;
247 button.style.opacity = '1';
248 button.style.cursor = 'pointer';
249 }
250 }
251
252 // Function to add the explain button
253 function addExplainButton() {
254 // Check if button already exists
255 if (document.getElementById('explain-chart-btn')) {
256 console.log('Explain button already exists');
257 return;
258 }
259
260 // Find the Chart Options dropdown container
261 const chartOptionsContainer = document.querySelector('.chart-options-cover');
262 if (!chartOptionsContainer) {
263 console.log('Chart Options container not found yet');
264 return;
265 }
266
267 console.log('Adding explain button next to Chart Options');
268
269 // Create a wrapper for our button
270 const buttonWrapper = document.createElement('div');
271 buttonWrapper.className = 'chart-options-cover col-i-3';
272 buttonWrapper.style.cssText = `
273 margin-left: 8px;
274 `;
275
276 // Create the explain button
277 const explainButton = document.createElement('button');
278 explainButton.id = 'explain-chart-btn';
279 explainButton.className = 'btn btn-secondary btn-block';
280 explainButton.textContent = '📊 Explain Chart';
281 explainButton.style.cssText = `
282 background: #28a745;
283 border-color: #28a745;
284 color: white;
285 font-weight: 500;
286 cursor: pointer;
287 transition: background 0.2s;
288 white-space: nowrap;
289 `;
290 explainButton.onmouseover = () => {
291 if (!explainButton.disabled) {
292 explainButton.style.background = '#218838';
293 explainButton.style.borderColor = '#1e7e34';
294 }
295 };
296 explainButton.onmouseout = () => {
297 if (!explainButton.disabled) {
298 explainButton.style.background = '#28a745';
299 explainButton.style.borderColor = '#28a745';
300 }
301 };
302 explainButton.onclick = generateExplanation;
303
304 buttonWrapper.appendChild(explainButton);
305
306 // Insert after Chart Options
307 chartOptionsContainer.parentNode.insertBefore(buttonWrapper, chartOptionsContainer.nextSibling);
308
309 console.log('Explain button added successfully');
310 }
311
312 // Initialize the extension
313 function init() {
314 console.log('Initializing YCharts Simple Chart Explainer');
315
316 // Try to add button immediately
317 addExplainButton();
318
319 // Watch for DOM changes in case the page loads dynamically
320 const observer = new MutationObserver(debounce(() => {
321 addExplainButton();
322 }, 500));
323
324 observer.observe(document.body, {
325 childList: true,
326 subtree: true
327 });
328
329 console.log('Observer set up to watch for Chart Options');
330 }
331
332 // Wait for page to be ready
333 if (document.readyState === 'loading') {
334 document.addEventListener('DOMContentLoaded', init);
335 } else {
336 init();
337 }
338})();