Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/lib/FormulaEvaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,19 @@ export function evaluateFormula(formula, variables) {
try {
const processedFormula = preprocessFormula(formula);
const expr = parser.parse(processedFormula);
let result = expr.evaluate(variables);

// Default any variables referenced by the formula but missing from the
// provided variables to 0. This keeps charts rendering when a referenced
// field has no value on a row (consistent with the "sum" operator, which
// already treats missing fields as 0).
const safeVariables = { ...(variables || {}) };
for (const varName of expr.variables()) {
if (!(varName in safeVariables)) {
safeVariables[varName] = 0;
}
}

let result = expr.evaluate(safeVariables);

// Convert boolean results to numbers (for comparison operators)
if (typeof result === 'boolean') {
Expand Down
11 changes: 9 additions & 2 deletions src/lib/tests/FormulaEvaluator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,15 @@ describe('FormulaEvaluator', () => {
expect(evaluateFormula('invalid syntax !!!', { x: 10 })).toBe(null);
});

it('should return null for undefined variables', () => {
expect(evaluateFormula('unknownVar * 2', {})).toBe(null);
it('should treat undefined variables as 0', () => {
// Undefined variables should default to 0 so charts still render
// when a referenced field has no value on a row (consistent with the
// behavior of the "sum" operator, which already treats missing fields
// as 0).
expect(evaluateFormula('unknownVar * 2', {})).toBe(0);
expect(evaluateFormula('x + unknownVar', { x: 5 })).toBe(5);
expect(evaluateFormula('x * y', { x: 10 })).toBe(0);
expect(evaluateFormula('a + b + c', { b: 7 })).toBe(7);
});

it('should return null for NaN results', () => {
Expand Down
49 changes: 49 additions & 0 deletions src/lib/tests/GraphDataUtils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,55 @@ describe('GraphDataUtils', () => {
// Should not throw, chart should render with regular values
expect(result).toHaveProperty('datasets');
});

it('should render chart when a referenced field is undefined on some rows', () => {
// Reproduces the bug where a formula referencing a field that is
// undefined on a row caused the entire chart to be empty. Undefined
// fields should be treated as 0 instead.
const dataWithMissingField = [
{ attributes: { month: 'Jan', price: 10 } }, // quantity undefined
{ attributes: { month: 'Feb', price: 20, quantity: 3 } },
{ attributes: { month: 'Mar', price: 15 } }, // quantity undefined
];

const calculatedValues = [{
name: 'Total',
operator: 'formula',
formula: 'price * quantity',
}];

const result = processBarLineData(dataWithMissingField, 'month', [], null, calculatedValues);

expect(result).toHaveProperty('datasets');
expect(result.datasets.length).toBe(1);
expect(result.datasets[0].label).toBe('Total');
// Jan: 10 * 0 = 0, Feb: 20 * 3 = 60, Mar: 15 * 0 = 0
expect(result.datasets[0].data).toContain(60);
});

it('should render chart when the referenced field is undefined on every row', () => {
// Even more extreme case: the field exists in the schema but no row
// has a value for it. The chart should still render (using 0 for the
// missing field) rather than disappearing entirely.
const dataWithAllMissing = [
{ attributes: { month: 'Jan', price: 10 } },
{ attributes: { month: 'Feb', price: 20 } },
];

const calculatedValues = [{
name: 'Total',
operator: 'formula',
formula: 'price + quantity',
}];

const result = processBarLineData(dataWithAllMissing, 'month', [], null, calculatedValues);

expect(result).toHaveProperty('datasets');
expect(result.datasets.length).toBe(1);
// Jan: 10 + 0 = 10, Feb: 20 + 0 = 20
expect(result.datasets[0].data).toContain(10);
expect(result.datasets[0].data).toContain(20);
});
});

describe('processPieData with formula', () => {
Expand Down
Loading