diff --git a/lib/folk-choice-conviction.ts b/lib/folk-choice-conviction.ts
index fb23ac4..36d2497 100644
--- a/lib/folk-choice-conviction.ts
+++ b/lib/folk-choice-conviction.ts
@@ -174,6 +174,20 @@ const styles = css`
text-align: center;
}
+ .conviction-chart {
+ padding: 4px 8px 0;
+ border-bottom: 1px solid #e2e8f0;
+ }
+
+ .conviction-chart svg {
+ width: 100%;
+ display: block;
+ }
+
+ .conviction-chart:empty {
+ display: none;
+ }
+
.add-form {
display: flex;
gap: 6px;
@@ -344,6 +358,7 @@ export class FolkChoiceConviction extends FolkShape {
#weightEl: HTMLElement | null = null;
#votersEl: HTMLElement | null = null;
#drawerEl: HTMLElement | null = null;
+ #chartEl: HTMLElement | null = null;
get title() { return this.#title; }
set title(v: string) { this.#title = v; this.requestUpdate("title"); }
@@ -432,6 +447,7 @@ export class FolkChoiceConviction extends FolkShape {
+
@@ -458,6 +474,7 @@ export class FolkChoiceConviction extends FolkShape {
this.#weightEl = wrapper.querySelector(".weight-bar") as HTMLElement;
this.#votersEl = wrapper.querySelector(".voters-count") as HTMLElement;
this.#drawerEl = wrapper.querySelector(".results-drawer") as HTMLElement;
+ this.#chartEl = wrapper.querySelector(".conviction-chart") as HTMLElement;
const titleEl = wrapper.querySelector(".title-text") as HTMLElement;
const drawerToggle = wrapper.querySelector(".drawer-toggle") as HTMLButtonElement;
@@ -534,6 +551,7 @@ export class FolkChoiceConviction extends FolkShape {
#render() {
this.#renderOptions();
+ this.#renderChart();
if (this.#drawerOpen) this.#renderDrawer();
}
@@ -547,7 +565,14 @@ export class FolkChoiceConviction extends FolkShape {
const maxConv = Math.max(1, ...convictions.map((c) => c.score));
const uniqueParticipants = new Set(this.#stakes.map((s) => s.userId)).size;
- this.#optionsEl.innerHTML = this.#options
+ // Sort options by conviction score (highest first)
+ const sortedOptions = [...this.#options].sort((a, b) => {
+ const scoreA = convictions.find((c) => c.id === a.id)!.score;
+ const scoreB = convictions.find((c) => c.id === b.id)!.score;
+ return scoreB - scoreA;
+ });
+
+ this.#optionsEl.innerHTML = sortedOptions
.map((opt) => {
const conv = convictions.find((c) => c.id === opt.id)!;
const barWidth = (conv.score / maxConv) * 100;
@@ -602,6 +627,110 @@ export class FolkChoiceConviction extends FolkShape {
}
}
+ #renderChart() {
+ if (!this.#chartEl) return;
+ if (this.#options.length === 0 || this.#stakes.length === 0) {
+ this.#chartEl.innerHTML = "";
+ return;
+ }
+
+ const now = Date.now();
+ const W = 280;
+ const H = 100;
+ const PAD = { top: 10, right: 10, bottom: 18, left: 34 };
+ const plotW = W - PAD.left - PAD.right;
+ const plotH = H - PAD.top - PAD.bottom;
+
+ // Collect all inflection time points from stakes
+ const timeSet = new Set
();
+ for (const s of this.#stakes) timeSet.add(s.since);
+ timeSet.add(now);
+ const sortedTimes = [...timeSet].sort((a, b) => a - b);
+
+ const earliest = sortedTimes[0];
+ const timeRange = Math.max(now - earliest, 60000); // at least 1 minute
+
+ // Compute conviction curve for each option
+ const curves: { id: string; color: string; points: { t: number; v: number }[] }[] = [];
+ let maxV = 0;
+
+ for (const opt of this.#options) {
+ const optStakes = this.#stakes.filter((s) => s.optionId === opt.id);
+ if (optStakes.length === 0) continue;
+
+ const points: { t: number; v: number }[] = [];
+ const optEarliest = Math.min(...optStakes.map((s) => s.since));
+
+ // Start at zero
+ if (optEarliest > earliest) points.push({ t: earliest, v: 0 });
+ points.push({ t: optEarliest, v: 0 });
+
+ // Compute conviction at each time point after this option's first stake
+ for (const t of sortedTimes) {
+ if (t <= optEarliest) continue;
+ let score = 0;
+ for (const s of optStakes) {
+ if (s.since <= t) score += s.weight * (t - s.since) / 3600000;
+ }
+ points.push({ t, v: score });
+ maxV = Math.max(maxV, score);
+ }
+
+ curves.push({ id: opt.id, color: opt.color, points });
+ }
+
+ if (maxV === 0) maxV = 1;
+
+ const x = (t: number) => PAD.left + ((t - earliest) / timeRange) * plotW;
+ const y = (v: number) => PAD.top + (1 - v / maxV) * plotH;
+
+ let svg = `";
+ this.#chartEl.innerHTML = svg;
+ }
+
#renderDrawer() {
if (!this.#drawerEl) return;
const now = Date.now();