sddsf
This commit is contained in:
@@ -438,8 +438,29 @@
|
||||
}
|
||||
|
||||
function SimpleChart(props) {
|
||||
const series = Array.isArray(props.series)
|
||||
? props.series
|
||||
.map((item, index) => ({
|
||||
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],
|
||||
data: Array.isArray(item && item.data)
|
||||
? item.data.filter((point) => point && point.y !== null && point.y !== undefined)
|
||||
: [],
|
||||
}))
|
||||
.filter((item) => item.data.length > 0)
|
||||
: [];
|
||||
const points = Array.isArray(props.data) ? props.data.filter((point) => point && point.y !== null && point.y !== undefined) : [];
|
||||
if (!points.length) {
|
||||
const isMultiSeries = series.length > 0;
|
||||
const activeSeries = isMultiSeries ? series : [{
|
||||
key: 'default',
|
||||
label: props.yLabel || 'Wert',
|
||||
color: props.type === 'area' ? '#2dd4bf' : '#60a5fa',
|
||||
data: points,
|
||||
}];
|
||||
const allPoints = activeSeries.flatMap((item) => item.data);
|
||||
|
||||
if (!allPoints.length) {
|
||||
return h('div', { className: 'mc-empty' }, 'Keine Daten fuer diese Ansicht.');
|
||||
}
|
||||
|
||||
@@ -463,21 +484,28 @@
|
||||
const width = 640;
|
||||
const height = 220;
|
||||
const padding = 24;
|
||||
const values = points.map((point) => Number(point.y));
|
||||
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 stepX = points.length > 1 ? (width - padding * 2) / (points.length - 1) : 0;
|
||||
const coords = points.map((point, index) => {
|
||||
const seriesCoords = activeSeries.map((item) => {
|
||||
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);
|
||||
return [x, y];
|
||||
});
|
||||
const line = coords.map((coord) => coord.join(',')).join(' ');
|
||||
const area = coords.length
|
||||
|
||||
return {
|
||||
...item,
|
||||
coords,
|
||||
line: coords.map((coord) => coord.join(',')).join(' '),
|
||||
area: coords.length
|
||||
? [[coords[0][0], height - padding]].concat(coords, [[coords[coords.length - 1][0], height - padding]])
|
||||
.map((coord) => coord.join(',')).join(' ')
|
||||
: '';
|
||||
: '',
|
||||
};
|
||||
});
|
||||
|
||||
return h('div', { className: 'mc-chart space-y-3' }, [
|
||||
h('div', { key: 'meta', className: 'mc-flex-split mc-kicker' }, [
|
||||
@@ -490,10 +518,9 @@
|
||||
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 }),
|
||||
]),
|
||||
props.type === 'area' ? h('polygon', { key: 'area', points: area, fill: 'rgba(45, 212, 191, 0.18)' }) : null,
|
||||
props.type === 'bar'
|
||||
? h('g', { key: 'bars' }, coords.map((coord, index) => {
|
||||
const barWidth = Math.max(10, stepX * 0.6 || 24);
|
||||
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);
|
||||
return h('rect', {
|
||||
key: index,
|
||||
x: coord[0] - barWidth / 2,
|
||||
@@ -504,26 +531,47 @@
|
||||
fill: 'rgba(59, 130, 246, 0.75)',
|
||||
});
|
||||
}))
|
||||
: h('polyline', {
|
||||
key: 'line',
|
||||
points: line,
|
||||
: h('g', { key: 'series' }, seriesCoords.flatMap((item, index) => {
|
||||
const nodes = [];
|
||||
if (props.type === 'area' && !isMultiSeries && item.area) {
|
||||
nodes.push(h('polygon', {
|
||||
key: `${item.key}-area`,
|
||||
points: item.area,
|
||||
fill: 'rgba(45, 212, 191, 0.18)',
|
||||
}));
|
||||
}
|
||||
nodes.push(h('polyline', {
|
||||
key: `${item.key}-line`,
|
||||
points: item.line,
|
||||
fill: 'none',
|
||||
stroke: props.type === 'area' ? '#2dd4bf' : '#60a5fa',
|
||||
stroke: item.color,
|
||||
strokeWidth: 3,
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
}),
|
||||
h('g', { key: 'dots' }, coords.map((coord, index) => h('circle', {
|
||||
key: index,
|
||||
}));
|
||||
nodes.push(h('g', { key: `${item.key}-dots` }, item.coords.map((coord, pointIndex) => h('circle', {
|
||||
key: `${item.key}-${pointIndex}`,
|
||||
cx: coord[0],
|
||||
cy: coord[1],
|
||||
r: 4,
|
||||
fill: '#f8fafc',
|
||||
stroke: props.type === 'area' ? '#2dd4bf' : '#60a5fa',
|
||||
stroke: item.color,
|
||||
strokeWidth: 2,
|
||||
}))),
|
||||
}))));
|
||||
return nodes;
|
||||
})),
|
||||
]),
|
||||
h('div', { key: 'labels', className: 'mc-mini-grid' },
|
||||
isMultiSeries
|
||||
? h('div', { key: 'legend', className: 'mc-mini-grid' },
|
||||
seriesCoords.map((item) => {
|
||||
const latestPoint = item.data[item.data.length - 1] || null;
|
||||
return h('div', { key: item.key, className: 'mc-mini-card' }, [
|
||||
h('div', { key: 'label', className: 'mc-kicker', style: { color: item.color } }, item.label),
|
||||
h('div', { key: 'value' }, latestPoint ? `${fmtNumber(latestPoint.y, 4)} · ${latestPoint.x}` : 'n/a'),
|
||||
]);
|
||||
})
|
||||
)
|
||||
: h('div', { key: 'labels', className: 'mc-mini-grid' },
|
||||
points.slice(-3).map((point, index) => h('div', { key: index, className: 'mc-mini-card' }, `${point.x}: ${fmtNumber(point.y, 6)}`))
|
||||
),
|
||||
]);
|
||||
@@ -1128,13 +1176,44 @@
|
||||
loadSavedDashboards();
|
||||
}, [payload, projectKey]);
|
||||
|
||||
const overviewCharts = useMemo(() => ({
|
||||
const overviewCharts = useMemo(() => {
|
||||
const comparisonRows = measurements.filter((row) => {
|
||||
const miningRate = Number(row.doge_per_hour_per_mh_interval);
|
||||
const price = Number(row.effective_price_per_coin ?? row.price_per_coin);
|
||||
return Number.isFinite(miningRate) && miningRate > 0 && Number.isFinite(price) && price > 0;
|
||||
});
|
||||
const baseComparison = comparisonRows[0] || null;
|
||||
const baseMining = baseComparison ? Number(baseComparison.doge_per_hour_per_mh_interval) : null;
|
||||
const basePrice = baseComparison ? Number(baseComparison.effective_price_per_coin ?? baseComparison.price_per_coin) : null;
|
||||
|
||||
return {
|
||||
mining: measurements.map((row) => ({ x: row.measured_at.slice(5, 16), y: row.coins_total })),
|
||||
performance: measurements.filter((row) => row.doge_per_day_interval !== null)
|
||||
.map((row) => ({ x: row.measured_at.slice(5, 16), y: row.doge_per_day_interval })),
|
||||
pricing: measurements.filter((row) => row.price_per_coin !== null)
|
||||
.map((row) => ({ x: row.measured_at.slice(5, 16), y: row.price_per_coin })),
|
||||
}), [measurements]);
|
||||
miningVsPrice: baseMining && basePrice ? [
|
||||
{
|
||||
key: 'mining-rate',
|
||||
label: 'Mining/h je MH/s Index',
|
||||
color: '#2dd4bf',
|
||||
data: comparisonRows.map((row) => ({
|
||||
x: row.measured_at.slice(5, 16),
|
||||
y: (Number(row.doge_per_hour_per_mh_interval) / baseMining) * 100,
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: 'doge-price',
|
||||
label: 'DOGE-Kurs Index',
|
||||
color: '#f59e0b',
|
||||
data: comparisonRows.map((row) => ({
|
||||
x: row.measured_at.slice(5, 16),
|
||||
y: (Number(row.effective_price_per_coin ?? row.price_per_coin) / basePrice) * 100,
|
||||
})),
|
||||
},
|
||||
] : [],
|
||||
};
|
||||
}, [measurements]);
|
||||
|
||||
async function submitMeasurement(fromPreview) {
|
||||
const preview = normalizeOcrPreview(ocrPreview);
|
||||
@@ -2139,6 +2218,10 @@
|
||||
panel('Mining-Verlauf', 'Coins total der letzten 15 Tage.', h(SimpleChart, { type: 'line', data: overviewCharts.mining })),
|
||||
panel('Performance-Verlauf', 'DOGE-pro-Tag-Raten der letzten 15 Tage.', h(SimpleChart, { type: 'area', data: overviewCharts.performance })),
|
||||
panel('Kurs-Verlauf', 'Preiswerte der letzten 15 Tage.', h(SimpleChart, { type: 'line', data: overviewCharts.pricing })),
|
||||
panel('Mining vs. Kurs', 'Index-Vergleich der letzten 15 Tage. Mining wird auf DOGE pro Stunde je MH/s normalisiert, beide Reihen starten bei 100.', h(SimpleChart, {
|
||||
type: 'line',
|
||||
series: overviewCharts.miningVsPrice,
|
||||
})),
|
||||
]),
|
||||
panel('Zielmonitor', 'Rest-DOGE und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.',
|
||||
h('div', { className: 'mc-target-grid' },
|
||||
|
||||
@@ -20,6 +20,7 @@ final class AnalyticsService
|
||||
$baselineAt = (string) ($settings['baseline_measured_at'] ?? '');
|
||||
$costPlans = is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [];
|
||||
$payouts = is_array($settings['payouts'] ?? null) ? $settings['payouts'] : [];
|
||||
$purchasedMiners = is_array($settings['purchased_miners'] ?? null) ? $settings['purchased_miners'] : [];
|
||||
$preferredPriceCurrencies = array_values(array_unique(array_filter([
|
||||
strtoupper(trim((string) ($settings['report_currency'] ?? ''))),
|
||||
'USD',
|
||||
@@ -30,11 +31,11 @@ final class AnalyticsService
|
||||
$baselineTs = $this->utcTimestamp($baselineAt);
|
||||
$previous = null;
|
||||
$previousMeasuredTs = null;
|
||||
$previousEffectiveCoinsTotal = null;
|
||||
$previousIntervalRate = null;
|
||||
$result = [];
|
||||
$payoutIndex = 0;
|
||||
$cumulativePayouts = 0.0;
|
||||
$lastPayoutTs = null;
|
||||
$latestPriceByCurrency = [];
|
||||
|
||||
foreach ($measurements as $row) {
|
||||
@@ -46,6 +47,7 @@ final class AnalyticsService
|
||||
}
|
||||
|
||||
$cumulativePayouts += (float) ($payouts[$payoutIndex]['coins_amount'] ?? 0);
|
||||
$lastPayoutTs = $payoutTs;
|
||||
$payoutIndex++;
|
||||
}
|
||||
|
||||
@@ -55,17 +57,32 @@ final class AnalyticsService
|
||||
$hoursSinceBaseline = $baselineTs > 0 && $measuredTs > $baselineTs ? ($measuredTs - $baselineTs) / 3600 : 0.0;
|
||||
$perHourSinceBaseline = $hoursSinceBaseline > 0 ? $growth / $hoursSinceBaseline : null;
|
||||
$perDaySinceBaseline = $perHourSinceBaseline !== null ? $perHourSinceBaseline * 24 : null;
|
||||
$activeHashrateMh = $this->measurementHashrateMh($costPlans, $purchasedMiners, $measuredTs > 0 ? $measuredTs : null);
|
||||
|
||||
$intervalHours = null;
|
||||
$intervalGrowth = null;
|
||||
$perHourInterval = null;
|
||||
$perDayInterval = null;
|
||||
$perHourPerMhInterval = null;
|
||||
$perDayPerMhInterval = null;
|
||||
|
||||
if (is_array($previous) && $previousMeasuredTs !== null && $previousEffectiveCoinsTotal !== null) {
|
||||
$intervalHours = max(0.0, ($measuredTs - $previousMeasuredTs) / 3600);
|
||||
$intervalGrowth = $effectiveCoinsTotal - $previousEffectiveCoinsTotal;
|
||||
if (is_array($previous) && $previousMeasuredTs !== null) {
|
||||
$intervalStartTs = $previousMeasuredTs;
|
||||
$intervalStartCoins = (float) ($previous['coins_total'] ?? 0.0);
|
||||
|
||||
if ($lastPayoutTs !== null && $lastPayoutTs > $previousMeasuredTs && $lastPayoutTs <= $measuredTs) {
|
||||
$intervalStartTs = $lastPayoutTs;
|
||||
$intervalStartCoins = 0.0;
|
||||
}
|
||||
|
||||
$intervalHours = max(0.0, ($measuredTs - $intervalStartTs) / 3600);
|
||||
$intervalGrowth = $visibleCoinsTotal - $intervalStartCoins;
|
||||
$perHourInterval = $intervalHours > 0 ? $intervalGrowth / $intervalHours : null;
|
||||
$perDayInterval = $perHourInterval !== null ? $perHourInterval * 24 : null;
|
||||
if ($perHourInterval !== null && $activeHashrateMh > 0) {
|
||||
$perHourPerMhInterval = $perHourInterval / $activeHashrateMh;
|
||||
$perDayPerMhInterval = $perDayInterval !== null ? $perDayInterval / $activeHashrateMh : null;
|
||||
}
|
||||
}
|
||||
|
||||
$trendLabel = 'stabil';
|
||||
@@ -147,10 +164,13 @@ final class AnalyticsService
|
||||
'hours_since_baseline' => $this->roundOrNull($hoursSinceBaseline, 4),
|
||||
'doge_per_hour_since_baseline' => $this->roundOrNull($perHourSinceBaseline, 6),
|
||||
'doge_per_day_since_baseline' => $this->roundOrNull($perDaySinceBaseline, 6),
|
||||
'active_hashrate_mh' => $this->roundOrNull($activeHashrateMh > 0 ? $activeHashrateMh : null, 4),
|
||||
'interval_hours' => $this->roundOrNull($intervalHours, 4),
|
||||
'interval_growth' => $this->roundOrNull($intervalGrowth, 6),
|
||||
'doge_per_hour_interval' => $this->roundOrNull($perHourInterval, 6),
|
||||
'doge_per_day_interval' => $this->roundOrNull($perDayInterval, 6),
|
||||
'doge_per_hour_per_mh_interval' => $this->roundOrNull($perHourPerMhInterval, 8),
|
||||
'doge_per_day_per_mh_interval' => $this->roundOrNull($perDayPerMhInterval, 8),
|
||||
'trend_label' => $trendLabel,
|
||||
'effective_price_per_coin' => $this->roundOrNull($price, 8),
|
||||
'effective_price_currency' => $priceCurrency,
|
||||
@@ -172,7 +192,6 @@ final class AnalyticsService
|
||||
}
|
||||
$previous = $row;
|
||||
$previousMeasuredTs = $measuredTs > 0 ? $measuredTs : null;
|
||||
$previousEffectiveCoinsTotal = $effectiveCoinsTotal;
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -565,6 +584,37 @@ final class AnalyticsService
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function measurementHashrateMh(array $costPlans, array $purchasedMiners, ?int $measurementTs = null): float
|
||||
{
|
||||
$total = 0.0;
|
||||
|
||||
foreach ($costPlans as $plan) {
|
||||
if (array_key_exists('is_active', $plan) && empty($plan['is_active'])) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->entryIsCovered($plan, $measurementTs)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$total += $this->normalizeHashrateMh($plan['mining_speed_value'] ?? null, $plan['mining_speed_unit'] ?? null);
|
||||
$total += $this->normalizeHashrateMh($plan['bonus_speed_value'] ?? null, $plan['bonus_speed_unit'] ?? null);
|
||||
}
|
||||
|
||||
foreach ($purchasedMiners as $miner) {
|
||||
if (array_key_exists('is_active', $miner) && empty($miner['is_active'])) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->entryIsCovered($miner, $measurementTs, 'purchased_at')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$total += $this->normalizeHashrateMh($miner['mining_speed_value'] ?? null, $miner['mining_speed_unit'] ?? null);
|
||||
$total += $this->normalizeHashrateMh($miner['bonus_speed_value'] ?? null, $miner['bonus_speed_unit'] ?? null);
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function totalPurchasedMinerCost(array $purchasedMiners, string $targetCurrency, ?int $measurementTs = null, ?array $fxContext = null): ?float
|
||||
{
|
||||
$target = strtoupper(trim($targetCurrency));
|
||||
|
||||
Reference in New Issue
Block a user