mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-13 15:14:40 +00:00
fix: fix analyitics page for @SethBurkart123
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
let showSubjectTrends = $state(false);
|
||||
|
||||
let timestampInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let contentReady = $state(false);
|
||||
|
||||
const formattedTimestamp = $derived(() => {
|
||||
if (!lastUpdated) return "";
|
||||
@@ -170,6 +171,9 @@
|
||||
analyticsData = [];
|
||||
} finally {
|
||||
loading = false;
|
||||
requestAnimationFrame(() => {
|
||||
contentReady = true;
|
||||
});
|
||||
}
|
||||
|
||||
const ttl = getCacheTtlMs(24);
|
||||
@@ -235,7 +239,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
{#if loading || !contentReady}
|
||||
<div class="bsplus-analytics-loading bsplus-analytics-animate">
|
||||
<div class="bsplus-analytics-spinner" aria-label="Loading analytics"></div>
|
||||
</div>
|
||||
@@ -261,6 +265,7 @@
|
||||
</section>
|
||||
|
||||
<div class="bsplus-analytics-toolbar bsplus-analytics-animate bsplus-analytics-delay-2">
|
||||
<div class="bsplus-analytics-toolbar-grid">
|
||||
<div
|
||||
class="bsplus-analytics-field bsplus-analytics-toolbar-dropdown-field"
|
||||
data-analytics-dropdown
|
||||
@@ -364,11 +369,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bsplus-analytics-field bsplus-analytics-grade-range">
|
||||
<span class="bsplus-analytics-field-label">Grade range</span>
|
||||
<GradeRangeSlider bind:value={gradeRange} />
|
||||
</div>
|
||||
|
||||
<div class="bsplus-analytics-field bsplus-analytics-toolbar-search">
|
||||
<span class="bsplus-analytics-field-label">Search</span>
|
||||
<input
|
||||
@@ -379,14 +379,34 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="bsplus-analytics-checkbox">
|
||||
{#if hasActiveFilters()}
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-analytics-btn bsplus-analytics-btn-ghost bsplus-analytics-toolbar-clear"
|
||||
onclick={clearFilters}
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="bsplus-analytics-field bsplus-analytics-grade-range">
|
||||
<span class="bsplus-analytics-field-label">Grade range</span>
|
||||
<GradeRangeSlider bind:value={gradeRange} />
|
||||
</div>
|
||||
|
||||
<label
|
||||
class="bsplus-analytics-checkbox bsplus-analytics-toolbar-trends"
|
||||
class:bsplus-analytics-toolbar-trends-top={!hasActiveFilters()}
|
||||
>
|
||||
<input type="checkbox" bind:checked={showSubjectTrends} />
|
||||
<span>Show per-subject trends on chart</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bsplus-analytics-charts">
|
||||
{#key filteredData().length + "-" + gradeRange.join(",") + filterSearch + filterSubjects.join("|") + timeRange + String(showSubjectTrends)}
|
||||
<div class="bsplus-analytics-chart-cell">
|
||||
<div class="bsplus-analytics-animate bsplus-analytics-delay-3">
|
||||
<AnalyticsAreaChart
|
||||
data={gradedFiltered()}
|
||||
@@ -394,13 +414,16 @@
|
||||
showSubjectTrends={showSubjectTrends}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bsplus-analytics-chart-cell">
|
||||
<div class="bsplus-analytics-animate bsplus-analytics-delay-4">
|
||||
<AnalyticsBarChart data={gradedFiltered()} {timeRange} />
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<div class="bsplus-analytics-animate bsplus-analytics-delay-4" style="animation-delay: 400ms;">
|
||||
<div class="bsplus-analytics-animate bsplus-analytics-delay-5">
|
||||
<AssessmentTable data={timeScopedData()} />
|
||||
</div>
|
||||
|
||||
@@ -411,15 +434,6 @@
|
||||
({gradedFiltered().length} with grades)
|
||||
{/if}
|
||||
</span>
|
||||
{#if hasActiveFilters()}
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-analytics-btn bsplus-analytics-btn-ghost"
|
||||
onclick={clearFilters}
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
{/if}
|
||||
</footer>
|
||||
{:else}
|
||||
<div class="bsplus-analytics-empty bsplus-analytics-animate" transition:fade={{ duration: 300 }}>
|
||||
|
||||
@@ -25,10 +25,32 @@
|
||||
return config;
|
||||
},
|
||||
});
|
||||
|
||||
function observeChartResize(node: HTMLElement) {
|
||||
let frame = 0;
|
||||
const notify = () => {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = requestAnimationFrame(() => {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(notify);
|
||||
observer.observe(node);
|
||||
notify();
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
cancelAnimationFrame(frame);
|
||||
observer.disconnect();
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
use:observeChartResize
|
||||
data-chart={chartId}
|
||||
data-slot="chart"
|
||||
class="bsplus-chart-host {className}"
|
||||
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
|
||||
.bsplus-analytics-animate {
|
||||
animation: bsplus-analytics-fade-in-up 0.55s var(--bsplus-analytics-ease)
|
||||
forwards;
|
||||
opacity: 0;
|
||||
animation: bsplus-analytics-fade-in-up 0.55s var(--bsplus-analytics-ease) both;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
.bsplus-analytics-delay-4 {
|
||||
animation-delay: 320ms;
|
||||
}
|
||||
.bsplus-analytics-delay-5 {
|
||||
animation-delay: 400ms;
|
||||
}
|
||||
|
||||
/* ─── Header ─── */
|
||||
.bsplus-analytics-header {
|
||||
@@ -165,10 +168,9 @@
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
vertical-align: middle;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--bsplus-analytics-accent) 18%,
|
||||
transparent
|
||||
background: var(
|
||||
--bsplus-analytics-accent-subtle,
|
||||
color-mix(in srgb, var(--bsplus-analytics-accent) 18%, transparent)
|
||||
);
|
||||
color: var(--bsplus-analytics-accent);
|
||||
}
|
||||
@@ -324,10 +326,6 @@
|
||||
|
||||
/* ─── Filter toolbar ─── */
|
||||
.bsplus-analytics-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.15rem;
|
||||
border-radius: var(
|
||||
--bsplus-theme-card-radius,
|
||||
@@ -346,6 +344,73 @@
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.bsplus-analytics-toolbar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(9rem, 1fr) minmax(9rem, 1fr) minmax(12rem, 1.4fr) auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 0.9rem 1rem;
|
||||
align-items: end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bsplus-analytics-toolbar-clear {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
align-self: end;
|
||||
padding: 0.65rem 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bsplus-analytics-toolbar-trends {
|
||||
grid-column: 4;
|
||||
grid-row: 2;
|
||||
justify-self: end;
|
||||
align-self: center;
|
||||
margin-left: 0;
|
||||
max-width: 14rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bsplus-analytics-toolbar-trends-top {
|
||||
grid-row: 1;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.bsplus-analytics-toolbar-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.bsplus-analytics-toolbar-search {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.bsplus-analytics-toolbar-clear {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: auto;
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.bsplus-analytics-grade-range {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.bsplus-analytics-toolbar-trends {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: auto;
|
||||
justify-self: start;
|
||||
max-width: none;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.bsplus-analytics-toolbar-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.bsplus-analytics-toolbar-dropdown-field {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@@ -370,7 +435,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
margin-left: auto;
|
||||
padding: 0.35rem 0;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
@@ -389,13 +453,6 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.bsplus-analytics-checkbox {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bsplus-analytics-select,
|
||||
.bsplus-analytics-input {
|
||||
appearance: none;
|
||||
@@ -456,9 +513,10 @@
|
||||
}
|
||||
|
||||
.bsplus-analytics-grade-range {
|
||||
flex: 1;
|
||||
min-width: 12rem;
|
||||
max-width: 20rem;
|
||||
grid-column: 1 / 4;
|
||||
grid-row: 2;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.bsplus-analytics-range-value {
|
||||
@@ -471,17 +529,7 @@
|
||||
}
|
||||
|
||||
.bsplus-analytics-toolbar-search {
|
||||
margin-left: auto;
|
||||
flex: 1 1 14rem;
|
||||
max-width: 18rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.bsplus-analytics-toolbar-search {
|
||||
margin-left: 0;
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Toolbar custom dropdowns (time period, subjects) */
|
||||
@@ -601,6 +649,29 @@
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.bsplus-analytics-chart-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bsplus-analytics-chart-cell > :global(.bsplus-analytics-animate) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bsplus-analytics-chart-cell :global(.bsplus-analytics-card) {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Fade-in animation must not paint above the filter toolbar / dropdown */
|
||||
@@ -610,7 +681,7 @@
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.bsplus-analytics-charts {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
@@ -618,6 +689,7 @@
|
||||
.bsplus-analytics-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border-radius: var(
|
||||
--bsplus-theme-card-radius,
|
||||
var(--bsplus-analytics-radius)
|
||||
@@ -645,8 +717,9 @@
|
||||
.bsplus-analytics-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
align-items: flex-end;
|
||||
gap: 1rem;
|
||||
min-height: 4.75rem;
|
||||
padding: 1.15rem 1.25rem;
|
||||
border-bottom: 1px solid var(--bsplus-analytics-border);
|
||||
}
|
||||
@@ -657,9 +730,10 @@
|
||||
|
||||
.bsplus-analytics-card-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bsplus-analytics-card-control {
|
||||
@@ -705,6 +779,8 @@
|
||||
.bsplus-analytics-card-body {
|
||||
padding: 1rem 1.15rem;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
background: var(--bsplus-analytics-surface);
|
||||
}
|
||||
|
||||
@@ -734,20 +810,24 @@
|
||||
|
||||
/* ─── Layerchart / SVG (fix default black rects in dark UI) ─── */
|
||||
.bsplus-chart-host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
color: var(--bsplus-analytics-muted);
|
||||
}
|
||||
|
||||
.bsplus-analytics-root .bsplus-chart-surface {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 280px;
|
||||
min-height: 280px;
|
||||
max-height: 280px;
|
||||
}
|
||||
|
||||
.bsplus-analytics-root .bsplus-chart-surface-bar {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 320px;
|
||||
min-height: 320px;
|
||||
max-height: 320px;
|
||||
@@ -787,6 +867,9 @@
|
||||
}
|
||||
|
||||
.bsplus-analytics-root [data-slot="chart"] svg {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
background: transparent !important;
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -964,10 +1047,9 @@
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--bsplus-analytics-accent) 14%,
|
||||
transparent
|
||||
background: var(
|
||||
--bsplus-analytics-accent-subtle,
|
||||
color-mix(in srgb, var(--bsplus-analytics-accent) 14%, transparent)
|
||||
);
|
||||
color: var(--bsplus-analytics-accent);
|
||||
}
|
||||
@@ -998,9 +1080,11 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bsplus-analytics-muted);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* ─── States ─── */
|
||||
|
||||
@@ -3,6 +3,7 @@ import pluginStyles from "./styles.css?inline";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { mount, unmount } from "svelte";
|
||||
import GradeAnalyticsPage from "./GradeAnalyticsPage.svelte";
|
||||
import { buildContrastAccentPalette } from "./utils/accentColor";
|
||||
|
||||
type ThemeSettingKey =
|
||||
| "selectedColor"
|
||||
@@ -96,8 +97,17 @@ function syncThemeFromPage(target: HTMLElement) {
|
||||
}
|
||||
|
||||
const accent = resolvePageAccentColor();
|
||||
target.style.setProperty("--bsplus-analytics-accent", accent);
|
||||
target.style.setProperty("--better-main", accent);
|
||||
const surface =
|
||||
target.style.getPropertyValue("--background-primary").trim() ||
|
||||
computed.getPropertyValue("--background-primary").trim() ||
|
||||
(target.classList.contains("dark") ? "#1e293b" : "#ffffff");
|
||||
const palette = buildContrastAccentPalette(accent, surface);
|
||||
|
||||
target.style.setProperty("--bsplus-analytics-accent", palette.accent);
|
||||
target.style.setProperty("--bsplus-analytics-accent-subtle", palette.accentSubtle);
|
||||
target.style.setProperty("--better-main", palette.accent);
|
||||
target.style.setProperty("--bsplus-theme-btn-primary-bg", palette.accent);
|
||||
target.style.setProperty("--bsplus-theme-btn-primary-color", palette.onAccent);
|
||||
|
||||
target.classList.toggle(
|
||||
"dark",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import Color from "color";
|
||||
|
||||
export type ContrastAccentPalette = {
|
||||
accent: string;
|
||||
accentSubtle: string;
|
||||
onAccent: string;
|
||||
};
|
||||
|
||||
type ColorInstance = ReturnType<typeof Color>;
|
||||
|
||||
const MIN_CONTRAST_LIGHT = 4.5;
|
||||
const MIN_CONTRAST_DARK = 3;
|
||||
|
||||
function contrastRatio(foreground: ColorInstance, background: ColorInstance): number {
|
||||
const fg = foreground.luminosity();
|
||||
const bg = background.luminosity();
|
||||
const lighter = Math.max(fg, bg);
|
||||
const darker = Math.min(fg, bg);
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
function adjustLightnessForContrast(
|
||||
hue: number,
|
||||
saturation: number,
|
||||
lightness: number,
|
||||
background: ColorInstance,
|
||||
isDark: boolean,
|
||||
): ColorInstance {
|
||||
const minContrast = isDark ? MIN_CONTRAST_DARK : MIN_CONTRAST_LIGHT;
|
||||
let candidate = Color.hsl(hue, saturation, lightness);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if (contrastRatio(candidate, background) >= minContrast) {
|
||||
return candidate;
|
||||
}
|
||||
const { l } = candidate.hsl().object();
|
||||
candidate = Color.hsl(
|
||||
hue,
|
||||
saturation,
|
||||
isDark ? Math.min(l + 5, 82) : Math.max(l - 5, 18),
|
||||
);
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the user's hue/saturation but pick lightness so accent text and fills
|
||||
* stay readable against the analytics surface background.
|
||||
*/
|
||||
export function buildContrastAccentPalette(
|
||||
accentRaw: string,
|
||||
backgroundRaw: string,
|
||||
): ContrastAccentPalette {
|
||||
const accent = Color(accentRaw);
|
||||
const background = Color(backgroundRaw);
|
||||
const isDark = background.isDark();
|
||||
|
||||
const { h, s } = accent.hsl().object();
|
||||
const saturation = Math.min(Math.max(s, 42), 88);
|
||||
const baseLightness = isDark ? 64 : 40;
|
||||
|
||||
const foreground = adjustLightnessForContrast(
|
||||
h,
|
||||
saturation,
|
||||
baseLightness,
|
||||
background,
|
||||
isDark,
|
||||
);
|
||||
|
||||
const accentHex = foreground.hex();
|
||||
const subtleLightness = isDark ? 28 : 94;
|
||||
const subtle = Color.hsl(h, saturation * 0.75, subtleLightness);
|
||||
|
||||
return {
|
||||
accent: accentHex,
|
||||
accentSubtle: subtle.alpha(isDark ? 0.22 : 0.14).rgb().string(),
|
||||
onAccent: foreground.isLight() ? "#141414" : "#ffffff",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user