182 lines
8.1 KiB
JavaScript
182 lines
8.1 KiB
JavaScript
// 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>`);
|
|
}
|