diff --git a/src/lib/FormulaEvaluator.js b/src/lib/FormulaEvaluator.js index f2a59faf3..3eb950901 100644 --- a/src/lib/FormulaEvaluator.js +++ b/src/lib/FormulaEvaluator.js @@ -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') { diff --git a/src/lib/tests/FormulaEvaluator.test.js b/src/lib/tests/FormulaEvaluator.test.js index e607c4aa8..05d1a0b9f 100644 --- a/src/lib/tests/FormulaEvaluator.test.js +++ b/src/lib/tests/FormulaEvaluator.test.js @@ -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', () => { diff --git a/src/lib/tests/GraphDataUtils.test.js b/src/lib/tests/GraphDataUtils.test.js index 5bae460dc..3de87a962 100644 --- a/src/lib/tests/GraphDataUtils.test.js +++ b/src/lib/tests/GraphDataUtils.test.js @@ -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', () => {