ysdsad
This commit is contained in:
@@ -674,9 +674,20 @@
|
||||
}
|
||||
|
||||
function SimpleChart(props) {
|
||||
const [hoveredPoint, setHoveredPoint] = useState(null);
|
||||
const touchLike = typeof window !== 'undefined'
|
||||
&& typeof window.matchMedia === 'function'
|
||||
&& window.matchMedia('(hover: none), (pointer: coarse)').matches;
|
||||
|
||||
function pointTooltip(seriesLabel, point) {
|
||||
const label = seriesLabel ? `${seriesLabel} · ` : '';
|
||||
return `${label}${String(point.x)}: ${fmtNumber(point.y, 6)}`;
|
||||
return `${label}${String(point.x)}: ${point.formatted || fmtNumber(point.y, 6)}`;
|
||||
}
|
||||
|
||||
function axisLabel(axis) {
|
||||
return axis === 'right'
|
||||
? (props.rightAxisLabel || 'Rechts')
|
||||
: (props.leftAxisLabel || props.yLabel || 'Wert');
|
||||
}
|
||||
|
||||
const series = Array.isArray(props.series)
|
||||
@@ -685,18 +696,36 @@
|
||||
key: item && item.key ? String(item.key) : `series-${index}`,
|
||||
label: item && item.label ? String(item.label) : `Serie ${index + 1}`,
|
||||
color: item && item.color ? String(item.color) : ['#60a5fa', '#2dd4bf', '#f59e0b', '#f472b6'][index % 4],
|
||||
axis: item && item.axis === 'right' ? 'right' : 'left',
|
||||
formatter: item && typeof item.formatter === 'function' ? item.formatter : null,
|
||||
data: Array.isArray(item && item.data)
|
||||
? item.data.filter((point) => point && point.y !== null && point.y !== undefined)
|
||||
? item.data
|
||||
.filter((point) => point && point.y !== null && point.y !== undefined)
|
||||
.map((point) => ({
|
||||
...point,
|
||||
formatted: item && typeof item.formatter === 'function'
|
||||
? item.formatter(Number(point.y), point)
|
||||
: (point.formatted || fmtNumber(point.y, 6)),
|
||||
}))
|
||||
: [],
|
||||
}))
|
||||
.filter((item) => item.data.length > 0)
|
||||
: [];
|
||||
const points = Array.isArray(props.data) ? props.data.filter((point) => point && point.y !== null && point.y !== undefined) : [];
|
||||
const points = Array.isArray(props.data)
|
||||
? props.data
|
||||
.filter((point) => point && point.y !== null && point.y !== undefined)
|
||||
.map((point) => ({
|
||||
...point,
|
||||
formatted: point.formatted || fmtNumber(point.y, 6),
|
||||
}))
|
||||
: [];
|
||||
const isMultiSeries = series.length > 0;
|
||||
const activeSeries = isMultiSeries ? series : [{
|
||||
key: 'default',
|
||||
label: props.yLabel || 'Wert',
|
||||
color: props.type === 'area' ? '#2dd4bf' : '#60a5fa',
|
||||
axis: 'left',
|
||||
formatter: null,
|
||||
data: points,
|
||||
}];
|
||||
const allPoints = activeSeries.flatMap((item) => item.data);
|
||||
@@ -725,15 +754,32 @@
|
||||
const width = 640;
|
||||
const height = 220;
|
||||
const padding = 24;
|
||||
const values = allPoints.map((point) => Number(point.y));
|
||||
const minY = Math.min.apply(null, values);
|
||||
const maxY = Math.max.apply(null, values);
|
||||
const range = maxY - minY || 1;
|
||||
const axisStats = activeSeries.reduce((stats, item) => {
|
||||
const values = item.data.map((point) => Number(point.y)).filter((value) => Number.isFinite(value));
|
||||
if (!values.length) {
|
||||
return stats;
|
||||
}
|
||||
|
||||
const axis = item.axis === 'right' ? 'right' : 'left';
|
||||
const min = Math.min.apply(null, values);
|
||||
const max = Math.max.apply(null, values);
|
||||
const existing = stats[axis];
|
||||
stats[axis] = existing
|
||||
? {
|
||||
min: Math.min(existing.min, min),
|
||||
max: Math.max(existing.max, max),
|
||||
}
|
||||
: { min, max };
|
||||
return stats;
|
||||
}, { left: null, right: null });
|
||||
const seriesCoords = activeSeries.map((item) => {
|
||||
const axis = item.axis === 'right' ? 'right' : 'left';
|
||||
const axisMeta = axisStats[axis] || { min: 0, max: 1 };
|
||||
const range = axisMeta.max - axisMeta.min || 1;
|
||||
const stepX = item.data.length > 1 ? (width - padding * 2) / (item.data.length - 1) : 0;
|
||||
const coords = item.data.map((point, index) => {
|
||||
const x = padding + stepX * index;
|
||||
const y = height - padding - ((Number(point.y) - minY) / range) * (height - padding * 2);
|
||||
const y = height - padding - ((Number(point.y) - axisMeta.min) / range) * (height - padding * 2);
|
||||
return [x, y];
|
||||
});
|
||||
|
||||
@@ -747,18 +793,32 @@
|
||||
: '',
|
||||
};
|
||||
});
|
||||
const leftAxis = axisStats.left || axisStats.right || { min: 0, max: 1 };
|
||||
const rightAxis = axisStats.right;
|
||||
|
||||
return h('div', { className: 'mc-chart space-y-3' }, [
|
||||
return h('div', { className: 'mc-chart space-y-3', style: { position: 'relative' } }, [
|
||||
h('div', { key: 'meta', className: 'mc-flex-split mc-kicker' }, [
|
||||
h('span', { key: 'min' }, 'Min ' + fmtNumber(minY, 4)),
|
||||
h('span', { key: 'max' }, 'Max ' + fmtNumber(maxY, 4)),
|
||||
h('span', { key: 'left' }, `${axisLabel('left')} · Min ${fmtNumber(leftAxis.min, 4)} · Max ${fmtNumber(leftAxis.max, 4)}`),
|
||||
rightAxis
|
||||
? h('span', { key: 'right' }, `${axisLabel('right')} · Min ${fmtNumber(rightAxis.min, 4)} · Max ${fmtNumber(rightAxis.max, 4)}`)
|
||||
: null,
|
||||
]),
|
||||
hoveredPoint
|
||||
? h('div', { key: 'hover', className: 'mc-mini-card' }, [
|
||||
h('div', { key: 'label', className: 'mc-kicker', style: { color: hoveredPoint.color } }, hoveredPoint.label),
|
||||
h('div', { key: 'value' }, `${hoveredPoint.x}: ${hoveredPoint.value}`),
|
||||
])
|
||||
: null,
|
||||
h('svg', { key: 'svg', viewBox: `0 0 ${width} ${height}`, className: 'overflow-visible' }, [
|
||||
h('g', { key: 'grid', stroke: 'rgba(255,255,255,0.08)' }, [
|
||||
h('line', { key: 'top', x1: padding, x2: width - padding, y1: padding, y2: padding }),
|
||||
h('line', { key: 'mid', x1: padding, x2: width - padding, y1: height / 2, y2: height / 2 }),
|
||||
h('line', { key: 'base', x1: padding, x2: width - padding, y1: height - padding, y2: height - padding }),
|
||||
]),
|
||||
h('g', { key: 'axis-labels', fill: 'rgba(148,163,184,0.95)', fontSize: 10 }, [
|
||||
h('text', { key: 'left-top', x: padding, y: 14, textAnchor: 'start' }, axisLabel('left')),
|
||||
rightAxis ? h('text', { key: 'right-top', x: width - padding, y: 14, textAnchor: 'end' }, axisLabel('right')) : null,
|
||||
]),
|
||||
props.type === 'bar' && !isMultiSeries
|
||||
? h('g', { key: 'bars' }, seriesCoords[0].coords.map((coord, index) => {
|
||||
const barWidth = Math.max(10, (((width - padding * 2) / Math.max(seriesCoords[0].coords.length - 1, 1)) * 0.6) || 24);
|
||||
@@ -770,6 +830,19 @@
|
||||
height: height - padding - coord[1],
|
||||
rx: 8,
|
||||
fill: 'rgba(59, 130, 246, 0.75)',
|
||||
onMouseEnter: () => setHoveredPoint({
|
||||
label: seriesCoords[0].label,
|
||||
x: String(seriesCoords[0].data[index].x),
|
||||
value: seriesCoords[0].data[index].formatted,
|
||||
color: seriesCoords[0].color,
|
||||
}),
|
||||
onMouseLeave: () => !touchLike && setHoveredPoint(null),
|
||||
onClick: () => setHoveredPoint({
|
||||
label: seriesCoords[0].label,
|
||||
x: String(seriesCoords[0].data[index].x),
|
||||
value: seriesCoords[0].data[index].formatted,
|
||||
color: seriesCoords[0].color,
|
||||
}),
|
||||
}, [
|
||||
h('title', { key: 'title' }, pointTooltip(seriesCoords[0].label, seriesCoords[0].data[index])),
|
||||
]);
|
||||
@@ -796,10 +869,24 @@
|
||||
key: `${item.key}-${pointIndex}`,
|
||||
cx: coord[0],
|
||||
cy: coord[1],
|
||||
r: 4,
|
||||
r: 5,
|
||||
fill: '#f8fafc',
|
||||
stroke: item.color,
|
||||
strokeWidth: 2,
|
||||
style: { cursor: 'pointer' },
|
||||
onMouseEnter: () => setHoveredPoint({
|
||||
label: item.label,
|
||||
x: String(item.data[pointIndex].x),
|
||||
value: item.data[pointIndex].formatted,
|
||||
color: item.color,
|
||||
}),
|
||||
onMouseLeave: () => !touchLike && setHoveredPoint(null),
|
||||
onClick: () => setHoveredPoint({
|
||||
label: item.label,
|
||||
x: String(item.data[pointIndex].x),
|
||||
value: item.data[pointIndex].formatted,
|
||||
color: item.color,
|
||||
}),
|
||||
}, [
|
||||
h('title', { key: 'title' }, pointTooltip(item.label, item.data[pointIndex])),
|
||||
]))));
|
||||
@@ -1040,6 +1127,7 @@
|
||||
const currentWalletSnapshots = Array.isArray(payload?.wallet_snapshots) ? payload.wallet_snapshots : [];
|
||||
const currentPurchasedMiners = Array.isArray(currentSettings.purchased_miners) ? currentSettings.purchased_miners : [];
|
||||
const currentMiningCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase();
|
||||
const overviewPerDayLabel = `${currentMiningCurrency} pro Tag`;
|
||||
const latestMeasurementDate = latest && latest.measured_at ? parseStoredUtcDate(latest.measured_at) : null;
|
||||
const latestMeasurementAgeMs = latestMeasurementDate ? (Date.now() - latestMeasurementDate.getTime()) : null;
|
||||
const canTransferAll = !!latest
|
||||
@@ -1789,32 +1877,87 @@
|
||||
const overviewWindowDays = Number(payload?.bootstrap_meta?.overview_window_days || 15);
|
||||
const overviewRows = recentMeasurementWindow(measurements, overviewWindowDays);
|
||||
const performanceByDay = new Map();
|
||||
const pricingByDay = new Map();
|
||||
|
||||
overviewRows.forEach((row) => {
|
||||
const rate = Number(row.doge_per_day_interval);
|
||||
const hashrate = Number(row.active_hashrate_mh);
|
||||
const dayKey = String(row.measured_date || '').trim() || String(row.measured_at || '').slice(0, 10);
|
||||
if (!dayKey || !Number.isFinite(rate) || rate < 0) {
|
||||
if (dayKey) {
|
||||
if (Number.isFinite(rate) && rate >= 0) {
|
||||
const existing = performanceByDay.get(dayKey) || { rateSum: 0, rateCount: 0, hashrateSum: 0, hashrateCount: 0, label: fmtDate(dayKey) };
|
||||
existing.rateSum += rate;
|
||||
existing.rateCount += 1;
|
||||
if (Number.isFinite(hashrate) && hashrate >= 0) {
|
||||
existing.hashrateSum += hashrate;
|
||||
existing.hashrateCount += 1;
|
||||
}
|
||||
performanceByDay.set(dayKey, existing);
|
||||
} else if (Number.isFinite(hashrate) && hashrate >= 0) {
|
||||
const existing = performanceByDay.get(dayKey) || { rateSum: 0, rateCount: 0, hashrateSum: 0, hashrateCount: 0, label: fmtDate(dayKey) };
|
||||
existing.hashrateSum += hashrate;
|
||||
existing.hashrateCount += 1;
|
||||
performanceByDay.set(dayKey, existing);
|
||||
}
|
||||
}
|
||||
|
||||
const effectivePrice = Number(row.effective_price_per_coin);
|
||||
const convertedPrice = Number.isFinite(effectivePrice)
|
||||
? convertMeasurementMoney(row, effectivePrice, reportCurrency)
|
||||
: null;
|
||||
if (!dayKey || !Number.isFinite(Number(convertedPrice))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = performanceByDay.get(dayKey) || { sum: 0, count: 0, label: fmtDate(dayKey) };
|
||||
existing.sum += rate;
|
||||
existing.count += 1;
|
||||
performanceByDay.set(dayKey, existing);
|
||||
const priceEntry = pricingByDay.get(dayKey) || { sum: 0, count: 0, label: fmtDate(dayKey) };
|
||||
priceEntry.sum += Number(convertedPrice);
|
||||
priceEntry.count += 1;
|
||||
pricingByDay.set(dayKey, priceEntry);
|
||||
});
|
||||
|
||||
return {
|
||||
performance: Array.from(performanceByDay.entries())
|
||||
performance: {
|
||||
series: [
|
||||
{
|
||||
key: 'doge-per-day',
|
||||
label: overviewPerDayLabel,
|
||||
color: '#2dd4bf',
|
||||
axis: 'left',
|
||||
formatter: (value) => `${fmtNumber(value, 4)} ${currentMiningCurrency}`,
|
||||
data: Array.from(performanceByDay.entries())
|
||||
.sort((left, right) => String(left[0]).localeCompare(String(right[0])))
|
||||
.map(([, entry]) => ({
|
||||
x: entry.label,
|
||||
y: entry.rateCount > 0 ? (entry.rateSum / entry.rateCount) : null,
|
||||
}))
|
||||
.filter((point) => Number.isFinite(Number(point.y))),
|
||||
},
|
||||
{
|
||||
key: 'hashrate',
|
||||
label: 'Aktive Hashrate',
|
||||
color: '#60a5fa',
|
||||
axis: 'right',
|
||||
formatter: (value) => `${fmtNumber(value, 4)} MH/s`,
|
||||
data: Array.from(performanceByDay.entries())
|
||||
.sort((left, right) => String(left[0]).localeCompare(String(right[0])))
|
||||
.map(([, entry]) => ({
|
||||
x: entry.label,
|
||||
y: entry.hashrateCount > 0 ? (entry.hashrateSum / entry.hashrateCount) : null,
|
||||
}))
|
||||
.filter((point) => Number.isFinite(Number(point.y))),
|
||||
},
|
||||
],
|
||||
},
|
||||
pricing: Array.from(pricingByDay.entries())
|
||||
.sort((left, right) => String(left[0]).localeCompare(String(right[0])))
|
||||
.map(([, entry]) => ({
|
||||
x: entry.label,
|
||||
y: entry.count > 0 ? (entry.sum / entry.count) : null,
|
||||
formatted: entry.count > 0 ? fmtMoney(entry.sum / entry.count, reportCurrency) : null,
|
||||
}))
|
||||
.filter((point) => Number.isFinite(Number(point.y))),
|
||||
pricing: overviewRows.filter((row) => row.price_per_coin !== null)
|
||||
.map((row) => ({ x: fmtDate(row.measured_at), y: row.price_per_coin })),
|
||||
};
|
||||
}, [currentSettings.crypto_currency, measurements, payload?.bootstrap_meta?.overview_window_days]);
|
||||
}, [currentMiningCurrency, measurements, overviewPerDayLabel, payload?.bootstrap_meta?.overview_window_days, reportCurrency]);
|
||||
|
||||
async function saveMeasurement(raw, successMessage) {
|
||||
setSaving(true);
|
||||
@@ -2866,14 +3009,6 @@
|
||||
value: latest ? fmtNumber(latest.coins_total_visible ?? latest.coins_total, 6) : 'n/a',
|
||||
sub: latest ? `Stand ${fmtDate(latest.measured_at)}` : '',
|
||||
}),
|
||||
h(StatCard, {
|
||||
key: 'holdings',
|
||||
label: `Theoretischer Bestand ${currentCoinCurrency}`,
|
||||
value: payload?.summary?.payouts ? fmtNumber(holdingsCurrentAsset, 6) : 'n/a',
|
||||
sub: payload?.summary?.payouts
|
||||
? `Wallet-Gegenwert ${fmtNumber(walletBalanceCurrentAsset, 6)} ${currentCoinCurrency}${currentWalletPrimaryCurrency !== currentCoinCurrency ? ` · direkt ${fmtNumber(walletBalanceCurrentAssetDirect, 6)} ${currentCoinCurrency}` : ''} · Miner ${fmtNumber(latest?.coins_total_visible ?? latest?.coins_total, 6)} ${currentCoinCurrency}`
|
||||
: '',
|
||||
}),
|
||||
h(StatCard, {
|
||||
key: 'earned-overall',
|
||||
label: `Bisher verdient gesamt ${currentCoinCurrency}`,
|
||||
@@ -2912,17 +3047,24 @@
|
||||
h(StatCard, {
|
||||
key: 'value',
|
||||
label: 'Aktueller Gegenwert',
|
||||
value: latestValue !== null ? fmtMoney(latestValue, reportCurrency) : 'n/a',
|
||||
sub: latestPrice !== null
|
||||
? `Kurs ${fmtNumber(latestPrice, 6)} ${reportCurrency}${latest && latest.price_is_fallback ? ' · Fallback aus letztem Kurs' : ''}`
|
||||
: 'Kein umrechenbarer Kurs am letzten Punkt',
|
||||
value: totalHoldingsValue !== null ? fmtMoney(totalHoldingsValue, reportCurrency) : 'n/a',
|
||||
sub: [
|
||||
holdingsCurrentAsset !== null && holdingsCurrentAsset !== undefined
|
||||
? `Gesamtbestand ${fmtNumber(holdingsCurrentAsset, 6)} ${currentCoinCurrency}`
|
||||
: null,
|
||||
latestValue !== null ? `Miner ${fmtMoney(latestValue, reportCurrency)}` : null,
|
||||
walletValue !== null ? `Wallet ${fmtMoney(walletValue, reportCurrency)}` : null,
|
||||
latestPrice !== null
|
||||
? `Kurs ${fmtNumber(latestPrice, 6)} ${reportCurrency}${latest && latest.price_is_fallback ? ' · Fallback aus letztem Kurs' : ''}`
|
||||
: null,
|
||||
].filter(Boolean).join(' · ') || 'Kein umrechenbarer Kurs am letzten Punkt',
|
||||
}),
|
||||
h(StatCard, {
|
||||
key: 'profit',
|
||||
label: 'Theoretischer Tagesgewinn',
|
||||
value: dailyProfit !== null ? fmtMoney(dailyProfit, reportCurrency) : 'n/a',
|
||||
sub: dailyCost !== null
|
||||
? `Tageskosten ${fmtMoney(dailyCost, reportCurrency)} · Walletwert ${walletValue !== null ? fmtMoney(walletValue, reportCurrency) : 'n/a'}`
|
||||
? `Tageskosten ${fmtMoney(dailyCost, reportCurrency)}`
|
||||
: 'Kein aktiver Miner fuer diese Waehrung',
|
||||
}),
|
||||
h(StatCard, {
|
||||
@@ -2934,7 +3076,7 @@
|
||||
? `${fmtNumber(breakEvenDaysOverall, 2)} Tage`
|
||||
: (investedCapital === null ? 'Keine Mietbasis' : 'Nicht erreichbar'),
|
||||
sub: investedCapital !== null
|
||||
? `${breakEvenEta ? `ETA ${breakEvenEta} · ` : ''}Cash ${fmtMoney(investedCapital, reportCurrency)}${reinvestedCapital !== null ? ` · Reinvest ${fmtMoney(reinvestedCapital, reportCurrency)}` : ''}${totalHoldingsValue !== null ? ` · Bestand ${fmtMoney(totalHoldingsValue, reportCurrency)}` : ''}`
|
||||
? `${breakEvenEta ? `ETA ${breakEvenEta} · ` : ''}Cash ${fmtMoney(investedCapital, reportCurrency)}${reinvestedCapital !== null ? ` · Reinvest ${fmtMoney(reinvestedCapital, reportCurrency)}` : ''}`
|
||||
: (breakEvenPrice !== null
|
||||
? `Break-even-Kurs ${fmtNumber(breakEvenPrice, 6)} ${reportCurrency}`
|
||||
: (investedCapital === null
|
||||
@@ -2943,8 +3085,17 @@
|
||||
}),
|
||||
]),
|
||||
h('div', { key: 'charts', className: 'mc-overview-grid' }, [
|
||||
panel('Performance-Verlauf', `${perDayLabel}-Raten der letzten 15 Tage als Tagesdurchschnitt ohne negative Ausreißer.`, h(SimpleChart, { type: 'area', data: overviewCharts.performance })),
|
||||
panel('Kurs-Verlauf', 'Preiswerte der letzten 15 Tage.', h(SimpleChart, { type: 'line', data: overviewCharts.pricing })),
|
||||
panel('Performance-Verlauf', `${perDayLabel}-Raten der letzten 15 Tage als Tagesdurchschnitt ohne negative Ausreißer plus aktive MH/s pro Tag.`, h(SimpleChart, {
|
||||
type: 'line',
|
||||
series: overviewCharts.performance.series,
|
||||
leftAxisLabel: perDayLabel,
|
||||
rightAxisLabel: 'Hashrate MH/s',
|
||||
})),
|
||||
panel('Kurs-Verlauf', `Durchschnittlicher Kurs pro Tag der letzten 15 Tage in ${reportCurrency}.`, h(SimpleChart, {
|
||||
type: 'line',
|
||||
data: overviewCharts.pricing,
|
||||
yLabel: `Kurs ${reportCurrency}`,
|
||||
})),
|
||||
]),
|
||||
panel('Zielmonitor', `Rest-${currentCoinCurrency} und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.`,
|
||||
h('div', { className: 'mc-target-grid' },
|
||||
|
||||
Reference in New Issue
Block a user