mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-09-19 12:04:54 +02:00
working on cool feature 2
This commit is contained in:
parent
6fdf813a94
commit
462186b1b7
@ -1,311 +1,320 @@
|
|||||||
{
|
{
|
||||||
"Canvas_Refined": {
|
"Canvas_Refined": {
|
||||||
"message": "Canvas Refined"
|
"message": "Canvas Refined"
|
||||||
},
|
},
|
||||||
"search_features": {
|
"search_features": {
|
||||||
"message": "Search features..."
|
"message": "Search features..."
|
||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"message": "Updates"
|
"message": "Updates"
|
||||||
},
|
},
|
||||||
"dark_mode": {
|
"dark_mode": {
|
||||||
"message": "Dark mode"
|
"message": "Dark mode"
|
||||||
},
|
},
|
||||||
"automatic_dark_mode": {
|
"automatic_dark_mode": {
|
||||||
"message": "Scheduled Dark Mode"
|
"message": "Scheduled Dark Mode"
|
||||||
},
|
},
|
||||||
"auto_start_time": {
|
"auto_start_time": {
|
||||||
"message": "Start Time"
|
"message": "Start Time"
|
||||||
},
|
},
|
||||||
"auto_end_time": {
|
"auto_end_time": {
|
||||||
"message": "End Time"
|
"message": "End Time"
|
||||||
},
|
},
|
||||||
"edit_dark_mode": {
|
"edit_dark_mode": {
|
||||||
"message": "Dark Mode"
|
"message": "Dark Mode"
|
||||||
},
|
},
|
||||||
"enter_url": {
|
"enter_url": {
|
||||||
"message": "Your Canvas url:"
|
"message": "Your Canvas url:"
|
||||||
},
|
},
|
||||||
"card_grades": {
|
"card_grades": {
|
||||||
"message": "Card Grades"
|
"message": "Card Grades"
|
||||||
},
|
},
|
||||||
"gradient_cards": {
|
"gradient_cards": {
|
||||||
"message": "Gradient Cards"
|
"message": "Gradient Cards"
|
||||||
},
|
},
|
||||||
"condensed_cards": {
|
"condensed_cards": {
|
||||||
"message": "Condensed Cards"
|
"message": "Condensed Cards"
|
||||||
},
|
},
|
||||||
"disable_color": {
|
"disable_color": {
|
||||||
"message": "Disable Color Overlay"
|
"message": "Disable Color Overlay"
|
||||||
},
|
},
|
||||||
"card_assignments": {
|
"card_assignments": {
|
||||||
"message": "Card Assignments"
|
"message": "Card Assignments"
|
||||||
},
|
},
|
||||||
"card_customization": {
|
"card_customization": {
|
||||||
"message": "Cards"
|
"message": "Cards"
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
"message": "Styles"
|
"message": "Styles"
|
||||||
},
|
},
|
||||||
"useddmm": {
|
"useddmm": {
|
||||||
"message": "Use dd/mm"
|
"message": "Use dd/mm"
|
||||||
},
|
},
|
||||||
"better_todo": {
|
"better_todo": {
|
||||||
"message": "Better todo list"
|
"message": "Better todo list"
|
||||||
},
|
},
|
||||||
"better_sidebar": {
|
"better_sidebar": {
|
||||||
"message": "Better Sidebar"
|
"message": "Better Sidebar"
|
||||||
},
|
},
|
||||||
"quiz_safe_mode": {
|
"quiz_safe_mode": {
|
||||||
"message": "Quiz Safe Mode"
|
"message": "Quiz Safe Mode"
|
||||||
},
|
},
|
||||||
"quiz_safe_mode_desc": {
|
"quiz_safe_mode_desc": {
|
||||||
"message": "Turns off most Canvas Refined features on quiz pages so you get the default Canvas quiz experience"
|
"message": "Turns off most Canvas Refined features on quiz pages so you get the default Canvas quiz experience"
|
||||||
},
|
},
|
||||||
"opacity_options": {
|
"opacity_options": {
|
||||||
"message": "Opacity & Blur"
|
"message": "Opacity & Blur"
|
||||||
},
|
},
|
||||||
"opacity_only_background": {
|
"opacity_only_background": {
|
||||||
"message": "Only available with a custom background"
|
"message": "Only available with a custom background"
|
||||||
},
|
},
|
||||||
"card_transparency": {
|
"card_transparency": {
|
||||||
"message": "Card transparency"
|
"message": "Card transparency"
|
||||||
},
|
},
|
||||||
"background_opacity": {
|
"background_opacity": {
|
||||||
"message": "Background opacity"
|
"message": "Background opacity"
|
||||||
},
|
},
|
||||||
"bg_blur": {
|
"bg_blur": {
|
||||||
"message": "Background blur"
|
"message": "Background blur"
|
||||||
},
|
},
|
||||||
"sidebar_opacity": {
|
"sidebar_opacity": {
|
||||||
"message": "Sidebar opacity"
|
"message": "Sidebar opacity"
|
||||||
},
|
},
|
||||||
"sidebar_blur": {
|
"sidebar_blur": {
|
||||||
"message": "Sidebar blur"
|
"message": "Sidebar blur"
|
||||||
},
|
},
|
||||||
"card_opacity": {
|
"card_opacity": {
|
||||||
"message": "Card opacity"
|
"message": "Card opacity"
|
||||||
},
|
},
|
||||||
"card_blur": {
|
"card_blur": {
|
||||||
"message": "Card blur"
|
"message": "Card blur"
|
||||||
},
|
},
|
||||||
"sidebar_opacity_auto": {
|
"sidebar_opacity_auto": {
|
||||||
"message": "Applies to the Better Sidebar when it's on, otherwise the normal sidebar."
|
"message": "Applies to the Better Sidebar when it's on, otherwise the normal sidebar."
|
||||||
},
|
},
|
||||||
"reset_default": {
|
"reset_default": {
|
||||||
"message": "Reset to default"
|
"message": "Reset to default"
|
||||||
},
|
},
|
||||||
"max_items": {
|
"max_items": {
|
||||||
"message": "Max Items: "
|
"message": "Max Items: "
|
||||||
},
|
},
|
||||||
"hover_preview": {
|
"hover_preview": {
|
||||||
"message": "Previews on hover"
|
"message": "Previews on hover"
|
||||||
},
|
},
|
||||||
"hide_completed": {
|
"hide_completed": {
|
||||||
"message": "Hide Completed"
|
"message": "Hide Completed"
|
||||||
},
|
},
|
||||||
"24hrformat": {
|
"24hrformat": {
|
||||||
"message": "Use 24 hr Format"
|
"message": "Use 24 hr Format"
|
||||||
},
|
},
|
||||||
"dashboard_notes": {
|
"dashboard_notes": {
|
||||||
"message": "Dashboard Notes"
|
"message": "Dashboard Notes"
|
||||||
},
|
},
|
||||||
"gpa_calculator": {
|
"gpa_calculator": {
|
||||||
"message": "GPA Calculator"
|
"message": "GPA Calculator"
|
||||||
},
|
},
|
||||||
"gpa_settings": {
|
"gpa_settings": {
|
||||||
"message": "GPA Settings"
|
"message": "GPA Settings"
|
||||||
},
|
},
|
||||||
"grade_hover": {
|
"grade_hover": {
|
||||||
"message": "Show Only On Hover"
|
"message": "Show Only On Hover"
|
||||||
},
|
},
|
||||||
"due": {
|
"due": {
|
||||||
"message": "Due"
|
"message": "Due"
|
||||||
},
|
},
|
||||||
"card_colors": {
|
"card_colors": {
|
||||||
"message": "Card Colors"
|
"message": "Card Colors"
|
||||||
},
|
},
|
||||||
"back": {
|
"back": {
|
||||||
"message": "Back"
|
"message": "Back"
|
||||||
},
|
},
|
||||||
"more_options": {
|
"more_options": {
|
||||||
"message": "More Options"
|
"message": "More Options"
|
||||||
},
|
},
|
||||||
"manual_customization": {
|
"manual_customization": {
|
||||||
"message": "Manual Customization"
|
"message": "Manual Customization"
|
||||||
},
|
},
|
||||||
"background_colors": {
|
"background_colors": {
|
||||||
"message": "Background"
|
"message": "Background"
|
||||||
},
|
},
|
||||||
"text_colors": {
|
"text_colors": {
|
||||||
"message": "Text"
|
"message": "Text"
|
||||||
},
|
},
|
||||||
"full_width": {
|
"full_width": {
|
||||||
"message": "Full Width Fix"
|
"message": "Full Width Fix"
|
||||||
},
|
},
|
||||||
"report_issue": {
|
"report_issue": {
|
||||||
"message": "Report Issue"
|
"message": "Report Issue"
|
||||||
},
|
},
|
||||||
"themes": {
|
"themes": {
|
||||||
"message": "Themes"
|
"message": "Themes"
|
||||||
},
|
},
|
||||||
"move_top": {
|
"move_top": {
|
||||||
"message": "Move To Top"
|
"message": "Move To Top"
|
||||||
},
|
},
|
||||||
"show_past_due": {
|
"show_past_due": {
|
||||||
"message": "Show Past Due"
|
"message": "Show Past Due"
|
||||||
},
|
},
|
||||||
"todo_separate_scrollbar": {
|
"todo_separate_scrollbar": {
|
||||||
"message": "Separate Scrollbar"
|
"message": "Separate Scrollbar"
|
||||||
},
|
},
|
||||||
"todo_full_height": {
|
"todo_full_height": {
|
||||||
"message": "Force Full Height"
|
"message": "Force Full Height"
|
||||||
},
|
},
|
||||||
"todo_alternate_colors": {
|
"todo_alternate_colors": {
|
||||||
"message": "Alternate colors"
|
"message": "Alternate colors"
|
||||||
},
|
},
|
||||||
"relative_due_dates": {
|
"relative_due_dates": {
|
||||||
"message": "Relative Due Dates"
|
"message": "Relative Due Dates"
|
||||||
},
|
},
|
||||||
"equal_height_cards": {
|
"equal_height_cards": {
|
||||||
"message": "Equal Height"
|
"message": "Equal Height"
|
||||||
},
|
},
|
||||||
"enter_multiple": {
|
"enter_multiple": {
|
||||||
"message": "(Enter Multiple With A Comma)"
|
"message": "(Enter Multiple With A Comma)"
|
||||||
},
|
},
|
||||||
"todo_remind": {
|
"todo_remind": {
|
||||||
"message": "Todo Reminders"
|
"message": "Todo Reminders"
|
||||||
},
|
},
|
||||||
"remlogo": {
|
"remlogo": {
|
||||||
"message": "Remove Sidebar Logo"
|
"message": "Remove Sidebar Logo"
|
||||||
},
|
},
|
||||||
"hide_feedback": {
|
"hide_feedback": {
|
||||||
"message": "Hide Recent Feedback"
|
"message": "Hide Recent Feedback"
|
||||||
},
|
},
|
||||||
"tab_icons": {
|
"tab_icons": {
|
||||||
"message": "Color Coded Tab Icons"
|
"message": "Color Coded Tab Icons"
|
||||||
},
|
},
|
||||||
"todo_hide_feedback": {
|
"todo_hide_feedback": {
|
||||||
"message": "Hide Recent Feedback"
|
"message": "Hide Recent Feedback"
|
||||||
},
|
},
|
||||||
"gpa_calc_cumulative": {
|
"gpa_calc_cumulative": {
|
||||||
"message": "Show Cumulative GPA"
|
"message": "Show Cumulative GPA"
|
||||||
},
|
},
|
||||||
"gpa_calc_weighted": {
|
"gpa_calc_weighted": {
|
||||||
"message": "Show Weighted GPA"
|
"message": "Show Weighted GPA"
|
||||||
},
|
},
|
||||||
"device_dark": {
|
"device_dark": {
|
||||||
"message": "Use Device Dark Mode Settings"
|
"message": "Use Device Dark Mode Settings"
|
||||||
},
|
},
|
||||||
"presets": {
|
"presets": {
|
||||||
"message": "Presets"
|
"message": "Presets"
|
||||||
},
|
},
|
||||||
"dark_lighter": {
|
"dark_lighter": {
|
||||||
"message": "Lighter"
|
"message": "Lighter"
|
||||||
},
|
},
|
||||||
"dark_light": {
|
"dark_light": {
|
||||||
"message": "Light"
|
"message": "Light"
|
||||||
},
|
},
|
||||||
"dark_default": {
|
"dark_default": {
|
||||||
"message": "Default"
|
"message": "Default"
|
||||||
},
|
},
|
||||||
"dark_dark": {
|
"dark_dark": {
|
||||||
"message": "Dark"
|
"message": "Dark"
|
||||||
},
|
},
|
||||||
"dark_darker": {
|
"dark_darker": {
|
||||||
"message": "Darker"
|
"message": "Darker"
|
||||||
},
|
},
|
||||||
"dark_mint": {
|
"dark_mint": {
|
||||||
"message": "Mint"
|
"message": "Mint"
|
||||||
},
|
},
|
||||||
"dark_blue": {
|
"dark_blue": {
|
||||||
"message": "Blue Tint"
|
"message": "Blue Tint"
|
||||||
},
|
},
|
||||||
"dark_burn": {
|
"dark_burn": {
|
||||||
"message": "Eyeball Burn"
|
"message": "Eyeball Burn"
|
||||||
},
|
},
|
||||||
"dark_unicorn": {
|
"dark_unicorn": {
|
||||||
"message": "Unicorn"
|
"message": "Unicorn"
|
||||||
},
|
},
|
||||||
"dark_lightmode": {
|
"dark_lightmode": {
|
||||||
"message": "Lightmode"
|
"message": "Lightmode"
|
||||||
},
|
},
|
||||||
"dark_catppuccin": {
|
"dark_catppuccin": {
|
||||||
"message": "Catppuccin\n By Angel"
|
"message": "Catppuccin\n By Angel"
|
||||||
},
|
},
|
||||||
"dark_sage": {
|
"dark_sage": {
|
||||||
"message": "Sabia\n By LivDoesDevStuf"
|
"message": "Sabia\n By LivDoesDevStuf"
|
||||||
},
|
},
|
||||||
"dark_pink": {
|
"dark_pink": {
|
||||||
"message": "Pink\n By Sayde"
|
"message": "Pink\n By Sayde"
|
||||||
},
|
},
|
||||||
"dark_coral": {
|
"dark_coral": {
|
||||||
"message": "Coral\n By Superchido"
|
"message": "Coral\n By Superchido"
|
||||||
},
|
},
|
||||||
"dp_sidebar_text": {
|
"dp_sidebar_text": {
|
||||||
"message": "Sidebar Text"
|
"message": "Sidebar Text"
|
||||||
},
|
},
|
||||||
"dp_sidebar": {
|
"dp_sidebar": {
|
||||||
"message": "Sidebar"
|
"message": "Sidebar"
|
||||||
},
|
},
|
||||||
"radio_sidebar_solid": {
|
"radio_sidebar_solid": {
|
||||||
"message": "Solid"
|
"message": "Solid"
|
||||||
},
|
},
|
||||||
"radio_sidebar_gradient": {
|
"radio_sidebar_gradient": {
|
||||||
"message": "Gradient"
|
"message": "Gradient"
|
||||||
},
|
},
|
||||||
"radio_sidebar_image": {
|
"radio_sidebar_image": {
|
||||||
"message": "Image"
|
"message": "Image"
|
||||||
},
|
},
|
||||||
"dp_background_0": {
|
"dp_background_0": {
|
||||||
"message": "Background Main"
|
"message": "Background Main"
|
||||||
},
|
},
|
||||||
"dp_background_1": {
|
"dp_background_1": {
|
||||||
"message": "Background Alt"
|
"message": "Background Alt"
|
||||||
},
|
},
|
||||||
"dp_background_2": {
|
"dp_background_2": {
|
||||||
"message": "Background Other"
|
"message": "Background Other"
|
||||||
},
|
},
|
||||||
"dp_borders": {
|
"dp_borders": {
|
||||||
"message": "Borders"
|
"message": "Borders"
|
||||||
},
|
},
|
||||||
"dp_links": {
|
"dp_links": {
|
||||||
"message": "Links"
|
"message": "Links"
|
||||||
},
|
},
|
||||||
"dp_text_0": {
|
"dp_text_0": {
|
||||||
"message": "Text Main"
|
"message": "Text Main"
|
||||||
},
|
},
|
||||||
"dp_text_1": {
|
"dp_text_1": {
|
||||||
"message": "Text Other"
|
"message": "Text Other"
|
||||||
},
|
},
|
||||||
"dp_text_2": {
|
"dp_text_2": {
|
||||||
"message": "Text Alt"
|
"message": "Text Alt"
|
||||||
},
|
},
|
||||||
"dark_mode_fixer": {
|
"dark_mode_fixer": {
|
||||||
"message": "Dark Mode Fixer (Beta)"
|
"message": "Dark Mode Fixer (Beta)"
|
||||||
},
|
},
|
||||||
"dark_mode_fixer_text": {
|
"dark_mode_fixer_text": {
|
||||||
"message": "This Will Try To Fix Any Dark Mode Gaps On The Current Page."
|
"message": "This Will Try To Fix Any Dark Mode Gaps On The Current Page."
|
||||||
},
|
},
|
||||||
"fix_dm_btn": {
|
"fix_dm_btn": {
|
||||||
"message": "Go"
|
"message": "Go"
|
||||||
},
|
},
|
||||||
"fix_dm_text": {
|
"fix_dm_text": {
|
||||||
"message": "The Fixer Is Automatically Running On These Pages:"
|
"message": "The Fixer Is Automatically Running On These Pages:"
|
||||||
},
|
},
|
||||||
"hide_links": {
|
"hide_links": {
|
||||||
"message": "Hide links"
|
"message": "Hide links"
|
||||||
},
|
},
|
||||||
"show_links": {
|
"show_links": {
|
||||||
"message": "Show links"
|
"message": "Show links"
|
||||||
},
|
},
|
||||||
"center_cards": {
|
"center_cards": {
|
||||||
"message": "Center Cards"
|
"message": "Center Cards"
|
||||||
},
|
},
|
||||||
"hide_new_canvas": {
|
"hide_new_canvas": {
|
||||||
"message": "Hide New Dashboard"
|
"message": "Hide New Dashboard"
|
||||||
},
|
},
|
||||||
"global_search": {
|
"hide_sequence_footer": {
|
||||||
"message": "Global Canvas Search"
|
"message": "Hide Next/Previous Buttons"
|
||||||
}
|
},
|
||||||
|
"global_search": {
|
||||||
|
"message": "Global Canvas Search"
|
||||||
|
},
|
||||||
|
"grade_analytics": {
|
||||||
|
"message": "Grade Analytics"
|
||||||
|
},
|
||||||
|
"grade_analytics_desc": {
|
||||||
|
"message": "Adds an Analytics toggle to the left side of course grade pages with a score-distribution chart and your overall grade over time."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -365,3 +365,4 @@
|
|||||||
border: 1px solid var(--bcborders, #c7cdd1); border-bottom-width: 2px; border-radius: 4px;
|
border: 1px solid var(--bcborders, #c7cdd1); border-bottom-width: 2px; border-radius: 4px;
|
||||||
padding: 0 4px; color: var(--bctext-1, #5b6770);
|
padding: 0 4px; color: var(--bctext-1, #5b6770);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ h2 {font-size:24px;}
|
|||||||
#numAssignmentsSlider::-webkit-slider-thumb, #numTodoItemsSlider::-webkit-slider-thumb, #sidebarScaleSlider::-webkit-slider-thumb, #customBackgroundScale::-webkit-slider-thumb, #bgOpacitySlider::-webkit-slider-thumb, #sidebarOpacitySlider::-webkit-slider-thumb, #bgBlurSlider::-webkit-slider-thumb, #sidebarBlurSlider::-webkit-slider-thumb, #cardOpacitySlider::-webkit-slider-thumb, #cardBlurSlider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none;height: 18px; width: 18px;background: #727272;cursor: pointer;border-radius:30px;}
|
#numAssignmentsSlider::-webkit-slider-thumb, #numTodoItemsSlider::-webkit-slider-thumb, #sidebarScaleSlider::-webkit-slider-thumb, #customBackgroundScale::-webkit-slider-thumb, #bgOpacitySlider::-webkit-slider-thumb, #sidebarOpacitySlider::-webkit-slider-thumb, #bgBlurSlider::-webkit-slider-thumb, #sidebarBlurSlider::-webkit-slider-thumb, #cardOpacitySlider::-webkit-slider-thumb, #cardBlurSlider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none;height: 18px; width: 18px;background: #727272;cursor: pointer;border-radius:30px;}
|
||||||
.option-container {font-size: 14px; margin-top: 8px;background: var(--containerbg);padding: 18px;border-radius: 14px;box-sizing:border-box}
|
.option-container {font-size: 14px; margin-top: 8px;background: var(--containerbg);padding: 18px;border-radius: 14px;box-sizing:border-box}
|
||||||
.options .option-container {padding: 14px;}
|
.options .option-container {padding: 14px;}
|
||||||
|
.options .option-container > .option + .option {margin-top: 10px;}
|
||||||
a.option-container {display: block;color:#5ca5f6;font-size: 14px;}
|
a.option-container {display: block;color:#5ca5f6;font-size: 14px;}
|
||||||
a.option-container:hover {background: #333}
|
a.option-container:hover {background: #333}
|
||||||
#customDomain {font-size: 12px; background: var(--inputbg); border: none;padding:8px;color: #e2e2e2;margin-top: 6px;border-radius: 7px; font-family: inherit;}
|
#customDomain {font-size: 12px; background: var(--inputbg); border: none;padding:8px;color: #e2e2e2;margin-top: 6px;border-radius: 7px; font-family: inherit;}
|
||||||
|
|||||||
@ -76,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="option-name" data-i18n="dark_mode">Dark mode</span>
|
<span class="option-name" data-i18n="dark_mode">Dark mode</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="option" id="auto_dark" style="margin-top:10px">
|
<div class="option" id="auto_dark">
|
||||||
<input type="radio" id="off" name="auto_dark">
|
<input type="radio" id="off" name="auto_dark">
|
||||||
<input type="radio" id="on" name="auto_dark">
|
<input type="radio" id="on" name="auto_dark">
|
||||||
<div class="slider">
|
<div class="slider">
|
||||||
@ -203,6 +203,14 @@
|
|||||||
<span class="cr-info-bubble-text" data-i18n="quiz_safe_mode_desc">Turns off most Canvas Refined features on quiz pages so you get the default Canvas quiz experience.</span>
|
<span class="cr-info-bubble-text" data-i18n="quiz_safe_mode_desc">Turns off most Canvas Refined features on quiz pages so you get the default Canvas quiz experience.</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="option" id="dashboard_notes">
|
||||||
|
<input type="radio" id="off" name="dashboard_notes">
|
||||||
|
<input type="radio" id="on" name="dashboard_notes">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="sliderknob"></div>
|
||||||
|
<div class="sliderbg"></div>
|
||||||
|
</div><span class="option-name" data-i18n="dashboard_notes">Dashboard Notes</span>
|
||||||
|
</div>
|
||||||
<div class="option" id="global_search">
|
<div class="option" id="global_search">
|
||||||
<input type="radio" id="off" name="global_search">
|
<input type="radio" id="off" name="global_search">
|
||||||
<input type="radio" id="on" name="global_search">
|
<input type="radio" id="on" name="global_search">
|
||||||
@ -211,6 +219,18 @@
|
|||||||
<div class="sliderbg"></div>
|
<div class="sliderbg"></div>
|
||||||
</div><span class="option-name" data-i18n="global_search">Global Canvas Search</span>
|
</div><span class="option-name" data-i18n="global_search">Global Canvas Search</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="option" id="grade_analytics">
|
||||||
|
<input type="radio" id="off" name="grade_analytics">
|
||||||
|
<input type="radio" id="on" name="grade_analytics">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="sliderknob"></div>
|
||||||
|
<div class="sliderbg"></div>
|
||||||
|
</div><span class="option-name" data-i18n="grade_analytics">Grade Analytics</span>
|
||||||
|
<span class="cr-info-bubble" tabindex="0" role="button" aria-label="Grade Analytics info">
|
||||||
|
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||||
|
<span class="cr-info-bubble-text" data-i18n="grade_analytics_desc">Adds an Analytics toggle to the left side of course grade pages with a score-distribution chart and your overall grade over time.</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="social-buttons">
|
<div class="social-buttons">
|
||||||
<a class="discord-button" href="https://discord.gg/GjFh4JA3wh" target="_blank" rel="noopener noreferrer" title="Join our Discord">
|
<a class="discord-button" href="https://discord.gg/GjFh4JA3wh" target="_blank" rel="noopener noreferrer" title="Join our Discord">
|
||||||
@ -326,7 +346,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="option-container" style="display:flex;flex-direction:column;gap:6px;">
|
<div class="option-container">
|
||||||
<div class="option" id="center_cards">
|
<div class="option" id="center_cards">
|
||||||
<input type="radio" id="off" name="center_cards">
|
<input type="radio" id="off" name="center_cards">
|
||||||
<input type="radio" id="on" name="center_cards">
|
<input type="radio" id="on" name="center_cards">
|
||||||
@ -336,14 +356,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="option-name" data-i18n="center_cards">Center Cards</span>
|
<span class="option-name" data-i18n="center_cards">Center Cards</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="option" id="dashboard_notes">
|
|
||||||
<input type="radio" id="off" name="dashboard_notes">
|
|
||||||
<input type="radio" id="on" name="dashboard_notes">
|
|
||||||
<div class="slider">
|
|
||||||
<div class="sliderknob"></div>
|
|
||||||
<div class="sliderbg"></div>
|
|
||||||
</div><span class="option-name" data-i18n="dashboard_notes">Dashboard Notes</span>
|
|
||||||
</div>
|
|
||||||
<div class="option" id="gradient_cards">
|
<div class="option" id="gradient_cards">
|
||||||
<input type="radio" id="off" name="gradient_cards">
|
<input type="radio" id="off" name="gradient_cards">
|
||||||
<input type="radio" id="on" name="gradient_cards">
|
<input type="radio" id="on" name="gradient_cards">
|
||||||
@ -394,6 +406,14 @@
|
|||||||
<div class="sliderbg"></div>
|
<div class="sliderbg"></div>
|
||||||
</div><span class="option-name" data-i18n="hide_new_canvas">Hide New Dashboard</span>
|
</div><span class="option-name" data-i18n="hide_new_canvas">Hide New Dashboard</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="option" id="hide_sequence_footer">
|
||||||
|
<input type="radio" id="off" name="hide_sequence_footer">
|
||||||
|
<input type="radio" id="on" name="hide_sequence_footer">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="sliderknob"></div>
|
||||||
|
<div class="sliderbg"></div>
|
||||||
|
</div><span class="option-name" data-i18n="hide_sequence_footer">Hide Next/Previous Buttons</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="option-container">
|
<div class="option-container">
|
||||||
<div class="customDomain">
|
<div class="customDomain">
|
||||||
|
|||||||
@ -75,6 +75,7 @@ chrome.runtime.onInstalled.addListener(function () {
|
|||||||
"relative_dues": false,
|
"relative_dues": false,
|
||||||
"equal_height_cards": false,
|
"equal_height_cards": false,
|
||||||
"hide_new_canvas": true,
|
"hide_new_canvas": true,
|
||||||
|
"hide_sequence_footer": false,
|
||||||
"quiz_safe_mode": false,
|
"quiz_safe_mode": false,
|
||||||
"dark_mode_fix": [],
|
"dark_mode_fix": [],
|
||||||
"assignment_states": {},
|
"assignment_states": {},
|
||||||
@ -116,6 +117,7 @@ chrome.runtime.onInstalled.addListener(function () {
|
|||||||
"bg_blur": 8,
|
"bg_blur": 8,
|
||||||
"sidebar_blur": 0,
|
"sidebar_blur": 0,
|
||||||
"global_search": false,
|
"global_search": false,
|
||||||
|
"grade_analytics": true,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
653
js/content.js
653
js/content.js
@ -15,6 +15,7 @@ function setupNavigationListener() {
|
|||||||
watchSequenceFooter();
|
watchSequenceFooter();
|
||||||
watchSubmissionPageButton();
|
watchSubmissionPageButton();
|
||||||
watchProfileLogoutPageButton();
|
watchProfileLogoutPageButton();
|
||||||
|
watchGradeAnalytics();
|
||||||
};
|
};
|
||||||
for (const method of ["pushState", "replaceState"]) {
|
for (const method of ["pushState", "replaceState"]) {
|
||||||
const orig = history[method];
|
const orig = history[method];
|
||||||
@ -106,6 +107,7 @@ let submissionPageButtonObserver = null;
|
|||||||
let submissionButtonScheduled = false;
|
let submissionButtonScheduled = false;
|
||||||
let profileLogoutButtonObserver = null;
|
let profileLogoutButtonObserver = null;
|
||||||
let newCanvasButtonObserver = null;
|
let newCanvasButtonObserver = null;
|
||||||
|
let sequenceFooterObserver = null;
|
||||||
|
|
||||||
function addSubmissionPageButton() {
|
function addSubmissionPageButton() {
|
||||||
const assignmentLink = getSubmissionAssignmentLink();
|
const assignmentLink = getSubmissionAssignmentLink();
|
||||||
@ -211,6 +213,7 @@ function isAssignmentPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeSequenceFooter() {
|
function removeSequenceFooter() {
|
||||||
|
if (options.hide_sequence_footer !== true) return false;
|
||||||
if (!isAssignmentPage()) return false;
|
if (!isAssignmentPage()) return false;
|
||||||
const sequenceFooter = document.getElementById("sequence_footer");
|
const sequenceFooter = document.getElementById("sequence_footer");
|
||||||
if (!sequenceFooter) return false;
|
if (!sequenceFooter) return false;
|
||||||
@ -218,11 +221,39 @@ function removeSequenceFooter() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSS-based hiding is the primary mechanism: the style element persists across
|
||||||
|
// Canvas re-renders and full reloads, so the footer can never flash back after
|
||||||
|
// the JS observer has removed it (or timed out) once.
|
||||||
|
function applyHideSequenceFooter() {
|
||||||
|
let style = document.getElementById("canvasrefined-hide-sequence-footer");
|
||||||
|
if (options.hide_sequence_footer === true) {
|
||||||
|
if (!style) {
|
||||||
|
style = document.createElement("style");
|
||||||
|
style.id = "canvasrefined-hide-sequence-footer";
|
||||||
|
style.textContent = "#sequence_footer{display:none!important}";
|
||||||
|
(document.head || document.documentElement).appendChild(style);
|
||||||
|
}
|
||||||
|
} else if (style) {
|
||||||
|
style.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function watchSequenceFooter() {
|
function watchSequenceFooter() {
|
||||||
|
applyHideSequenceFooter();
|
||||||
|
if (options.hide_sequence_footer !== true) {
|
||||||
|
if (sequenceFooterObserver) {
|
||||||
|
sequenceFooterObserver.disconnect();
|
||||||
|
sequenceFooterObserver = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!isAssignmentPage()) return;
|
if (!isAssignmentPage()) return;
|
||||||
if (removeSequenceFooter()) return;
|
if (removeSequenceFooter()) return;
|
||||||
if (sequenceFooterObserver) return;
|
if (sequenceFooterObserver) return;
|
||||||
|
|
||||||
|
// The observer strips the footer from the DOM (no leftover gap), and
|
||||||
|
// disconnects once removed — after that (or after the 10s timeout below)
|
||||||
|
// the CSS rule above is what keeps it hidden across Canvas re-renders.
|
||||||
sequenceFooterObserver = new MutationObserver(() => {
|
sequenceFooterObserver = new MutationObserver(() => {
|
||||||
if (removeSequenceFooter() && sequenceFooterObserver) {
|
if (removeSequenceFooter() && sequenceFooterObserver) {
|
||||||
sequenceFooterObserver.disconnect();
|
sequenceFooterObserver.disconnect();
|
||||||
@ -682,6 +713,7 @@ function startExtension() {
|
|||||||
ensureBetterSidebar();
|
ensureBetterSidebar();
|
||||||
watchSequenceFooter();
|
watchSequenceFooter();
|
||||||
watchProfileLogoutPageButton();
|
watchProfileLogoutPageButton();
|
||||||
|
watchGradeAnalytics();
|
||||||
|
|
||||||
setupQuizSafeModeBanner();
|
setupQuizSafeModeBanner();
|
||||||
|
|
||||||
@ -825,6 +857,9 @@ function applyOptionsChanges(changes) {
|
|||||||
case "hide_new_canvas":
|
case "hide_new_canvas":
|
||||||
watchNewCanvasButton();
|
watchNewCanvasButton();
|
||||||
break;
|
break;
|
||||||
|
case "hide_sequence_footer":
|
||||||
|
watchSequenceFooter();
|
||||||
|
break;
|
||||||
case "global_search":
|
case "global_search":
|
||||||
if (options.global_search) {
|
if (options.global_search) {
|
||||||
setupGlobalSearch();
|
setupGlobalSearch();
|
||||||
@ -918,6 +953,9 @@ function applyOptionsChanges(changes) {
|
|||||||
resetBetterSidebarLayout();
|
resetBetterSidebarLayout();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "grade_analytics":
|
||||||
|
watchGradeAnalytics();
|
||||||
|
break;
|
||||||
case "quiz_safe_mode":
|
case "quiz_safe_mode":
|
||||||
// Toggling safe mode changes which features run on quiz pages; reload
|
// Toggling safe mode changes which features run on quiz pages; reload
|
||||||
// so the gating is applied cleanly.
|
// so the gating is applied cleanly.
|
||||||
@ -6274,6 +6312,621 @@ function getAssignments() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== Grade Analytics =====================
|
||||||
|
// On course grades pages, adds an "Analytics" toggle on the left side (in the
|
||||||
|
// Better Sidebar when enabled, otherwise in the native course nav) that shows
|
||||||
|
// a panel with a score-distribution doughnut and an overall-grade-over-time
|
||||||
|
// line chart. Data comes from the Canvas API with the user's session, so it
|
||||||
|
// matches the numbers on the page. Charts are hand-drawn on <canvas> so the
|
||||||
|
// extension needs no CDN/library and no chart library dependency.
|
||||||
|
|
||||||
|
const GA_BUCKETS = [
|
||||||
|
{ label: "90+", min: 90, max: Infinity, color: "#16a34a" },
|
||||||
|
{ label: "80-89", min: 80, max: 90, color: "#4ade80" },
|
||||||
|
{ label: "70-79", min: 70, max: 80, color: "#a3e635" },
|
||||||
|
{ label: "60-69", min: 60, max: 70, color: "#facc15" },
|
||||||
|
{ label: "50-59", min: 50, max: 60, color: "#fb923c" },
|
||||||
|
{ label: "40-49", min: 40, max: 50, color: "#f87171" },
|
||||||
|
{ label: "30-39", min: 30, max: 40, color: "#ef4444" },
|
||||||
|
{ label: "20-29", min: 20, max: 30, color: "#dc2626" },
|
||||||
|
{ label: "10-19", min: 10, max: 20, color: "#b91c1c" },
|
||||||
|
{ label: "0-9", min: 0, max: 10, color: "#7f1d1d" },
|
||||||
|
];
|
||||||
|
const GA_UNGRADED_COLOR = "#6b7280";
|
||||||
|
const GA_OPEN_KEY = "grade_analytics_open";
|
||||||
|
|
||||||
|
async function getGradeAnalyticsOpenState() {
|
||||||
|
const result = await chrome.storage.local.get(GA_OPEN_KEY);
|
||||||
|
return result[GA_OPEN_KEY] ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGradeAnalyticsOpenState(open) {
|
||||||
|
chrome.storage.local.set({ [GA_OPEN_KEY]: open });
|
||||||
|
}
|
||||||
|
|
||||||
|
const GA_FIT_Y_KEY = "grade_analytics_fit_y";
|
||||||
|
|
||||||
|
async function getGradeAnalyticsFitY() {
|
||||||
|
const result = await chrome.storage.local.get(GA_FIT_Y_KEY);
|
||||||
|
return result[GA_FIT_Y_KEY] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGradeAnalyticsFitY(fit) {
|
||||||
|
chrome.storage.local.set({ [GA_FIT_Y_KEY]: fit });
|
||||||
|
}
|
||||||
|
|
||||||
|
let gaObserver = null;
|
||||||
|
let gaOpen = false; // panel open on this page view
|
||||||
|
let gaFitY = false; // scale the line chart Y axis to fit the data
|
||||||
|
let gaCourseId = null; // course whose data is cached
|
||||||
|
let gaData = null; // computed data for the current course
|
||||||
|
let gaLoading = false;
|
||||||
|
|
||||||
|
function gradeAnalyticsActive() {
|
||||||
|
return options.grade_analytics === true && isGradesPage() && !quizSafeModeActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paginated Canvas API GET using the user's session. Unwraps Firefox Xray
|
||||||
|
// proxies like getData() does. The first page is fetched serially to learn the
|
||||||
|
// total page count from its Link header; every remaining page is then fetched
|
||||||
|
// concurrently, so courses with many assignments don't pay one round trip per
|
||||||
|
// page.
|
||||||
|
async function gaFetchAll(path) {
|
||||||
|
const fetchPage = async (url) => {
|
||||||
|
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
||||||
|
if (!res.ok) throw new Error("Canvas API " + res.status + " for " + url.pathname);
|
||||||
|
return { data: JSON.parse(JSON.stringify(await res.json())), link: res.headers.get("Link") || "" };
|
||||||
|
};
|
||||||
|
const firstUrl = new URL(path, domain);
|
||||||
|
firstUrl.searchParams.set("per_page", "100");
|
||||||
|
const { data: first, link } = await fetchPage(firstUrl);
|
||||||
|
const out = Array.isArray(first) ? first.slice() : [];
|
||||||
|
const lastMatch = link.match(/<([^>]+)>;\s*rel="last"/);
|
||||||
|
if (lastMatch) {
|
||||||
|
const lastPage = parseInt(new URL(lastMatch[1]).searchParams.get("page") || "1", 10);
|
||||||
|
if (lastPage > 1) {
|
||||||
|
const pageUrls = [];
|
||||||
|
for (let p = 2; p <= lastPage; p++) {
|
||||||
|
const u = new URL(path, domain);
|
||||||
|
u.searchParams.set("per_page", "100");
|
||||||
|
u.searchParams.set("page", String(p));
|
||||||
|
pageUrls.push(u);
|
||||||
|
}
|
||||||
|
const rest = await Promise.all(pageUrls.map(u => fetchPage(u).then(r => r.data)));
|
||||||
|
for (const page of rest) if (Array.isArray(page)) out.push(...page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry point: called at init, on SPA navigation, and when the option changes.
|
||||||
|
function watchGradeAnalytics() {
|
||||||
|
if (!gradeAnalyticsActive()) {
|
||||||
|
removeGradeAnalyticsPanel();
|
||||||
|
if (gaObserver) { gaObserver.disconnect(); gaObserver = null; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SPA navigation between courses: drop cached data so the panel never
|
||||||
|
// shows the previous course's charts.
|
||||||
|
const courseId = getCurrentCourseId();
|
||||||
|
if (gaCourseId !== null && gaCourseId !== courseId) {
|
||||||
|
gaData = null;
|
||||||
|
gaCourseId = null;
|
||||||
|
removeGradeAnalyticsPanel();
|
||||||
|
}
|
||||||
|
if (!gaObserver) {
|
||||||
|
// Canvas re-renders the left nav and content area during SPA
|
||||||
|
// navigation; the observer keeps the panel placed.
|
||||||
|
gaObserver = new MutationObserver(() => scheduleGradeAnalyticsSync());
|
||||||
|
gaObserver.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
scheduleGradeAnalyticsSync();
|
||||||
|
// Restore the open/closed state and Y-axis preference the user last
|
||||||
|
// chose, then inject the panel below the Print Grades header.
|
||||||
|
Promise.all([getGradeAnalyticsOpenState(), getGradeAnalyticsFitY()]).then(([open, fit]) => {
|
||||||
|
gaOpen = open;
|
||||||
|
gaFitY = fit;
|
||||||
|
ensureGradeAnalyticsPanel();
|
||||||
|
if (gaOpen && gaData) renderGradeAnalytics();
|
||||||
|
});
|
||||||
|
if (!gaData && !gaLoading) loadGradeAnalytics();
|
||||||
|
}
|
||||||
|
|
||||||
|
let gaSyncRaf = null;
|
||||||
|
function scheduleGradeAnalyticsSync() {
|
||||||
|
if (gaSyncRaf) return;
|
||||||
|
gaSyncRaf = requestAnimationFrame(() => {
|
||||||
|
gaSyncRaf = null;
|
||||||
|
syncGradeAnalyticsUI();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncGradeAnalyticsUI() {
|
||||||
|
if (!gradeAnalyticsActive()) return;
|
||||||
|
const panel = ensureGradeAnalyticsPanel();
|
||||||
|
if (!panel) return;
|
||||||
|
// Self-heal: the one-shot render after data loads can be a no-op when the
|
||||||
|
// panel was created before Canvas finished laying out the page (zero-size
|
||||||
|
// canvases) or while the body was still hidden. gaSetupCanvas leaves the
|
||||||
|
// canvas backing store at width 0 in that case, so a 0-width canvas means
|
||||||
|
// "never drawn" — redraw now that layout is real.
|
||||||
|
if (gaOpen && gaData) {
|
||||||
|
const pie = panel.querySelector("#canvasrefined-ga-pie");
|
||||||
|
const line = panel.querySelector("#canvasrefined-ga-line");
|
||||||
|
if ((pie && pie.width === 0) || (line && line.width === 0)) {
|
||||||
|
renderGradeAnalytics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeGradeAnalyticsPanel() {
|
||||||
|
gaOpen = false;
|
||||||
|
document.getElementById("canvasrefined-grade-analytics")?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applies the in-memory open/closed state (restored from storage) to the panel
|
||||||
|
// DOM. Called both at panel creation and whenever an already-attached panel
|
||||||
|
// is reused, so a stored preference is never lost to a creation race (the DOM
|
||||||
|
// observer can build the panel before the storage read resolves).
|
||||||
|
function applyGradeAnalyticsOpenState(panel) {
|
||||||
|
const body = panel.querySelector("#canvasrefined-ga-body");
|
||||||
|
const btn = panel.querySelector("#canvasrefined-ga-toggle");
|
||||||
|
if (!body || !btn) return;
|
||||||
|
body.style.display = gaOpen ? "" : "none";
|
||||||
|
const svg = btn.querySelector("svg");
|
||||||
|
if (svg) svg.style.transform = gaOpen ? "rotate(180deg)" : "rotate(0deg)";
|
||||||
|
btn.setAttribute("aria-expanded", String(gaOpen));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panel is injected directly below the "Print Grades" action header on the
|
||||||
|
// grades page. Returns null (and retries via the DOM observer) if the anchor
|
||||||
|
// hasn't rendered yet.
|
||||||
|
function ensureGradeAnalyticsPanel() {
|
||||||
|
let panel = document.getElementById("canvasrefined-grade-analytics");
|
||||||
|
const anchor = document.getElementById("print-grades-container");
|
||||||
|
if (panel && panel.isConnected) {
|
||||||
|
// Canvas re-renders can shift the anchor or our position; keep the
|
||||||
|
// panel directly after #print-grades-container at all times.
|
||||||
|
if (anchor && panel.previousElementSibling !== anchor) {
|
||||||
|
anchor.insertAdjacentElement("afterend", panel);
|
||||||
|
}
|
||||||
|
// Re-apply the open/closed state in case it was restored from storage
|
||||||
|
// after this panel was first created.
|
||||||
|
applyGradeAnalyticsOpenState(panel);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
const container = anchor || findContentContainer();
|
||||||
|
if (!container) return null;
|
||||||
|
panel = document.createElement("div");
|
||||||
|
panel.id = "canvasrefined-grade-analytics";
|
||||||
|
if (anchor) {
|
||||||
|
anchor.insertAdjacentElement("afterend", panel);
|
||||||
|
} else {
|
||||||
|
// Fallback: top of the content container until the anchor renders.
|
||||||
|
container.insertBefore(panel, container.firstChild);
|
||||||
|
}
|
||||||
|
panel.style.cssText = `margin:18px 0;padding:16px;border:1px solid color-mix(in srgb, var(--bcborders) 75%, transparent);border-radius:10px;background-color:var(--bcbackground-0);color:var(--bctext-0);font-family:"Lato","Helvetica Neue",Helvetica,Arial,sans-serif;box-sizing:border-box;`;
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
|
<h2 style="margin:0;font-size:18px;color:var(--bctext-0);">Grade Analytics</h2>
|
||||||
|
<button id="canvasrefined-ga-toggle" type="button" aria-expanded="true" title="Toggle Grade Analytics" style="margin-left:auto;background:var(--bcbackground-1);color:var(--bctext-0);border:1px solid var(--bcborders);border-radius:8px;padding:4px 12px;font-size:14px;line-height:1.4;cursor:pointer;"><svg style="transform:rotate(180deg);display:block;" fill="currentColor" width="16px" height="16px" viewBox="-6.5 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="1.6"><g stroke-width="0"/><g stroke-linecap="round" stroke-linejoin="round"/><path d="M18.813 11.406l-7.906 9.906c-0.75 0.906-1.906 0.906-2.625 0l-7.906-9.906c-0.75-0.938-0.375-1.656 0.781-1.656h16.875c1.188 0 1.531 0.719 0.781 1.656z"/></svg></button>
|
||||||
|
</div>
|
||||||
|
<div id="canvasrefined-ga-body">
|
||||||
|
<p id="canvasrefined-ga-status" style="margin:0 0 10px;color:var(--bctext-1);font-size:13px;">Loading grade data…</p>
|
||||||
|
<div id="canvasrefined-ga-stats" style="display:none;flex-wrap:wrap;gap:10px;margin-bottom:14px;"></div>
|
||||||
|
<div id="canvasrefined-ga-charts" style="display:none;gap:24px;flex-wrap:wrap;">
|
||||||
|
<div id="canvasrefined-ga-box-pie" style="flex:1 1 calc(33.333% - 8px);min-width:0;">
|
||||||
|
<h3 style="margin:0 0 8px;font-size:14px;color:var(--bctext-0);">Score distribution (graded assignments)</h3>
|
||||||
|
<div style="position:relative;height:280px;"><canvas id="canvasrefined-ga-pie"></canvas><div id="canvasrefined-ga-pie-tip" style="position:absolute;display:none;pointer-events:none;background:var(--bcbackground-1);color:var(--bctext-0);border:1px solid var(--bcborders);border-radius:6px;padding:6px 10px;font-size:12px;z-index:10;white-space:nowrap;"></div></div>
|
||||||
|
</div>
|
||||||
|
<div id="canvasrefined-ga-box-line" style="flex:1 1 calc(66.666% - 16px);min-width:0;">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;margin:0 0 8px;">
|
||||||
|
<h3 style="margin:0;font-size:14px;color:var(--bctext-0);">Overall grade over time</h3>
|
||||||
|
<label for="canvasrefined-ga-fity" style="margin-left:auto;display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--bctext-1);cursor:pointer;user-select:none;"><input type="checkbox" id="canvasrefined-ga-fity"> Fit Y axis</label>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative;height:280px;"><canvas id="canvasrefined-ga-line"></canvas><div id="canvasrefined-ga-line-tip" style="position:absolute;display:none;pointer-events:none;background:var(--bcbackground-1);color:var(--bctext-0);border:1px solid var(--bcborders);border-radius:6px;padding:8px 10px;font-size:12px;z-index:20;max-width:260px;box-shadow:0 4px 14px rgba(0,0,0,0.25);"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const toggleBtn = panel.querySelector("#canvasrefined-ga-toggle");
|
||||||
|
const fitYCheckbox = panel.querySelector("#canvasrefined-ga-fity");
|
||||||
|
const applyOpenState = () => applyGradeAnalyticsOpenState(panel);
|
||||||
|
toggleBtn.addEventListener("click", () => {
|
||||||
|
gaOpen = !gaOpen;
|
||||||
|
setGradeAnalyticsOpenState(gaOpen);
|
||||||
|
applyOpenState();
|
||||||
|
if (gaOpen && gaData) renderGradeAnalytics();
|
||||||
|
});
|
||||||
|
// "Fit Y axis" scales the line chart's Y axis to the data instead of a
|
||||||
|
// fixed 0-100; the choice is remembered across pages via chrome.storage.
|
||||||
|
fitYCheckbox.checked = gaFitY;
|
||||||
|
fitYCheckbox.addEventListener("change", () => {
|
||||||
|
gaFitY = fitYCheckbox.checked;
|
||||||
|
setGradeAnalyticsFitY(gaFitY);
|
||||||
|
if (gaData) {
|
||||||
|
gaDrawLine(panel.querySelector("#canvasrefined-ga-line"), panel.querySelector("#canvasrefined-ga-line-tip"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
applyOpenState();
|
||||||
|
// If the data finished loading before this panel was created (or before
|
||||||
|
// the stored open state was restored), the earlier render call found no
|
||||||
|
// panel — draw the charts now that it exists.
|
||||||
|
if (gaOpen && gaData) renderGradeAnalytics();
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGradeAnalytics() {
|
||||||
|
const courseId = getCurrentCourseId();
|
||||||
|
if (courseId == null) return;
|
||||||
|
gaLoading = true;
|
||||||
|
const status = document.getElementById("canvasrefined-ga-status");
|
||||||
|
if (status) status.textContent = "Loading grade data…";
|
||||||
|
try {
|
||||||
|
const [assignments, groups] = await Promise.all([
|
||||||
|
gaFetchAll(`/api/v1/courses/${courseId}/assignments?include[]=submission&order_by=due_at`),
|
||||||
|
gaFetchAll(`/api/v1/courses/${courseId}/assignment_groups`),
|
||||||
|
]);
|
||||||
|
gaCourseId = courseId;
|
||||||
|
gaData = computeGradeAnalytics(assignments, groups);
|
||||||
|
gaLoading = false;
|
||||||
|
// renderGradeAnalytics self-guards on panel existence and canvas
|
||||||
|
// size; if the panel isn't ready yet, the retry below in
|
||||||
|
// ensureGradeAnalyticsPanel draws once it is.
|
||||||
|
renderGradeAnalytics();
|
||||||
|
} catch (err) {
|
||||||
|
logError(err);
|
||||||
|
const s = document.getElementById("canvasrefined-ga-status");
|
||||||
|
if (s) s.textContent = "Grade Analytics failed to load: " + (err && err.message ? err.message : err);
|
||||||
|
} finally {
|
||||||
|
gaLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gaIsGraded(a) {
|
||||||
|
return a.published !== false && a.points_possible > 0 &&
|
||||||
|
(a.submission_types || []).indexOf("not_graded") === -1 &&
|
||||||
|
a.submission && a.submission.score != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gaIsUngraded(a) {
|
||||||
|
// Graded-type assignment with points possible but no score yet.
|
||||||
|
if (a.published === false || a.points_possible <= 0) return false;
|
||||||
|
if ((a.submission_types || []).includes("not_graded")) return false;
|
||||||
|
return !(a.submission && a.submission.score != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeGradeAnalytics(assignments, groups) {
|
||||||
|
const counts = GA_BUCKETS.map(() => 0);
|
||||||
|
let ungraded = 0, graded = 0;
|
||||||
|
for (const a of assignments) {
|
||||||
|
if (gaIsUngraded(a)) { ungraded++; continue; }
|
||||||
|
if (!gaIsGraded(a)) continue;
|
||||||
|
const pct = (a.submission.score / a.points_possible) * 100;
|
||||||
|
const idx = GA_BUCKETS.findIndex(b => pct >= b.min && pct < b.max);
|
||||||
|
counts[idx >= 0 ? idx : GA_BUCKETS.length - 1]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupName = {};
|
||||||
|
const weight = {};
|
||||||
|
for (const g of groups) {
|
||||||
|
groupName[g.id] = g.name;
|
||||||
|
weight[g.id] = g.group_weight || 0;
|
||||||
|
}
|
||||||
|
const totalWeight = Object.values(weight).reduce((s, w) => s + w, 0);
|
||||||
|
|
||||||
|
// Running weighted grade in due-date order, exactly like the user sees it
|
||||||
|
// accumulate over the term.
|
||||||
|
const timeline = assignments
|
||||||
|
.filter(gaIsGraded)
|
||||||
|
.sort((x, y) => new Date(x.due_at || 0) - new Date(y.due_at || 0));
|
||||||
|
const running = {};
|
||||||
|
const points = timeline.map(a => {
|
||||||
|
const r = (running[a.assignment_group_id] ||= { score: 0, pts: 0 });
|
||||||
|
r.score += a.submission.score;
|
||||||
|
r.pts += a.points_possible;
|
||||||
|
let weighted = 0, used = 0;
|
||||||
|
for (const gid of Object.keys(running)) {
|
||||||
|
const g = running[gid];
|
||||||
|
if (g.pts <= 0) continue;
|
||||||
|
if (totalWeight > 0) {
|
||||||
|
weighted += (g.score / g.pts) * (weight[gid] || 0);
|
||||||
|
used += (weight[gid] || 0);
|
||||||
|
} else {
|
||||||
|
weighted += g.score;
|
||||||
|
used += g.pts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const grade = used > 0 ? (weighted / used) * 100 : null;
|
||||||
|
const pct = (a.submission.score / a.points_possible) * 100;
|
||||||
|
return {
|
||||||
|
title: a.name,
|
||||||
|
score: a.submission.score,
|
||||||
|
points: a.points_possible,
|
||||||
|
pct,
|
||||||
|
grade,
|
||||||
|
group: groupName[a.assignment_group_id] || "",
|
||||||
|
due: a.due_at ? new Date(a.due_at).toLocaleDateString() : "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const pcts = timeline.map(a => (a.submission.score / a.points_possible) * 100);
|
||||||
|
// Trend: change in the running overall grade over the last 5 graded
|
||||||
|
// assignments (or since the first, if fewer). Positive = climbing.
|
||||||
|
let trend = null;
|
||||||
|
if (points.length >= 2) {
|
||||||
|
const from = points[Math.max(0, points.length - 1 - 5)].grade;
|
||||||
|
const to = points[points.length - 1].grade;
|
||||||
|
if (from != null && to != null) trend = to - from;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
counts,
|
||||||
|
ungraded,
|
||||||
|
graded: pcts.length,
|
||||||
|
avg: pcts.length ? pcts.reduce((s, p) => s + p, 0) / pcts.length : null,
|
||||||
|
current: points.length ? points[points.length - 1].grade : null,
|
||||||
|
trend,
|
||||||
|
points,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGradeAnalytics() {
|
||||||
|
const panel = document.getElementById("canvasrefined-grade-analytics");
|
||||||
|
if (!panel || !gaData) return;
|
||||||
|
const status = panel.querySelector("#canvasrefined-ga-status");
|
||||||
|
if (status) status.style.display = "none";
|
||||||
|
|
||||||
|
const stats = panel.querySelector("#canvasrefined-ga-stats");
|
||||||
|
stats.style.display = "flex";
|
||||||
|
const stat = (cap, val, color) =>
|
||||||
|
`<div style="padding:8px 14px;border-radius:8px;background:var(--bcbackground-1);"><div style="font-size:18px;font-weight:700;color:${color || "var(--bctext-0)"};">${val}</div><div style="font-size:11px;text-transform:uppercase;color:var(--bctext-1);">${cap}</div></div>`;
|
||||||
|
// Trend card: arrow + colored delta of the overall grade over the last 5
|
||||||
|
// graded assignments (green climbing, red falling, grey steady).
|
||||||
|
let trendVal = "-", trendColor = "var(--bctext-0)";
|
||||||
|
if (gaData.trend != null) {
|
||||||
|
if (gaData.trend > 0.05) { trendVal = "\u25B2 +" + gaData.trend.toFixed(1) + "%"; trendColor = "#16a34a"; }
|
||||||
|
else if (gaData.trend < -0.05) { trendVal = "\u25BC " + gaData.trend.toFixed(1) + "%"; trendColor = "#dc2626"; }
|
||||||
|
else { trendVal = "\u25BA " + gaData.trend.toFixed(1) + "%"; trendColor = "var(--bctext-1)"; }
|
||||||
|
}
|
||||||
|
stats.innerHTML =
|
||||||
|
stat("Overall grade", gaData.current == null ? "-" : gaData.current.toFixed(1) + "%") +
|
||||||
|
stat("Grade trend (last 5)", trendVal, trendColor) +
|
||||||
|
stat("Graded assignments", gaData.graded) +
|
||||||
|
stat("Ungraded / no score", gaData.ungraded);
|
||||||
|
|
||||||
|
const charts = panel.querySelector("#canvasrefined-ga-charts");
|
||||||
|
charts.style.display = "flex";
|
||||||
|
const fitYBox = panel.querySelector("#canvasrefined-ga-fity");
|
||||||
|
if (fitYBox) fitYBox.checked = gaFitY;
|
||||||
|
|
||||||
|
gaDrawPie(panel.querySelector("#canvasrefined-ga-pie"), panel.querySelector("#canvasrefined-ga-pie-tip"));
|
||||||
|
gaDrawLine(panel.querySelector("#canvasrefined-ga-line"), panel.querySelector("#canvasrefined-ga-line-tip"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Canvas-drawn charts --------------------------------------------------
|
||||||
|
|
||||||
|
function gaSetupCanvas(canvas) {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const w = canvas.parentNode.clientWidth, h = canvas.parentNode.clientHeight;
|
||||||
|
// Zero size means the panel isn't visible/attached yet, so skip drawing;
|
||||||
|
// the DOM observer / resize handler redraws once it has real dimensions.
|
||||||
|
if (w <= 0 || h <= 0) return null;
|
||||||
|
canvas.width = Math.max(1, Math.round(w * dpr));
|
||||||
|
canvas.height = Math.max(1, Math.round(h * dpr));
|
||||||
|
canvas.style.width = w + "px";
|
||||||
|
canvas.style.height = h + "px";
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
return { ctx, w, h };
|
||||||
|
}
|
||||||
|
|
||||||
|
function gaShowTooltip(el, x, y, html) {
|
||||||
|
el.innerHTML = html;
|
||||||
|
el.style.display = "block";
|
||||||
|
const parent = el.parentNode;
|
||||||
|
const tw = el.offsetWidth, th = el.offsetHeight;
|
||||||
|
// Place the tooltip beside the cursor/point so it never covers the chart
|
||||||
|
// underneath; flip to the left side when the right edge is tight.
|
||||||
|
let left = x + 14;
|
||||||
|
if (left + tw > parent.clientWidth) left = Math.max(0, x - tw - 14);
|
||||||
|
let top = y - th / 2;
|
||||||
|
top = Math.min(Math.max(0, top), Math.max(0, parent.clientHeight - th));
|
||||||
|
el.style.left = left + "px";
|
||||||
|
el.style.top = top + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function gaDrawPie(canvas, tooltip) {
|
||||||
|
const size = gaSetupCanvas(canvas);
|
||||||
|
if (!size || !gaData) return;
|
||||||
|
const { ctx, w, h } = size;
|
||||||
|
const cx = w / 2, cy = h / 2;
|
||||||
|
const r = Math.max(10, Math.min(w, h) / 2 - 10);
|
||||||
|
const slices = GA_BUCKETS.map((b, i) => ({ label: b.label, value: gaData.counts[i], color: b.color }))
|
||||||
|
.concat([{ label: "Ungraded", value: gaData.ungraded, color: GA_UNGRADED_COLOR }]);
|
||||||
|
const total = slices.reduce((s, x) => s + x.value, 0);
|
||||||
|
const arcs = [];
|
||||||
|
if (total === 0) {
|
||||||
|
ctx.strokeStyle = "#888";
|
||||||
|
ctx.lineWidth = r - r * 0.55;
|
||||||
|
ctx.globalAlpha = 0.3;
|
||||||
|
ctx.beginPath(); ctx.arc(cx, cy, (r + r * 0.55) / 2, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.fillStyle = "#888"; ctx.textAlign = "center";
|
||||||
|
ctx.font = "13px Lato, sans-serif";
|
||||||
|
ctx.fillText("No graded assignments yet", cx, cy);
|
||||||
|
}
|
||||||
|
let start = -Math.PI / 2;
|
||||||
|
for (const s of slices) {
|
||||||
|
if (!s.value) continue;
|
||||||
|
const ang = (s.value / total) * Math.PI * 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, r, start, start + ang);
|
||||||
|
ctx.arc(cx, cy, r * 0.55, start + ang, start, true);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = s.color;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = "rgba(255,255,255,0.9)";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
arcs.push({ ...s, start, end: start + ang });
|
||||||
|
start += ang;
|
||||||
|
}
|
||||||
|
canvas._gaArcs = arcs;
|
||||||
|
canvas._gaCenter = { cx, cy, r };
|
||||||
|
if (canvas._gaHover) { canvas.removeEventListener("mousemove", canvas._gaHover); canvas.removeEventListener("mouseleave", canvas._gaLeave); }
|
||||||
|
canvas._gaHover = (e) => {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left - canvas._gaCenter.cx;
|
||||||
|
const y = e.clientY - rect.top - canvas._gaCenter.cy;
|
||||||
|
const dist = Math.hypot(x, y);
|
||||||
|
const hit = canvas._gaArcs.find(a => {
|
||||||
|
let d = Math.atan2(y, x);
|
||||||
|
if (d < a.start) d += Math.PI * 2;
|
||||||
|
return dist <= canvas._gaCenter.r && dist >= canvas._gaCenter.r * 0.55 && d >= a.start && d <= a.end;
|
||||||
|
});
|
||||||
|
if (hit) gaShowTooltip(tooltip, e.clientX - rect.left, e.clientY - rect.top, `<b>${hit.label}</b>: ${hit.value} assignment${hit.value === 1 ? "" : "s"}`);
|
||||||
|
else tooltip.style.display = "none";
|
||||||
|
};
|
||||||
|
canvas._gaLeave = () => { tooltip.style.display = "none"; };
|
||||||
|
canvas.addEventListener("mousemove", canvas._gaHover);
|
||||||
|
canvas.addEventListener("mouseleave", canvas._gaLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gaDrawLine(canvas, tooltip) {
|
||||||
|
const size = gaSetupCanvas(canvas);
|
||||||
|
if (!size || !gaData) return;
|
||||||
|
const pts = gaData.points;
|
||||||
|
const { ctx, w, h } = size;
|
||||||
|
const pad = { l: 38, r: 12, t: 12, b: 26 };
|
||||||
|
const text = getComputedStyle(document.body).color || "#666";
|
||||||
|
ctx.font = "11px Lato, sans-serif";
|
||||||
|
// Y axis: fixed 0-100 by default, or scaled to fit the data when the user
|
||||||
|
// toggled "Fit Y axis" (persisted in chrome.storage.local).
|
||||||
|
let yMin = 0, yMax = 100;
|
||||||
|
if (gaFitY) {
|
||||||
|
const vals = pts.map(p => p.grade).filter(v => v != null);
|
||||||
|
if (vals.length) {
|
||||||
|
const lo = Math.min(...vals), hi = Math.max(...vals);
|
||||||
|
if (hi - lo < 1e-9) {
|
||||||
|
yMin = Math.max(0, lo - 5);
|
||||||
|
yMax = hi + 5;
|
||||||
|
} else {
|
||||||
|
const margin = (hi - lo) * 0.1;
|
||||||
|
yMin = Math.max(0, lo - margin);
|
||||||
|
yMax = hi + margin;
|
||||||
|
}
|
||||||
|
if (yMax - yMin < 1) yMax = yMin + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Y grid: 5 evenly spaced lines across the current range.
|
||||||
|
ctx.textAlign = "right"; ctx.textBaseline = "middle";
|
||||||
|
const decimals = (yMax - yMin) <= 10 ? 1 : 0;
|
||||||
|
for (let i = 0; i <= 5; i++) {
|
||||||
|
const v = yMin + (yMax - yMin) * (i / 5);
|
||||||
|
const y = pad.t + (1 - (v - yMin) / (yMax - yMin)) * (h - pad.t - pad.b);
|
||||||
|
ctx.strokeStyle = "rgba(128,128,128,0.25)";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(w - pad.r, y); ctx.stroke();
|
||||||
|
ctx.fillStyle = text;
|
||||||
|
ctx.fillText(v.toFixed(decimals) + "%", pad.l - 6, y);
|
||||||
|
}
|
||||||
|
if (pts.length === 0) {
|
||||||
|
ctx.fillStyle = text; ctx.textAlign = "center";
|
||||||
|
ctx.fillText("No graded assignments yet", w / 2, h / 2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const X = (i) => pad.l + (pts.length === 1 ? (w - pad.l - pad.r) / 2 : (i / (pts.length - 1)) * (w - pad.l - pad.r));
|
||||||
|
const Y = (v) => pad.t + (1 - (Math.max(yMin, Math.min(yMax, v)) - yMin) / (yMax - yMin)) * (h - pad.t - pad.b);
|
||||||
|
// Full chart draw, parameterized by the hovered point index so mousemove
|
||||||
|
// can cheaply redraw with a highlight on the active dot.
|
||||||
|
const draw = (hover) => {
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
// Y grid: 5 evenly spaced lines across the current range.
|
||||||
|
ctx.font = "11px Lato, sans-serif";
|
||||||
|
ctx.textAlign = "right"; ctx.textBaseline = "middle";
|
||||||
|
for (let i = 0; i <= 5; i++) {
|
||||||
|
const v = yMin + (yMax - yMin) * (i / 5);
|
||||||
|
const y = pad.t + (1 - (v - yMin) / (yMax - yMin)) * (h - pad.t - pad.b);
|
||||||
|
ctx.strokeStyle = "rgba(128,128,128,0.25)";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(w - pad.r, y); ctx.stroke();
|
||||||
|
ctx.fillStyle = text;
|
||||||
|
ctx.fillText(v.toFixed(decimals) + "%", pad.l - 6, y);
|
||||||
|
}
|
||||||
|
// Line + area fill.
|
||||||
|
ctx.beginPath();
|
||||||
|
pts.forEach((p, i) => { const x = X(i), y = Y(p.grade); i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); });
|
||||||
|
ctx.strokeStyle = "#2563eb"; ctx.lineWidth = 2; ctx.lineJoin = "round"; ctx.stroke();
|
||||||
|
ctx.lineTo(X(pts.length - 1), h - pad.b); ctx.lineTo(X(0), h - pad.b); ctx.closePath();
|
||||||
|
ctx.fillStyle = "rgba(37,99,235,0.12)"; ctx.fill();
|
||||||
|
// Points.
|
||||||
|
pts.forEach((p, i) => {
|
||||||
|
ctx.beginPath(); ctx.arc(X(i), Y(p.grade), 3, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "#2563eb"; ctx.fill();
|
||||||
|
ctx.strokeStyle = "#fff"; ctx.lineWidth = 1; ctx.stroke();
|
||||||
|
});
|
||||||
|
// X labels: first, middle, last due dates.
|
||||||
|
ctx.textAlign = "center"; ctx.textBaseline = "top";
|
||||||
|
[[0], [Math.floor((pts.length - 1) / 2)], [pts.length - 1]].forEach(([i]) => {
|
||||||
|
if (pts.length > 1 && pts.length > 2 && i !== 0 && i !== pts.length - 1 && Math.abs(X(i) - X(0)) < 40) return;
|
||||||
|
const label = pts[i].due || "";
|
||||||
|
if (!label) return;
|
||||||
|
ctx.fillStyle = text;
|
||||||
|
ctx.fillText(label, X(i), h - pad.b + 6);
|
||||||
|
});
|
||||||
|
// Hover indicator: dashed vertical guide plus a halo ring around the
|
||||||
|
// hovered dot so it's obvious which point the tooltip describes.
|
||||||
|
if (hover != null && pts[hover]) {
|
||||||
|
const hx = X(hover), hy = Y(pts[hover].grade);
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = "rgba(37,99,235,0.55)";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([4, 3]);
|
||||||
|
ctx.beginPath(); ctx.moveTo(hx, pad.t); ctx.lineTo(hx, h - pad.b); ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
ctx.beginPath(); ctx.arc(hx, hy, 7, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "rgba(37,99,235,0.25)"; ctx.fill();
|
||||||
|
ctx.beginPath(); ctx.arc(hx, hy, 4.5, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "#2563eb"; ctx.fill();
|
||||||
|
ctx.strokeStyle = "#fff"; ctx.lineWidth = 1.5; ctx.stroke();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
draw(null);
|
||||||
|
canvas._gaHoverIdx = null; // stale hover index from a previous draw
|
||||||
|
canvas._gaPts = pts;
|
||||||
|
canvas._gaX = X; canvas._gaY = Y; canvas._gaPad = pad;
|
||||||
|
if (canvas._gaHover) { canvas.removeEventListener("mousemove", canvas._gaHover); canvas.removeEventListener("mouseleave", canvas._gaLeave); }
|
||||||
|
canvas._gaHover = (e) => {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const mx = e.clientX - rect.left;
|
||||||
|
let best = 0, bestD = Infinity;
|
||||||
|
pts.forEach((_, i) => { const d = Math.abs(X(i) - mx); if (d < bestD) { bestD = d; best = i; } });
|
||||||
|
if (best !== canvas._gaHoverIdx) {
|
||||||
|
canvas._gaHoverIdx = best;
|
||||||
|
draw(best);
|
||||||
|
}
|
||||||
|
const p = pts[best];
|
||||||
|
gaShowTooltip(tooltip, X(best), Y(p.grade),
|
||||||
|
`<b>${p.title}</b><br>Overall: ${p.grade == null ? "-" : p.grade.toFixed(1) + "%"}<br>This: ${p.score}/${p.points} (${p.pct.toFixed(1)}%)${p.due ? `<br>Due: ${p.due}` : ""}`);
|
||||||
|
};
|
||||||
|
canvas._gaLeave = () => {
|
||||||
|
tooltip.style.display = "none";
|
||||||
|
if (canvas._gaHoverIdx != null) {
|
||||||
|
canvas._gaHoverIdx = null;
|
||||||
|
draw(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
canvas.addEventListener("mousemove", canvas._gaHover);
|
||||||
|
canvas.addEventListener("mouseleave", canvas._gaLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redraw open charts when the window is resized.
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
const panel = document.getElementById("canvasrefined-grade-analytics");
|
||||||
|
if (panel && gaOpen && gaData) renderGradeAnalytics();
|
||||||
|
});
|
||||||
|
|
||||||
function getApiData() {
|
function getApiData() {
|
||||||
if (current_page === "/" || current_page === "" || options.better_todo || options.better_sidebar) {
|
if (current_page === "/" || current_page === "" || options.better_todo || options.better_sidebar) {
|
||||||
getAssignments();
|
getAssignments();
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const syncedSwitches = ['remind', 'tab_icons', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'better_todo', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'center_cards', 'quiz_safe_mode', 'global_search'];
|
const syncedSwitches = ['remind', 'tab_icons', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'better_todo', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'hide_sequence_footer', 'center_cards', 'quiz_safe_mode', 'global_search', 'grade_analytics'];
|
||||||
const syncedSubOptions = [
|
const syncedSubOptions = [
|
||||||
"todo_hide_feedback",
|
"todo_hide_feedback",
|
||||||
"todo_full_height",
|
"todo_full_height",
|
||||||
@ -150,6 +150,7 @@ const defaultOptions = {
|
|||||||
"relative_dues": false,
|
"relative_dues": false,
|
||||||
"equal_height_cards": false,
|
"equal_height_cards": false,
|
||||||
"hide_new_canvas": true,
|
"hide_new_canvas": true,
|
||||||
|
"hide_sequence_footer": false,
|
||||||
"quiz_safe_mode": false,
|
"quiz_safe_mode": false,
|
||||||
"dark_mode_fix": [],
|
"dark_mode_fix": [],
|
||||||
"assignment_states": {},
|
"assignment_states": {},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user