Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,181 @@
|
||||
// Generated, do not edit. sha256:c1446a22c58388fe401f93d2f54194b187120f3e6ec07b997b51d0acb9bb51b4
|
||||
import * as dialog from '../ui/dialog.js';
|
||||
import { esc } from '../ui/escape.js';
|
||||
import { fmtBaseSigned, fmtPct, pctTone, colouredSigns, isBaseAsset } from '../ui/format.js';
|
||||
import { assetLabel, byKindAndLabel } from '../ui/asset-label.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { localDateNl } from '../../shared/time.js';
|
||||
import { ENUMS } from '../../shared/config.js';
|
||||
import { seriesFor } from './series.js';
|
||||
import { periodChoices, startPeriod, returnLine, idsForSelection, contributorsPerDay } from './return-view.js';
|
||||
import { getPref, setPref } from '../data/preferences.js';
|
||||
import { drawReturnChart } from './return-chart.js';
|
||||
import { carriedDays } from './carried.js';
|
||||
import { selectLabel } from '../ui/select-label.js';
|
||||
|
||||
const DASH = '-';
|
||||
const PREF_SCOPE = 'wealth.return';
|
||||
const live = new Set();
|
||||
|
||||
export function openReturnDialog({ reeks, assets = [], page = null, selection = null } = {}) {
|
||||
const closed = dialog.detail({ title: t('wealth.returnTitle'), html: '<div class="return-view return-view--dialog" data-return></div>' });
|
||||
const hosts = document.querySelectorAll('.dialog-overlay [data-return]');
|
||||
const host = hosts[hosts.length - 1];
|
||||
if (!host) return;
|
||||
const controls = host.closest('.dialog')?.querySelector('[data-dialog-aside]') || null;
|
||||
const ids = idsForSelection(selection, assets);
|
||||
const view = mount(host, { reeks, assets, page, data: dataKeyFor(selection, ids), selectionIds: ids, controls });
|
||||
closed.then(() => view.destroy());
|
||||
}
|
||||
|
||||
export async function mountReturnPanel(host, { reeks, asset }) {
|
||||
const key = String(asset.id);
|
||||
const stored = await getPref(PREF_SCOPE, key, null).catch(() => null);
|
||||
if (!host.isConnected) return null;
|
||||
return mount(host, { reeks, assets: [asset], page: null, data: `asset:${asset.id}`, stored,
|
||||
onPeriod: period => setPref(PREF_SCOPE, key, period).catch(() => { }) });
|
||||
}
|
||||
|
||||
export function refreshReturn() {
|
||||
for (const v of [...live]) v.render();
|
||||
}
|
||||
|
||||
export function lineForData({ reeks, assets = [], data = 'total', venster = null, selectionIds = null }) {
|
||||
const ids = idsOf(data, assets, selectionIds);
|
||||
const line = returnLine(seriesFor(reeks, ids && ids.length === 1 ? ids[0] : ids), venster);
|
||||
const noPct = withoutPct(ids, assets);
|
||||
const top = contributorsOf(reeks, ids ?? assets.map(a => a.id), venster, assets);
|
||||
const carried = carriedDays(reeks, ids, assets);
|
||||
return {
|
||||
amount: line.amount,
|
||||
pct: noPct ? null : line.pct,
|
||||
points: line.points.map(p => ({ ...p, pct: noPct ? null : p.pct, top: top.get(p.i) || [], carried: carried.has(p.i) })),
|
||||
};
|
||||
}
|
||||
|
||||
export function drawLine(canvas, reeks, points) {
|
||||
return drawReturnChart(canvas, points, points.map(p => localDateNl(`${reeks.dagen[p.i]}T00:00:00Z`)));
|
||||
}
|
||||
|
||||
function dataKeyFor(selection, ids) {
|
||||
if (!ids) return 'total';
|
||||
if (selection?.kind) return `kind:${selection.kind}`;
|
||||
if (ids.length === 1) return `asset:${ids[0]}`;
|
||||
return 'selection';
|
||||
}
|
||||
|
||||
function idsOf(key, assets, selectionIds) {
|
||||
if (key === 'total') return null;
|
||||
if (key === 'selection') return selectionIds;
|
||||
if (key.startsWith('kind:')) return assets.filter(a => a.kind === key.slice(5)).map(a => a.id);
|
||||
return [Number(key.slice(6))];
|
||||
}
|
||||
|
||||
const withoutPct = (ids, assets) => ids?.length === 1 && isBaseAsset(assets.find(a => a.id === ids[0]) || {});
|
||||
|
||||
function contributorsOf(reeks, ids, venster, assets) {
|
||||
const byId = new Map(assets.map(a => [a.id, a]));
|
||||
const named = new Map();
|
||||
for (const [i, list] of contributorsPerDay(reeks, ids, venster)) {
|
||||
named.set(i, list.filter(c => byId.has(c.id)).map(c => ({
|
||||
label: assetLabel(byId.get(c.id)), day: c.day, pct: isBaseAsset(byId.get(c.id)) ? null : c.pct,
|
||||
})));
|
||||
}
|
||||
return named;
|
||||
}
|
||||
|
||||
function mount(host, { reeks, assets, page, data, selectionIds = null, controls = null, stored = null, onPeriod = null }) {
|
||||
const length = reeks?.dagen?.length || 0;
|
||||
const periods = periodChoices(length, page);
|
||||
const state = { data, period: startPeriod(periods, stored), chart: null, gen: 0, lines: null };
|
||||
|
||||
const view = {
|
||||
render() {
|
||||
if (!host.isConnected) { view.destroy(); return; }
|
||||
if (!length) { host.innerHTML = `<div class="t-muted-cell">${t('wealth.noChart')}</div>`; return; }
|
||||
const lines = new Map(periods.map(p => [p.key, lineForData({ reeks, assets, data: state.data, venster: p.venster, selectionIds })]));
|
||||
state.lines = lines;
|
||||
if (controls) controls.innerHTML = dataSelect(assets, state.data, selectionIds);
|
||||
host.innerHTML = `
|
||||
<div class="return-periods" role="group">${periods.map(p => periodButton(p, lines.get(p.key), state.period)).join('')}</div>
|
||||
<div class="return-plot"><canvas data-return-chart></canvas></div>`;
|
||||
drawChart(lines.get(state.period)?.points || []);
|
||||
},
|
||||
destroy() {
|
||||
live.delete(view);
|
||||
state.chart?.destroy();
|
||||
state.chart = null;
|
||||
},
|
||||
};
|
||||
|
||||
async function drawChart(points) {
|
||||
const gen = ++state.gen;
|
||||
state.chart?.destroy();
|
||||
state.chart = null;
|
||||
const canvas = host.querySelector('[data-return-chart]');
|
||||
if (!canvas) return;
|
||||
if (points.length < 2) {
|
||||
canvas.parentElement.innerHTML = `<div class="t-muted-cell">${t('wealth.noChart')}</div>`;
|
||||
return;
|
||||
}
|
||||
const chart = await drawLine(canvas, reeks, points).catch(() => null);
|
||||
if (gen !== state.gen || !host.isConnected) { chart?.destroy(); return; }
|
||||
state.chart = chart;
|
||||
}
|
||||
|
||||
host.addEventListener('click', e => {
|
||||
const btn = e.target.closest('[data-period]');
|
||||
if (!btn || btn.disabled || btn.dataset.period === state.period) return;
|
||||
state.period = btn.dataset.period;
|
||||
onPeriod?.(state.period);
|
||||
for (const b of host.querySelectorAll('[data-period]')) b.classList.toggle('is-on', b === btn);
|
||||
if (!host.querySelector('[data-return-chart]')) {
|
||||
host.querySelector('.return-plot').innerHTML = '<canvas data-return-chart></canvas>';
|
||||
}
|
||||
drawChart(state.lines?.get(state.period)?.points || []);
|
||||
});
|
||||
controls?.addEventListener('change', e => {
|
||||
const sel = e.target.closest('[data-return-data]');
|
||||
if (!sel) return;
|
||||
state.data = sel.value;
|
||||
view.render();
|
||||
});
|
||||
|
||||
live.add(view);
|
||||
view.render();
|
||||
return view;
|
||||
}
|
||||
|
||||
function periodLabel(p) {
|
||||
if (p.key === 'page') return p.label ?? DASH;
|
||||
if (p.days == null) return t('wealth.sinceStart');
|
||||
if (p.days === 1) return t('wealth.periodDay');
|
||||
if (p.days === 365) return t('wealth.periodYear');
|
||||
return t('wealth.periodDays', { n: p.days });
|
||||
}
|
||||
|
||||
const tone = v => (v == null ? '' : v > 0.005 ? 't-pos' : v < -0.005 ? 't-neg' : '');
|
||||
|
||||
function periodButton(p, line, current) {
|
||||
const on = p.key === current ? ' is-on' : '';
|
||||
const off = p.venster ? '' : ' disabled';
|
||||
const amount = line?.amount == null ? DASH : `<span class="val">${esc(fmtBaseSigned(line.amount, colouredSigns()))}</span>`;
|
||||
const pct = line?.pct == null ? '' : fmtPct(line.pct, { fine: p.days === 1, signs: colouredSigns() });
|
||||
return `<button type="button" class="return-period${on}" data-period="${esc(p.key)}"${off}>
|
||||
<span class="return-period-label">${esc(periodLabel(p))}</span>
|
||||
<span class="return-period-amount ${tone(line?.amount)}">${amount}</span>
|
||||
<span class="return-period-pct">${pct ? `<span class="delta ${pctTone(line.pct, { fine: p.days === 1 })}">${esc(pct)}</span>` : ''}</span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
function dataSelect(assets, current, selectionIds) {
|
||||
const opt = (value, label) => `<option value="${esc(value)}"${value === current ? ' selected' : ''}>${esc(label)}</option>`;
|
||||
const kinds = ENUMS.ASSET_KIND.filter(k => assets.some(a => a.kind === k));
|
||||
const sorted = [...assets].sort(byKindAndLabel);
|
||||
return selectLabel(`<select data-return-data aria-label="${esc(t('wealth.returnData'))}">
|
||||
${opt('total', t('wealth.returnTotal'))}
|
||||
${selectionIds?.length > 1 ? opt('selection', t('wealth.returnSelection', { n: selectionIds.length })) : ''}
|
||||
<optgroup label="${esc(t('wealth.returnKinds'))}">${kinds.map(k => opt(`kind:${k}`, t('enums.asset_kind.' + k))).join('')}</optgroup>
|
||||
<optgroup label="${esc(t('wealth.returnAssets'))}">${sorted.map(a => opt(`asset:${a.id}`, assetLabel(a))).join('')}</optgroup>
|
||||
</select>`);
|
||||
}
|
||||
Reference in new issue
Block a user