Source: js/chart-data-utils.js

// 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 };
}