Skip to content

Commit cadf517

Browse files
Aditya PutatundaAditya Putatunda
authored andcommitted
range_slider
1 parent 9734901 commit cadf517

1 file changed

Lines changed: 179 additions & 14 deletions

File tree

index.html

Lines changed: 179 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,72 @@
226226
}
227227
.checkbox input { accent-color: var(--hyb); }
228228

229+
/* PERIOD (dual-thumb) */
230+
.period-labels {
231+
display: flex;
232+
justify-content: space-between;
233+
margin-bottom: 8px;
234+
font-family: var(--mono);
235+
font-size: 12px;
236+
color: var(--ink);
237+
}
238+
.period-slider {
239+
position: relative;
240+
height: 18px;
241+
}
242+
.period-slider .track {
243+
position: absolute;
244+
top: 7px; left: 0;
245+
width: 100%; height: 4px;
246+
background: var(--line-2);
247+
border-radius: 2px;
248+
}
249+
.period-slider .track-fill {
250+
position: absolute;
251+
top: 0; height: 100%;
252+
background: var(--hyb);
253+
border-radius: 2px;
254+
}
255+
.period-slider input[type=range] {
256+
position: absolute;
257+
top: 0; left: 0;
258+
width: 100%; height: 18px;
259+
background: transparent;
260+
pointer-events: none;
261+
-webkit-appearance: none;
262+
margin: 0;
263+
}
264+
.period-slider input[type=range]::-webkit-slider-runnable-track {
265+
background: transparent; height: 18px; border: none;
266+
}
267+
.period-slider input[type=range]::-moz-range-track {
268+
background: transparent; height: 18px; border: none;
269+
}
270+
.period-slider input[type=range]::-webkit-slider-thumb {
271+
pointer-events: auto;
272+
-webkit-appearance: none;
273+
width: 14px; height: 14px;
274+
border-radius: 50%;
275+
background: var(--ink);
276+
border: 2px solid var(--surface);
277+
box-shadow: 0 0 0 1px var(--line-2);
278+
cursor: pointer;
279+
position: relative;
280+
z-index: 2;
281+
margin-top: 0;
282+
transition: transform 0.1s;
283+
}
284+
.period-slider input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.15); }
285+
.period-slider input[type=range]::-moz-range-thumb {
286+
pointer-events: auto;
287+
width: 14px; height: 14px;
288+
border-radius: 50%;
289+
background: var(--ink);
290+
border: 2px solid var(--surface);
291+
box-shadow: 0 0 0 1px var(--line-2);
292+
cursor: pointer;
293+
}
294+
229295
/* RIGHT PANE */
230296
.pane {
231297
padding: 32px 48px;
@@ -362,6 +428,22 @@ <h1>Buy the dip <em>vs</em> just keep buying.</h1>
362428
<main>
363429
<aside class="knobs">
364430

431+
<div class="knob-group">
432+
<h3>Period</h3>
433+
<div class="knob">
434+
<div class="period-labels">
435+
<span id="lbl-period-start"></span>
436+
<span id="lbl-period-end"></span>
437+
</div>
438+
<div class="period-slider">
439+
<div class="track"></div>
440+
<div class="track-fill" id="period-fill"></div>
441+
<input type="range" id="p-start" min="0" max="100" step="1" value="0">
442+
<input type="range" id="p-end" min="0" max="100" step="1" value="100">
443+
</div>
444+
</div>
445+
</div>
446+
365447
<div class="knob-group">
366448
<h3>Contribution</h3>
367449
<div class="knob">
@@ -513,6 +595,36 @@ <h2>Strategy comparison</h2>
513595
// ============================================================
514596
function ymKey(d) { return d.getFullYear() * 12 + d.getMonth(); }
515597

598+
function ymString(d) {
599+
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
600+
}
601+
602+
const MONTH_NAMES = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
603+
function fmtMonth(ym) {
604+
const [y, m] = ym.split('-').map(Number);
605+
return `${MONTH_NAMES[m-1]} ${y}`;
606+
}
607+
608+
let MONTHS = []; // sorted unique "YYYY-MM" strings present in DATA
609+
function buildMonths(data) {
610+
const set = new Set();
611+
for (const r of data) set.add(ymString(r.date));
612+
return [...set].sort();
613+
}
614+
615+
function filterByMonths(data, startYM, endYM) {
616+
const out = [];
617+
for (const r of data) {
618+
const ym = ymString(r.date);
619+
if (ym >= startYM && ym <= endYM) out.push({ ...r });
620+
}
621+
// recompute nextClose so the slice is self-contained (T+1 within slice; last → same close)
622+
for (let i = 0; i < out.length; i++) {
623+
out[i].nextClose = (i+1 < out.length) ? out[i+1].close : out[i].close;
624+
}
625+
return out;
626+
}
627+
516628
function firstTradingDays(data) {
517629
const seen = new Set();
518630
const set = new Set();
@@ -762,6 +874,8 @@ <h2>Strategy comparison</h2>
762874
deployLeftover: document.getElementById('leftover').checked,
763875
sipAmt: monthly * splitPct / 100,
764876
dipAmt: monthly * (100 - splitPct) / 100,
877+
periodStart: +document.getElementById('p-start').value,
878+
periodEnd: +document.getElementById('p-end').value,
765879
};
766880
}
767881

@@ -948,16 +1062,62 @@ <h2>Strategy comparison</h2>
9481062
}
9491063

