Skip to content

Commit c78f8b4

Browse files
Aditya PutatundaAditya Putatunda
authored andcommitted
interactive2
1 parent b4639c5 commit c78f8b4

1 file changed

Lines changed: 158 additions & 32 deletions

File tree

index.html

Lines changed: 158 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,29 @@
1515
<script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
1616

1717
<style>
18+
/* Light is default */
1819
:root {
20+
--bg: #fbfaf6;
21+
--surface: #ffffff;
22+
--surface-2: #f4f1ea;
23+
--line: rgba(0,0,0,0.07);
24+
--line-2: rgba(0,0,0,0.15);
25+
--ink: #1c1c1f;
26+
--ink-mute: #6e6a64;
27+
--ink-dim: #a8a39c;
28+
--sip: #2563eb;
29+
--dip: #ea580c;
30+
--hyb: #b45309;
31+
--gain: #15803d;
32+
--loss: #b91c1c;
33+
--serif: 'Fraunces', Georgia, serif;
34+
--sans: 'IBM Plex Sans', system-ui, sans-serif;
35+
--mono: 'IBM Plex Mono', ui-monospace, monospace;
36+
--bg-grad-1: rgba(180,83,9,0.04);
37+
--bg-grad-2: rgba(37,99,235,0.03);
38+
}
39+
40+
[data-theme="dark"] {
1941
--bg: #0d0d10;
2042
--surface: #15151a;
2143
--surface-2: #1c1c22;
@@ -29,9 +51,8 @@
2951
--hyb: #f5b961;
3052
--gain: #7dd181;
3153
--loss: #ef6b6b;
32-
--serif: 'Fraunces', Georgia, serif;
33-
--sans: 'IBM Plex Sans', system-ui, sans-serif;
34-
--mono: 'IBM Plex Mono', ui-monospace, monospace;
54+
--bg-grad-1: rgba(245,185,97,0.06);
55+
--bg-grad-2: rgba(110,168,254,0.04);
3556
}
3657

3758
* { box-sizing: border-box; }
@@ -44,15 +65,38 @@
4465
line-height: 1.5;
4566
-webkit-font-smoothing: antialiased;
4667
background-image:
47-
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(245,185,97,0.06), transparent 60%),
48-
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(110,168,254,0.04), transparent 60%);
68+
radial-gradient(ellipse 80% 50% at 50% -20%, var(--bg-grad-1), transparent 60%),
69+
radial-gradient(ellipse 60% 40% at 100% 100%, var(--bg-grad-2), transparent 60%);
70+
transition: background-color 0.2s, color 0.2s;
4971
}
5072

5173
/* HEADER */
5274
header {
5375
padding: 48px 48px 24px;
5476
border-bottom: 1px solid var(--line);
77+
position: relative;
78+
}
79+
.theme-toggle {
80+
position: absolute;
81+
top: 24px;
82+
right: 24px;
83+
width: 36px;
84+
height: 36px;
85+
border-radius: 50%;
86+
border: 1px solid var(--line-2);
87+
background: var(--surface);
88+
color: var(--ink);
89+
cursor: pointer;
90+
display: flex;
91+
align-items: center;
92+
justify-content: center;
93+
transition: background 0.15s, transform 0.15s;
5594
}
95+
.theme-toggle:hover { background: var(--surface-2); transform: scale(1.05); }
96+
.theme-toggle svg { width: 16px; height: 16px; }
97+
[data-theme="dark"] .icon-moon { display: none; }
98+
:root:not([data-theme="dark"]) .icon-sun { display: none; }
99+
56100
.eyebrow {
57101
font-family: var(--mono);
58102
font-size: 11px;
@@ -68,6 +112,8 @@
68112
line-height: 1.05;
69113
letter-spacing: -0.02em;
70114
margin: 0 0 8px;
115+
font-variation-settings: "opsz" 48;
116+
font-optical-sizing: none;
71117
}
72118
h1 em {
73119
font-style: italic;
@@ -135,7 +181,7 @@
135181
-webkit-appearance: none;
136182
width: 100%;
137183
height: 4px;
138-
background: var(--surface-2);
184+
background: var(--line-2);
139185
border-radius: 2px;
140186
outline: none;
141187
}
@@ -256,13 +302,22 @@
256302
font-family: var(--sans);
257303
font-size: 12px;
258304
}
259-
table.cmp tr:hover td { background: rgba(255,255,255,0.02); }
305+
table.cmp tr:hover td { background: rgba(127,127,127,0.04); }
260306
table.cmp .pos { color: var(--gain); }
261307
table.cmp .neg { color: var(--loss); }
262308
table.cmp .big {
263309
font-size: 15px;
264310
color: var(--ink);
265311
}
312+
table.cmp tr.delta-row td {
313+
border-top: 1px solid var(--line-2);
314+
color: var(--ink-mute);
315+
font-size: 12px;
316+
}
317+
table.cmp tr.delta-row td:first-child {
318+
color: var(--ink-dim);
319+
font-style: italic;
320+
}
266321

