import { ChartRuleError } from './errors'; import { type ConditionNode, type ComparisonOperandNode, type EqualityComparisonNode, type IfElseNode, type LiteralNode, type NumericComparisonNode, type NumericOperandNode, type RefNode, type RulePlaceholder, type Scalar, } from './types'; /** A non-recursive comparison operand: `value`, a literal node, or a scalar. */ export type Operand = ComparisonOperandNode | Scalar; export type NumericOperand = NumericOperandNode | number; function hasOwn(value: object, key: PropertyKey): boolean { return Object.prototype.hasOwnProperty.call(value, key); } function isOperandNode(operand: Operand): operand is ComparisonOperandNode { return typeof operand === 'object' && operand !== null && hasOwn(operand, 'kind'); } /** True when a value is a `{$rule}` placeholder — the marker the builders emit. */ export function isRulePlaceholder(value: unknown): value is RulePlaceholder { return typeof value === 'object' && value !== null && hasOwn(value, '$rule'); } /** Wrap a bare scalar as a literal node. */ export function literal(value: TValue): LiteralNode { if (typeof value === 'number' && !Number.isFinite(value)) { throw new ChartRuleError('literal(value) requires a finite number'); } return { kind: 'literal', value }; } function toOperandNode(operand: Operand): ComparisonOperandNode { return isOperandNode(operand) ? operand : literal(operand); } function toNumericOperandNode(operand: NumericOperand): NumericOperandNode { return typeof operand === 'number' ? literal(operand) : operand; } /** * `value` — this data point's plotted numeric value: a bar's height, a point's * y, a slice's amount. It is what a colour rule compares against a constant. */ export const value: RefNode = Object.freeze({ kind: 'value' }); /* Comparisons — return a condition with no nested expressions. */ function equalityComparison( kind: EqualityComparisonNode['kind'], ): (left: Operand, right: Operand) => EqualityComparisonNode { return (left, right) => ({ kind, left: toOperandNode(left), right: toOperandNode(right) }); } function numericComparison( kind: NumericComparisonNode['kind'], ): (left: NumericOperand, right: NumericOperand) => NumericComparisonNode { return (left, right) => ({ kind, left: toNumericOperandNode(left), right: toNumericOperandNode(right) }); } export const equals = equalityComparison('equals'); export const isNot = equalityComparison('isNot'); export const lessThan = numericComparison('lessThan'); export const greaterThan = numericComparison('greaterThan'); export const lessOrEqual = numericComparison('lessOrEqual'); export const greaterOrEqual = numericComparison('greaterOrEqual'); /** * A conditional colour: pick `ifTrue` when the condition holds, else `ifFalse`. * * when(lessThan(value, 100), '#E24B4A', '#378ADD') * // → '#E24B4A' when this point's value is under 100, '#378ADD' otherwise * * `ifFalse` is optional and defaults to `null` — for a colour, that means no * override (the chart's default is kept). Branches are colour literals only; * nested rules are intentionally unsupported in v1. */ export function when(condition: ConditionNode, ifTrue: string, ifFalse: string | null = null): IfElseNode { if (ifTrue === undefined) { throw new ChartRuleError('when(condition, ifTrue, ifFalse?) needs a value for the true case'); } return { kind: 'ifElse', cond: condition, then: literal(ifTrue), else: literal(ifFalse), }; } /** * Target the colour channel — the v1 channel. Wrap a conditional value so the * renderer knows it resolves to a colour, and the panel knows what it edits. * * color(when(lessThan(value, 100), '#E24B4A', '#378ADD')) * // → red when this point's value is under 100, blue otherwise * * Takes the flat conditional value produced by `when(...)`. Returns a `{$rule}` * placeholder tagged `channel: 'color'`. */ export function color(rule: IfElseNode): RulePlaceholder { return { $rule: rule, channel: 'color' }; }