wip
This commit is contained in:
165
packages/db/src/engine/compute.ts
Normal file
165
packages/db/src/engine/compute.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { round } from '@openpanel/common';
|
||||
import { alphabetIds } from '@openpanel/constants';
|
||||
import type { IChartFormula } from '@openpanel/validation';
|
||||
import * as mathjs from 'mathjs';
|
||||
import type { ConcreteSeries } from './types';
|
||||
|
||||
/**
|
||||
* Compute formula series from fetched event series
|
||||
* Formulas reference event series using alphabet IDs (A, B, C, etc.)
|
||||
*/
|
||||
export function compute(
|
||||
fetchedSeries: ConcreteSeries[],
|
||||
definitions: Array<{
|
||||
type: 'event' | 'formula';
|
||||
id?: string;
|
||||
formula?: string;
|
||||
}>,
|
||||
): ConcreteSeries[] {
|
||||
const results: ConcreteSeries[] = [...fetchedSeries];
|
||||
|
||||
// Process formulas in order (they can reference previous formulas)
|
||||
definitions.forEach((definition, formulaIndex) => {
|
||||
if (definition.type !== 'formula') {
|
||||
return;
|
||||
}
|
||||
|
||||
const formula = definition as IChartFormula;
|
||||
if (!formula.formula) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Group ALL series (events + previously computed formulas) by breakdown signature
|
||||
// Series with the same breakdown values should be computed together
|
||||
const seriesByBreakdown = new Map<string, ConcreteSeries[]>();
|
||||
|
||||
// Include both fetched event series AND previously computed formulas
|
||||
const allSeries = [
|
||||
...fetchedSeries,
|
||||
...results.filter((s) => s.definitionIndex < formulaIndex),
|
||||
];
|
||||
|
||||
allSeries.forEach((serie) => {
|
||||
// Create breakdown signature: skip first name part (event/formula name) and use breakdown values
|
||||
// If name.length === 1, it means no breakdowns (just event name)
|
||||
// If name.length > 1, name[0] is event name, name[1+] are breakdown values
|
||||
const breakdownSignature =
|
||||
serie.name.length > 1 ? serie.name.slice(1).join(':::') : '';
|
||||
|
||||
if (!seriesByBreakdown.has(breakdownSignature)) {
|
||||
seriesByBreakdown.set(breakdownSignature, []);
|
||||
}
|
||||
seriesByBreakdown.get(breakdownSignature)!.push(serie);
|
||||
});
|
||||
|
||||
// Compute formula for each breakdown group
|
||||
for (const [breakdownSignature, breakdownSeries] of seriesByBreakdown) {
|
||||
// Map series by their definition index for formula evaluation
|
||||
const seriesByIndex = new Map<number, ConcreteSeries>();
|
||||
breakdownSeries.forEach((serie) => {
|
||||
seriesByIndex.set(serie.definitionIndex, serie);
|
||||
});
|
||||
|
||||
// Get all unique dates across all series in this breakdown group
|
||||
const allDates = new Set<string>();
|
||||
breakdownSeries.forEach((serie) => {
|
||||
serie.data.forEach((item) => {
|
||||
allDates.add(item.date);
|
||||
});
|
||||
});
|
||||
|
||||
const sortedDates = Array.from(allDates).sort(
|
||||
(a, b) => new Date(a).getTime() - new Date(b).getTime(),
|
||||
);
|
||||
|
||||
// Calculate formula for each date
|
||||
const formulaData = sortedDates.map((date) => {
|
||||
const scope: Record<string, number> = {};
|
||||
|
||||
// Build scope using alphabet IDs (A, B, C, etc.)
|
||||
definitions.slice(0, formulaIndex).forEach((depDef, depIndex) => {
|
||||
const readableId = alphabetIds[depIndex];
|
||||
if (!readableId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the series for this dependency in the current breakdown group
|
||||
const depSeries = seriesByIndex.get(depIndex);
|
||||
if (depSeries) {
|
||||
const dataPoint = depSeries.data.find((d) => d.date === date);
|
||||
scope[readableId] = dataPoint?.count ?? 0;
|
||||
} else {
|
||||
// Could be a formula from a previous breakdown group - find it in results
|
||||
// Match by definitionIndex AND breakdown signature
|
||||
const formulaSerie = results.find(
|
||||
(s) =>
|
||||
s.definitionIndex === depIndex &&
|
||||
'type' in s.definition &&
|
||||
s.definition.type === 'formula' &&
|
||||
s.name.slice(1).join(':::') === breakdownSignature,
|
||||
);
|
||||
if (formulaSerie) {
|
||||
const dataPoint = formulaSerie.data.find((d) => d.date === date);
|
||||
scope[readableId] = dataPoint?.count ?? 0;
|
||||
} else {
|
||||
scope[readableId] = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Evaluate formula
|
||||
let count: number;
|
||||
try {
|
||||
count = mathjs
|
||||
.parse(formula.formula)
|
||||
.compile()
|
||||
.evaluate(scope) as number;
|
||||
} catch (error) {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
date,
|
||||
count:
|
||||
Number.isNaN(count) || !Number.isFinite(count)
|
||||
? 0
|
||||
: round(count, 2),
|
||||
total_count: breakdownSeries[0]?.data.find((d) => d.date === date)
|
||||
?.total_count,
|
||||
};
|
||||
});
|
||||
|
||||
// Create concrete series for this formula
|
||||
const templateSerie = breakdownSeries[0]!;
|
||||
|
||||
// Extract breakdown values from template series name
|
||||
// name[0] is event/formula name, name[1+] are breakdown values
|
||||
const breakdownValues =
|
||||
templateSerie.name.length > 1 ? templateSerie.name.slice(1) : [];
|
||||
|
||||
const formulaName =
|
||||
breakdownValues.length > 0
|
||||
? [formula.displayName || formula.formula, ...breakdownValues]
|
||||
: [formula.displayName || formula.formula];
|
||||
|
||||
const formulaSeries: ConcreteSeries = {
|
||||
id: `formula-${formula.id ?? formulaIndex}-${breakdownSignature || 'default'}`,
|
||||
definitionId:
|
||||
formula.id ?? alphabetIds[formulaIndex] ?? `formula-${formulaIndex}`,
|
||||
definitionIndex: formulaIndex,
|
||||
name: formulaName,
|
||||
context: {
|
||||
filters: templateSerie.context.filters,
|
||||
breakdownValue: templateSerie.context.breakdownValue,
|
||||
breakdowns: templateSerie.context.breakdowns,
|
||||
},
|
||||
data: formulaData,
|
||||
definition: formula,
|
||||
};
|
||||
|
||||
results.push(formulaSeries);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user