267322
/* CHART */
268323
.chart-wrap {
@@ -298,6 +353,10 @@
298353
<body>
299354

300355
<header>
356+
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme" title="Toggle theme">
357+
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
358+
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
359+
</button>
301360
<div class="eyebrow">Backtest · 2000 → 2026</div>
302361
<h1>Buy the dip <em>vs</em> just keep buying.</h1>
303362
<p class="sub">
@@ -737,15 +796,39 @@ <h2>Strategy comparison</h2>
737796
const v = (Math.pow(r.finalValue / r.invested, 1 / periodYears) - 1) * 100;
738797
return fmtPct(v);
739798
};
740-
const fmtXirr = (r) => {
799+
const xirrPct = (r) => {
741800
const x = xirr(r.cashflows);
742-
return x == null ? '—' : fmtPct(x * 100);
801+
return x == null ? null : x * 100;
802+
};
803+
const fmtXirr = (r) => {
804+
const v = xirrPct(r);
805+
return v == null ? '—' : fmtPct(v);
743806
};
744807
const fmtGain = (r) => {
745808
const g = r.finalValue - r.invested;
746809
const pct = (r.finalValue / r.invested - 1) * 100;
747810
const cls = g >= 0 ? 'pos' : 'neg';
748-
return `<span class="${cls}">${indianFmt(g)} <small>(${pct.toFixed(0)}%)</small></span>`;
811+
return `<span class="${cls}">${indianFmt(g)} <small>(${pct.toFixed(1)}%)</small></span>`;
812+
};
813+
814+
// Δ vs SIP
815+
const sipFinal = sip.finalValue;
816+
const sipXirr = xirrPct(sip);
817+
const fmtDeltaFinal = (r) => {
818+
if (r === sip) return '<span style="color:var(--ink-dim)">—</span>';
819+
const d = r.finalValue - sipFinal;
820+
const cls = d >= 0 ? 'pos' : 'neg';
821+
const sign = d >= 0 ? '+' : '';
822+
return `<span class="${cls}">${sign}${indianFmt(d)}</span>`;
823+
};
824+
const fmtDeltaXirr = (r) => {
825+
if (r === sip) return '<span style="color:var(--ink-dim)">—</span>';
826+
const v = xirrPct(r);
827+
if (v == null || sipXirr == null) return '—';
828+
const d = v - sipXirr;
829+
const cls = d >= 0 ? 'pos' : 'neg';
830+
const sign = d >= 0 ? '+' : '';
831+
return `<span class="${cls}">${sign}${d.toFixed(2)}%</span>`;
749832
};
750833

751834
const rows = [
@@ -755,33 +838,62 @@ <h2>Strategy comparison</h2>
755838
['Absolute gain', [sip, dip, hyb].map(fmtGain)],
756839
['CAGR', [sip, dip, hyb].map(fmtCagr)],
757840
['XIRR', [sip, dip, hyb].map(fmtXirr)],
841+
['Interest on idle cash', [sip, dip, hyb].map(r => r.interestEarned > 0 ? indianFmt(r.interestEarned) : '—')],
758842
];
759843

760-
const idleRow = [
761-
[sip, dip, hyb].map(r => r.interestEarned > 0 ? indianFmt(r.interestEarned) : '—'),
844+
const deltaRows = [
845+
['Δ Final vs SIP', [sip, dip, hyb].map(fmtDeltaFinal)],
846+
['Δ XIRR vs SIP', [sip, dip, hyb].map(fmtDeltaXirr)],
762847
];
763-
rows.push(['Interest on idle cash', idleRow[0]]);
764848

765-
document.getElementById('cmp-body').innerHTML = rows.map(([label, vals]) => `
766-
<tr>
849+
const renderRow = (label, vals, cls = '') => `
850+
<tr${cls ? ` class="${cls}"` : ''}>
767851
<td>${label}</td>
768852
<td>${vals[0]}</td>
769853
<td>${vals[1]}</td>
770854
<td>${vals[2]}</td>
771855
</tr>
772-
`).join('');
856+
`;
857+
858+
document.getElementById('cmp-body').innerHTML =
859+
rows.map(([l, v]) => renderRow(l, v)).join('') +
860+
deltaRows.map(([l, v]) => renderRow(l, v, 'delta-row')).join('');
861+
}
862+
863+
function chartTheme() {
864+
const dark = document.documentElement.dataset.theme === 'dark';
865+
return dark ? {
866+
line: 'rgba(236,232,223,0.85)',
867+
sip: 'rgba(110,168,254,0.85)',
868+
dip: 'rgba(255,122,89,0.9)',
869+
hyb: 'rgba(245,185,97,0.95)',
870+
fg: '#ece8df',
871+
grid: 'rgba(255,255,255,0.04)',
872+
axis: 'rgba(255,255,255,0.15)',
873+
muted:'#8b857a',
874+
hoverBg: '#1c1c22',
875+
} : {
876+
line: 'rgba(28,28,31,0.78)',
877+
sip: '#2563eb',
878+
dip: '#ea580c',
879+
hyb: '#b45309',
880+
fg: '#1c1c1f',
881+
grid: 'rgba(0,0,0,0.06)',
882+
axis: 'rgba(0,0,0,0.18)',
883+
muted:'#6e6a64',
884+
hoverBg: '#ffffff',
885+
};
773886
}
774887

775888
function renderChart(data, sip, dip, hyb) {
889+
const t = chartTheme();
776890
const xLine = data.map(r => r.date);
777891
const yLine = data.map(r => r.close);
778892

779893
const buyY = (dates) => dates.map(d => {
780-
// find closest data point — dates are already actual data point dates
781-
const t = d.getTime();
782-
// simple linear search; data is small enough
894+
const ts = d.getTime();
783895
for (let i = 0; i < data.length; i++) {
784-
if (data[i].date.getTime() === t) return data[i].close;
896+
if (data[i].date.getTime() === ts) return data[i].close;
785897
}
786898
return null;
787899
});
@@ -790,25 +902,25 @@ <h2>Strategy comparison</h2>
790902
{
791903
x: xLine, y: yLine, type: 'scatter', mode: 'lines',
792904
name: 'Nifty 50 TRI',
793-
line: { color: 'rgba(236,232,223,0.85)', width: 1.2 },
905+
line: { color: t.line, width: 1.2 },
794906
hovertemplate: '<b>%{x|%b %Y}</b><br>%{y:,.0f}<extra></extra>',
795907
},
796908
{
797909
x: sip.buyDates, y: buyY(sip.buyDates), type: 'scatter', mode: 'markers',
798910
name: `SIP buys (${sip.buyDates.length})`,
799-
marker: { color: 'rgba(110,168,254,0.85)', size: 5, symbol: 'circle' },
911+
marker: { color: t.sip, size: 5, symbol: 'circle' },
800912
hovertemplate: 'SIP · %{x|%d %b %Y}<extra></extra>',
801913
},
802914
{
803915
x: dip.buyDates, y: buyY(dip.buyDates), type: 'scatter', mode: 'markers',
804916
name: `Dip buys (${dip.buyDates.length})`,
805-
marker: { color: 'rgba(255,122,89,0.9)', size: 6, symbol: 'diamond' },
917+
marker: { color: t.dip, size: 6, symbol: 'diamond' },
806918
hovertemplate: 'Dip · %{x|%d %b %Y}<extra></extra>',
807919
},
808920
{
809921
x: hyb.dipDates, y: buyY(hyb.dipDates), type: 'scatter', mode: 'markers',
810922
name: `Hybrid dip buys (${hyb.dipBuys})`,
811-
marker: { color: 'rgba(245,185,97,0.95)', size: 7, symbol: 'triangle-up' },
923+
marker: { color: t.hyb, size: 7, symbol: 'triangle-up' },
812924
hovertemplate: 'Hybrid-dip · %{x|%d %b %Y}<extra></extra>',
813925
visible: 'legendonly',
814926
},
@@ -817,25 +929,23 @@ <h2>Strategy comparison</h2>
817929
const layout = {
818930
paper_bgcolor: 'rgba(0,0,0,0)',
819931
plot_bgcolor: 'rgba(0,0,0,0)',
820-
font: { color: '#ece8df', family: 'IBM Plex Mono, monospace', size: 11 },
932+
font: { color: t.fg, family: 'IBM Plex Mono, monospace', size: 11 },
821933
margin: { l: 60, r: 20, t: 20, b: 50 },
822934
xaxis: {
823-
gridcolor: 'rgba(255,255,255,0.04)', linecolor: 'rgba(255,255,255,0.15)',
824-
tickcolor: 'rgba(255,255,255,0.15)', zeroline: false,
935+
gridcolor: t.grid, linecolor: t.axis, tickcolor: t.axis, zeroline: false,
825936
},
826937
yaxis: {
827938
type: 'log',
828-
gridcolor: 'rgba(255,255,255,0.04)', linecolor: 'rgba(255,255,255,0.15)',
829-
tickcolor: 'rgba(255,255,255,0.15)', zeroline: false,
830-
title: { text: 'Nifty 50 TRI (log)', font: { size: 11, color: '#8b857a' } },
939+
gridcolor: t.grid, linecolor: t.axis, tickcolor: t.axis, zeroline: false,
940+
title: { text: 'Nifty 50 TRI (log)', font: { size: 11, color: t.muted } },
831941
},
832942
legend: {
833943
orientation: 'h', y: -0.18, x: 0,
834944
bgcolor: 'rgba(0,0,0,0)', font: { size: 11 },
835945
},
836946
hoverlabel: {
837-
bgcolor: '#1c1c22', bordercolor: 'rgba(255,255,255,0.15)',
838-
font: { family: 'IBM Plex Mono, monospace', color: '#ece8df' },
947+
bgcolor: t.hoverBg, bordercolor: t.axis,
948+
font: { family: 'IBM Plex Mono, monospace', color: t.fg },
839949
},
840950
};
841951

@@ -858,6 +968,7 @@ <h2>Strategy comparison</h2>
858968
const sip = strategySip(DATA, p);
859969
const dip = strategyDip(DATA, p);
860970
const hyb = strategyHybrid(DATA, p);
971+
lastResults = { sip, dip, hyb };
861972
const periodYears = (DATA[DATA.length-1].date - DATA[0].date) / MS_YR;
862973
renderTable(sip, dip, hyb, periodYears);
863974
renderChart(DATA, sip, dip, hyb);
@@ -870,8 +981,23 @@ <h2>Strategy comparison</h2>
870981
el.addEventListener('input', debouncedRecompute);
871982
el.addEventListener('change', debouncedRecompute);
872983
});
984+
document.getElementById('theme-toggle').addEventListener('click', () => {
985+
const cur = document.documentElement.dataset.theme || 'light';
986+
const next = cur === 'dark' ? 'light' : 'dark';
987+
document.documentElement.dataset.theme = next;
988+
try { localStorage.setItem('theme', next); } catch(e) {}
989+
if (DATA && lastResults.sip) renderChart(DATA, lastResults.sip, lastResults.dip, lastResults.hyb);
990+
});
873991
}
874992

993+
let lastResults = { sip: null, dip: null, hyb: null };
994+
995+
(function initTheme() {
996+
let saved = 'light';
997+
try { saved = localStorage.getItem('theme') || 'light'; } catch(e) {}
998+
document.documentElement.dataset.theme = saved;
999+
})();
1000+
8751001
(async () => {
8761002
try {
8771003
const { data, source } = await loadData();

0 commit comments

Comments
 (0)