9501064
function recompute() {
951-
if (!DATA) return;
1065+
if (!DATA || MONTHS.length === 0) return;
9521066
const p = readKnobs();
9531067
syncLabels(p);
954-
const sip = strategySip(DATA, p);
955-
const dip = strategyDip(DATA, p);
956-
const hyb = strategyHybrid(DATA, p);
957-
lastResults = { sip, dip, hyb };
958-
const periodYears = (DATA[DATA.length-1].date - DATA[0].date) / MS_YR;
1068+
const startYM = MONTHS[p.periodStart];
1069+
const endYM = MONTHS[p.periodEnd];
1070+
const data = filterByMonths(DATA, startYM, endYM);
1071+
if (data.length < 20) return;
1072+
const sip = strategySip(data, p);
1073+
const dip = strategyDip(data, p);
1074+
const hyb = strategyHybrid(data, p);
1075+
lastResults = { sip, dip, hyb, data };
1076+
const periodYears = (data[data.length-1].date - data[0].date) / MS_YR;
9591077
renderTable(sip, dip, hyb, periodYears);
960-
renderChart(DATA, sip, dip, hyb);
1078+
renderChart(data, sip, dip, hyb);
1079+
updatePeriodMeta(data);
1080+
}
1081+
1082+
function updatePeriodMeta(data) {
1083+
const start = data[0].date.toISOString().slice(0,10);
1084+
const end = data[data.length-1].date.toISOString().slice(0,10);
1085+
const yrs = ((data[data.length-1].date - data[0].date) / MS_YR).toFixed(1);
1086+
document.getElementById('period-meta').textContent =
1087+
`${start}${end} · ${yrs} yr · ${data.length.toLocaleString()} sessions`;
1088+
}
1089+
1090+
function updatePeriodFill() {
1091+
const s = +document.getElementById('p-start').value;
1092+
const e = +document.getElementById('p-end').value;
1093+
const max = +document.getElementById('p-start').max;
1094+
const fill = document.getElementById('period-fill');
1095+
fill.style.left = `${(s/max)*100}%`;
1096+
fill.style.width = `${((e-s)/max)*100}%`;
1097+
}
1098+
1099+
function syncPeriodLabels() {
1100+
const s = +document.getElementById('p-start').value;
1101+
const e = +document.getElementById('p-end').value;
1102+
document.getElementById('lbl-period-start').textContent = fmtMonth(MONTHS[s]);
1103+
document.getElementById('lbl-period-end').textContent = fmtMonth(MONTHS[e]);
1104+
}
1105+
1106+
function wirePeriod() {
1107+
const start = document.getElementById('p-start');
1108+
const end = document.getElementById('p-end');
1109+
const MIN_SPAN = 12; // months — at least 1 year of contributions
1110+
1111+
const handler = (which) => () => {
1112+
let s = +start.value, e = +end.value;
1113+
if (which === 'start' && s > e - MIN_SPAN) start.value = Math.max(0, e - MIN_SPAN);
1114+
if (which === 'end' && e < s + MIN_SPAN) end.value = Math.min(+end.max, s + MIN_SPAN);
1115+
updatePeriodFill();
1116+
syncPeriodLabels();
1117+
debouncedRecompute();
1118+
};
1119+
start.addEventListener('input', handler('start'));
1120+
end.addEventListener('input', handler('end'));
9611121
}
9621122

9631123
function wire() {
@@ -967,16 +1127,17 @@ <h2>Strategy comparison</h2>
9671127
el.addEventListener('input', debouncedRecompute);
9681128
el.addEventListener('change', debouncedRecompute);
9691129
});
1130+
wirePeriod();
9701131
document.getElementById('theme-toggle').addEventListener('click', () => {
9711132
const cur = document.documentElement.dataset.theme || 'light';
9721133
const next = cur === 'dark' ? 'light' : 'dark';
9731134
document.documentElement.dataset.theme = next;
9741135
try { localStorage.setItem('theme', next); } catch(e) {}
975-
if (DATA && lastResults.sip) renderChart(DATA, lastResults.sip, lastResults.dip, lastResults.hyb);
1136+
if (lastResults.sip) renderChart(lastResults.data, lastResults.sip, lastResults.dip, lastResults.hyb);
9761137
});
9771138
}
9781139

979-
let lastResults = { sip: null, dip: null, hyb: null };
1140+
let lastResults = { sip: null, dip: null, hyb: null, data: null };
9801141

9811142
(function initTheme() {
9821143
let saved = 'light';
@@ -988,14 +1149,18 @@ <h2>Strategy comparison</h2>
9881149
try {
9891150
const { data, source } = await loadData();
9901151
DATA = data;
991-
const start = data[0].date.toISOString().slice(0,10);
992-
const end = data[data.length-1].date.toISOString().slice(0,10);
993-
const yrs = ((data[data.length-1].date - data[0].date) / MS_YR).toFixed(1);
994-
document.getElementById('period-meta').textContent =
995-
`${start}${end} · ${yrs} yr · ${data.length.toLocaleString()} sessions`;
1152+
MONTHS = buildMonths(data);
1153+
// initialize period sliders to full range
1154+
const lastIdx = MONTHS.length - 1;
1155+
const pStart = document.getElementById('p-start');
1156+
const pEnd = document.getElementById('p-end');
1157+
pStart.max = lastIdx; pStart.value = 0;
1158+
pEnd.max = lastIdx; pEnd.value = lastIdx;
9961159
document.getElementById('status').style.display = 'none';
9971160
document.getElementById('content').style.display = 'block';
9981161
wire();
1162+
updatePeriodFill();
1163+
syncPeriodLabels();
9991164
recompute();
10001165
} catch (e) {
10011166
const s = document.getElementById('status');

0 commit comments

Comments
 (0)