// chart-data-utils.js
// Daten-Mapping, Picker, Extrema, Statistiken
import { toMs } from 'chart-time-utils';
/**
* Wählt ersten verfügbaren Wert aus Objekt
* @param {Object} obj - Quell-Objekt
* @param {string[]} keys - Array von Schlüsseln (Priorität von links)
* @returns {*} Erster verfügbarer Wert oder null
*/
export function pick(obj, keys) {
if (!obj) return null;
for (const k of keys) {
const v = obj[k];
if (v !== undefined && v !== null && v !== '') return v;
}
return null;
}
/**
* Wählt ersten verfügbaren numerischen Wert aus Objekt
* @param {Object} obj - Quell-Objekt
* @param {string[]} keys - Array von Schlüsseln (Priorität von links)
* @returns {number|null} Erster verfügbarer numerischer Wert oder null
*/
export function pickNum(obj, keys) {
for (const k of keys) {
const v = obj?.[k];
if (v === undefined || v === null || v === '') continue;
const n = typeof v === 'string' ? Number(v.replace(',', '.')) : Number(v);
if (Number.isFinite(n)) return n;
}
return null;
}
/**
* Berechnet Min/Max/Avg aus Array
* @param {number[]} arr - Array von Zahlen
* @returns {{min: number|null, max: number|null, avg: number|null}} Statistiken
*/
export function stats(arr) {
const vals = arr.filter(Number.isFinite);
if (!vals.length) return { min: null, max: null, avg: null };
const min = Math.min(...vals);
const max = Math.max(...vals);
const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
return { min, max, avg };
}
/**
* Berechnet Extrema (min/max) aus Arrays oder Series-Objekten
* @param {Array|Object[]} tsOrSeries - Zeitstempel-Array oder Series-Array
* @param {Array|string} valuesOrKey - Werte-Array oder Property-Name
* @returns {{min: number|null, max: number|null, iMin: number, iMax: number, tMin: number|null, tMax: number|null}}
*/
export function computeExtrema(tsOrSeries, valuesOrKey) {
// Modus A: (tsArray, valuesArray)
// Modus B: (seriesArray, 'key') -> Objektfelder + .t (ms/sek/String)
let getLen, getVal, getTs;
if (Array.isArray(tsOrSeries) && Array.isArray(valuesOrKey)) {
const ts = tsOrSeries,
vals = valuesOrKey;
getLen = () => Math.min(ts.length, vals.length);
getVal = (i) => vals[i];
getTs = (i) => ts[i] ?? null;
} else {
const series = tsOrSeries,
key = valuesOrKey;
getLen = () => series.length;
getVal = (i) => series[i]?.[key];
getTs = (i) => {
const s = series[i]?.ts ?? series[i]?.t ?? series[i]?.time ?? series[i]?.timestamp;
return toMs(s);
};
}
let iMin = -1,
iMax = -1,
vMin = +Infinity,
vMax = -Infinity;
const n = getLen();
for (let i = 0; i < n; i++) {
const v = getVal(i);
if (!Number.isFinite(v)) continue;
if (v < vMin) {
vMin = v;
iMin = i;
}
if (v > vMax) {
vMax = v;
iMax = i;
}
}
return {
min: Number.isFinite(vMin) ? vMin : null,
max: Number.isFinite(vMax) ? vMax : null,
iMin,
iMax,
tMin: iMin >= 0 ? getTs(iMin) : null,
tMax: iMax >= 0 ? getTs(iMax) : null,
};
}
/**
* Wandelt API-Rohdaten in Chart-Serien um (Zeitstempel + Werte)
* @param {Array} rows
* @param {Array} valueKeysOut - z.B. ['temperature', 'outdoortemperature']
* @param {Array} valueKeysIn - z.B. ['indoortemperature', 'in_temp']
* @returns {Array.<{t:number, out:(number|null), in:(number|null)}>} Array sortierter Punkte
*/
export function rowsToSeries(rows, valueKeysOut, valueKeysIn) {
const out = [];
for (const r of rows || []) {
// Zeitstempel extrahieren (kann Zahl oder String sein)
const rawTs = r?.datatimestamp ?? r?.timestamp ?? r?.ts ?? r?.time ?? r?.t ?? null;
if (rawTs == null) continue;
const t = toMs(rawTs);
if (t == null) continue;
const o = pickNum(r, valueKeysOut);
const i = pickNum(r, valueKeysIn);
if (!Number.isFinite(o) && !Number.isFinite(i)) continue;
out.push({ t, in: Number.isFinite(i) ? i : null, out: Number.isFinite(o) ? o : null });
}
out.sort((a, b) => a.t - b.t);
return out;
}
/**
* Liefert Gap-Detection-Parameter je nach Modus
* @param {string} mode - Chart-Modus ('auto', 'day', 'week', 'month')
* @returns {{minGapMs: number, maxGapCount: number}} Gap-Detection-Konfiguration
*/
export function gapOptionsForMode(mode) {
const base = {
expectedMs: 60_000,
tolerancePct: 0.35,
factor: 1.6,
mergeWithinMs: 90_000,
};
const minByMode = {
auto: 0,
day: 5 * 60_000,
week: 15 * 60_000,
month: 60 * 60_000,
};
return { ...base, minDurationMs: minByMode[mode] ?? 0 };
}