@svgrid/enterprise 3.0.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SvFormulaBar.svelte +606 -0
- package/dist/SvFormulaBar.svelte.d.ts +62 -0
- package/dist/SvPivotDesigner.svelte +65 -14
- package/dist/SvRibbonIcon.svelte +46 -0
- package/dist/SvRibbonIcon.svelte.d.ts +17 -0
- package/dist/SvSchemaChart.svelte +4 -2
- package/dist/SvSheet.svelte +4114 -0
- package/dist/SvSheet.svelte.d.ts +142 -0
- package/dist/SvSheetComment.svelte +98 -0
- package/dist/SvSheetComment.svelte.d.ts +22 -0
- package/dist/SvSheetConditionalFormat.svelte +162 -0
- package/dist/SvSheetConditionalFormat.svelte.d.ts +14 -0
- package/dist/SvSheetDataValidation.svelte +249 -0
- package/dist/SvSheetDataValidation.svelte.d.ts +14 -0
- package/dist/SvSheetFilterMenu.svelte +217 -0
- package/dist/SvSheetFilterMenu.svelte.d.ts +17 -0
- package/dist/SvSheetFindReplace.svelte +132 -0
- package/dist/SvSheetFindReplace.svelte.d.ts +10 -0
- package/dist/SvSheetFormatCells.svelte +421 -0
- package/dist/SvSheetFormatCells.svelte.d.ts +17 -0
- package/dist/SvSheetGoalSeek.svelte +144 -0
- package/dist/SvSheetGoalSeek.svelte.d.ts +12 -0
- package/dist/SvSheetInsertFunction.svelte +147 -0
- package/dist/SvSheetInsertFunction.svelte.d.ts +8 -0
- package/dist/SvSheetListPicker.svelte +89 -0
- package/dist/SvSheetListPicker.svelte.d.ts +16 -0
- package/dist/SvSheetManageRules.svelte +185 -0
- package/dist/SvSheetManageRules.svelte.d.ts +16 -0
- package/dist/SvSheetNameManager.svelte +225 -0
- package/dist/SvSheetNameManager.svelte.d.ts +11 -0
- package/dist/SvSheetPasteSpecial.svelte +105 -0
- package/dist/SvSheetPasteSpecial.svelte.d.ts +12 -0
- package/dist/SvSheetRemoveDuplicates.svelte +149 -0
- package/dist/SvSheetRemoveDuplicates.svelte.d.ts +12 -0
- package/dist/SvSheetRibbon.svelte +1201 -0
- package/dist/SvSheetRibbon.svelte.d.ts +34 -0
- package/dist/SvSheetSizeDialog.svelte +58 -0
- package/dist/SvSheetSizeDialog.svelte.d.ts +11 -0
- package/dist/SvSheetTabs.svelte +441 -0
- package/dist/SvSheetTabs.svelte.d.ts +36 -0
- package/dist/SvSheetTabs.test-harness.svelte.d.ts +17 -0
- package/dist/SvSheetTabs.test-harness.svelte.js +22 -0
- package/dist/SvSheetTextToColumns.svelte +172 -0
- package/dist/SvSheetTextToColumns.svelte.d.ts +13 -0
- package/dist/SvSheetValidationAlert.svelte +83 -0
- package/dist/SvSheetValidationAlert.svelte.d.ts +15 -0
- package/dist/cdn/svgrid-enterprise.svelte-external.js +18997 -5396
- package/dist/designer/assets/GridMenus-BlsvL_aU.js +8 -0
- package/dist/designer/assets/SvChartMenu-BYO9cYdy.css +1 -0
- package/dist/designer/assets/SvChartMenu-D9hEeHar.js +1 -0
- package/dist/designer/assets/SvDateRangeInput-BBRzrQeg.js +1 -0
- package/dist/designer/assets/SvDateTimePicker-DARvKUL2.js +1 -0
- package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +4 -0
- package/dist/designer/assets/SvGridChart-B8j1SSy3.js +3 -0
- package/dist/designer/assets/SvGridChart-BgLuPO7I.css +1 -0
- package/dist/designer/assets/SvGridChartBuilder-08YWWeqY.css +1 -0
- package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +1 -0
- package/dist/designer/assets/SvGridChartPanel-4Pc8y5yO.css +1 -0
- package/dist/designer/assets/SvGridChartPanel-pT6ITc2P.js +4 -0
- package/dist/designer/assets/{SvGridChartView-DUc-1RUf.css → SvGridChartView-7msfnWZT.css} +1 -1
- package/dist/designer/assets/SvGridChartView-bg281UA1.js +1 -0
- package/dist/designer/assets/SvListBox-BDS66lmZ.js +110 -0
- package/dist/designer/assets/SvMenuList-DJ47ba1h.css +1 -0
- package/dist/designer/assets/SvMenuList-Dgq2D5VN.js +1 -0
- package/dist/designer/assets/SvTabs-DKmwb5p1.css +1 -0
- package/dist/designer/assets/SvTabs-UFCMTHXS.js +1 -0
- package/dist/designer/assets/chart-cIPUsbM5.js +1 -0
- package/dist/designer/assets/chart-export-B6bkQ3X8.js +11 -0
- package/dist/designer/assets/chart-export-pdf-BbA28-gt.js +11 -0
- package/dist/designer/assets/chart-panel-messages-DtTxpBUU.js +1 -0
- package/dist/designer/assets/chart-summary-Czu_5epb.js +1 -0
- package/dist/designer/assets/chart-trend-D5I5VEe8.js +1 -0
- package/dist/designer/assets/chart-validate-BMqM-qkF.js +1 -0
- package/dist/designer/assets/column-resize-BYDzR1Ve.js +1 -0
- package/dist/designer/assets/date-format-B8JbEtIG.js +1 -0
- package/dist/designer/assets/date-format-DKS-z0xS.css +1 -0
- package/dist/designer/assets/editor-contract-Bpoa7A5g.js +1 -0
- package/dist/designer/assets/focus-trap-CAreAhlU.js +1 -0
- package/dist/designer/assets/index-CMFgAoVV.css +1 -0
- package/dist/designer/assets/{index-C_a9Cj8Y.js → index-DwU7CAMm.js} +291 -290
- package/dist/designer/assets/list-option-BP_dWS66.js +1 -0
- package/dist/designer/assets/row-resize-DLktcqUK.js +1 -0
- package/dist/designer/assets/src-CITpcm6b.js +1 -0
- package/dist/designer/assets/src-EdI3Xqds.js +58 -0
- package/dist/designer/index.html +23 -14
- package/dist/export-pdf.d.ts +89 -0
- package/dist/export-pdf.js +130 -0
- package/dist/export.js +17 -10
- package/dist/index.d.ts +64 -1
- package/dist/index.js +68 -1
- package/dist/install.js +5 -0
- package/dist/node/studio.js +64 -2
- package/dist/pivot-chart.d.ts +25 -1
- package/dist/pivot-chart.js +25 -69
- package/dist/sheet/address.d.ts +43 -0
- package/dist/sheet/address.js +81 -0
- package/dist/sheet/ast.d.ts +94 -0
- package/dist/sheet/ast.js +56 -0
- package/dist/sheet/auto-filter.d.ts +66 -0
- package/dist/sheet/auto-filter.js +147 -0
- package/dist/sheet/autocomplete.d.ts +42 -0
- package/dist/sheet/autocomplete.js +162 -0
- package/dist/sheet/coerce.d.ts +31 -0
- package/dist/sheet/coerce.js +122 -0
- package/dist/sheet/commands.d.ts +89 -0
- package/dist/sheet/commands.js +230 -0
- package/dist/sheet/comments.d.ts +29 -0
- package/dist/sheet/comments.js +73 -0
- package/dist/sheet/conditional-formats.d.ts +144 -0
- package/dist/sheet/conditional-formats.js +316 -0
- package/dist/sheet/deps.d.ts +60 -0
- package/dist/sheet/deps.js +199 -0
- package/dist/sheet/document.d.ts +150 -0
- package/dist/sheet/document.js +264 -0
- package/dist/sheet/edit-keys.d.ts +14 -0
- package/dist/sheet/edit-keys.js +40 -0
- package/dist/sheet/entry.d.ts +31 -0
- package/dist/sheet/entry.js +76 -0
- package/dist/sheet/evaluate.d.ts +61 -0
- package/dist/sheet/evaluate.js +337 -0
- package/dist/sheet/find-replace.d.ts +62 -0
- package/dist/sheet/find-replace.js +152 -0
- package/dist/sheet/format-store.d.ts +99 -0
- package/dist/sheet/format-store.js +233 -0
- package/dist/sheet/freeze.d.ts +54 -0
- package/dist/sheet/freeze.js +58 -0
- package/dist/sheet/function-catalog.d.ts +10 -0
- package/dist/sheet/function-catalog.js +94 -0
- package/dist/sheet/functions.d.ts +28 -0
- package/dist/sheet/functions.js +362 -0
- package/dist/sheet/goal-seek.d.ts +73 -0
- package/dist/sheet/goal-seek.js +153 -0
- package/dist/sheet/index.d.ts +53 -0
- package/dist/sheet/index.js +53 -0
- package/dist/sheet/merges.d.ts +71 -0
- package/dist/sheet/merges.js +137 -0
- package/dist/sheet/names.d.ts +31 -0
- package/dist/sheet/names.js +87 -0
- package/dist/sheet/navigate.d.ts +52 -0
- package/dist/sheet/navigate.js +135 -0
- package/dist/sheet/number-format.d.ts +63 -0
- package/dist/sheet/number-format.js +508 -0
- package/dist/sheet/palette.d.ts +19 -0
- package/dist/sheet/palette.js +84 -0
- package/dist/sheet/parse.d.ts +13 -0
- package/dist/sheet/parse.js +108 -0
- package/dist/sheet/paste-special.d.ts +164 -0
- package/dist/sheet/paste-special.js +592 -0
- package/dist/sheet/protection.d.ts +21 -0
- package/dist/sheet/protection.js +48 -0
- package/dist/sheet/rects.d.ts +45 -0
- package/dist/sheet/rects.js +112 -0
- package/dist/sheet/refs.d.ts +82 -0
- package/dist/sheet/refs.js +266 -0
- package/dist/sheet/ribbon-icons.d.ts +31 -0
- package/dist/sheet/ribbon-icons.js +350 -0
- package/dist/sheet/ribbon.d.ts +173 -0
- package/dist/sheet/ribbon.js +753 -0
- package/dist/sheet/shortcuts.d.ts +164 -0
- package/dist/sheet/shortcuts.js +623 -0
- package/dist/sheet/structure.d.ts +87 -0
- package/dist/sheet/structure.js +166 -0
- package/dist/sheet/tables.d.ts +70 -0
- package/dist/sheet/tables.js +147 -0
- package/dist/sheet/tokenize.d.ts +54 -0
- package/dist/sheet/tokenize.js +304 -0
- package/dist/sheet/transforms.d.ts +76 -0
- package/dist/sheet/transforms.js +154 -0
- package/dist/sheet/validation.d.ts +92 -0
- package/dist/sheet/validation.js +241 -0
- package/dist/sheet/workbook.d.ts +72 -0
- package/dist/sheet/workbook.js +409 -0
- package/dist/sheet-enable.d.ts +6 -0
- package/dist/sheet-enable.js +46 -0
- package/dist/studio/ui-components.generated.js +63 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -4
- package/dist/designer/assets/GridMenus-B1JtIN21.js +0 -8
- package/dist/designer/assets/SvDateRangeInput-CjO7nn_x.js +0 -1
- package/dist/designer/assets/SvDateTimePicker-ByBD_ov6.js +0 -1
- package/dist/designer/assets/SvField-YDLipR6O.css +0 -1
- package/dist/designer/assets/SvField-cj1PFNgZ.js +0 -1
- package/dist/designer/assets/SvGridCellEditor-0W1L2hJ1.js +0 -2
- package/dist/designer/assets/SvGridChart-DBOR4xcz.js +0 -2
- package/dist/designer/assets/SvGridChart-fkMWeh1J.css +0 -1
- package/dist/designer/assets/SvGridChartPanel-symrhNnR.js +0 -11
- package/dist/designer/assets/SvGridChartView-BmRxq6mU.js +0 -1
- package/dist/designer/assets/SvListBox-1oUF6Iav.js +0 -110
- package/dist/designer/assets/chart-DXFganU8.js +0 -1
- package/dist/designer/assets/column-resize-C_inr5zT.js +0 -1
- package/dist/designer/assets/date-format-DGagAqRc.css +0 -1
- package/dist/designer/assets/date-format-onlYDsrY.js +0 -1
- package/dist/designer/assets/easing-zaypKrr0.js +0 -3
- package/dist/designer/assets/index-k2stW6L8.css +0 -1
- package/dist/designer/assets/row-resize-B0FWHcNr.js +0 -1
- package/dist/designer/assets/src-D9p27OGC.js +0 -58
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional formatting, Excel's: rules over cells that colour them by
|
|
3
|
+
* what they hold. Highlight Cells Rules (greater than, between, text that
|
|
4
|
+
* contains, duplicates), Top/Bottom Rules (top 10, above average), Data
|
|
5
|
+
* Bars, Color Scales and Icon Sets, each over rectangles that move with
|
|
6
|
+
* an insert or delete.
|
|
7
|
+
*
|
|
8
|
+
* Rules are evaluated over COMPUTED values (what a formula shows, not its
|
|
9
|
+
* text), which is why the model lives here beside the workbook rather than
|
|
10
|
+
* in the grid's own value-driven rules. The statistics a rule needs over
|
|
11
|
+
* its range (min, max, mean, rank, counts) are computed once per repaint by
|
|
12
|
+
* the shell and handed in, so a thousand cells cost one pass.
|
|
13
|
+
*
|
|
14
|
+
* Priority is array order, first rule first, as Excel's Manage Rules lists
|
|
15
|
+
* them: the first rule that decides a property wins it, and a rule with
|
|
16
|
+
* Stop If True stops the rules below it for the cells it matches.
|
|
17
|
+
*/
|
|
18
|
+
import { isError } from './ast.js';
|
|
19
|
+
import { toText } from './coerce.js';
|
|
20
|
+
import { rectContains, shiftRects, subtractRect, rectsIntersect } from './rects.js';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Excel's palettes
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/** The six "with" styles of Excel's Highlight Cells dialogs. */
|
|
25
|
+
export const CF_PRESET_STYLES = [
|
|
26
|
+
{ id: 'light-red', label: 'Light Red Fill with Dark Red Text', style: { fill: '#FFC7CE', color: '#9C0006' } },
|
|
27
|
+
{ id: 'yellow', label: 'Yellow Fill with Dark Yellow Text', style: { fill: '#FFEB9C', color: '#9C5700' } },
|
|
28
|
+
{ id: 'green', label: 'Green Fill with Dark Green Text', style: { fill: '#C6EFCE', color: '#006100' } },
|
|
29
|
+
{ id: 'red-fill', label: 'Light Red Fill', style: { fill: '#FFC7CE' } },
|
|
30
|
+
{ id: 'red-text', label: 'Red Text', style: { color: '#9C0006' } },
|
|
31
|
+
];
|
|
32
|
+
export const DATA_BAR_COLOR = '#638EC6';
|
|
33
|
+
export const COLOR_SCALES = {
|
|
34
|
+
'green-yellow-red': ['#63BE7B', '#FFEB84', '#F8696B'],
|
|
35
|
+
'red-yellow-green': ['#F8696B', '#FFEB84', '#63BE7B'],
|
|
36
|
+
'green-white': ['#63BE7B', '#FCFCFF'],
|
|
37
|
+
'white-red': ['#FCFCFF', '#F8696B'],
|
|
38
|
+
};
|
|
39
|
+
let nextId = 1;
|
|
40
|
+
export function cfId() {
|
|
41
|
+
nextId += 1;
|
|
42
|
+
return `cf${Date.now().toString(36)}${nextId}`;
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Statistics
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/**
|
|
48
|
+
* The numbers and the texts a rule's rectangles hold. One pass; the shell
|
|
49
|
+
* caches the result per rule until the next repaint.
|
|
50
|
+
*/
|
|
51
|
+
export function ruleStats(rule, valueAt, displayAt) {
|
|
52
|
+
const numbers = [];
|
|
53
|
+
const counts = new Map();
|
|
54
|
+
let sum = 0;
|
|
55
|
+
for (const [r1, c1, r2, c2] of rule.rects) {
|
|
56
|
+
for (let r = r1; r <= r2; r += 1) {
|
|
57
|
+
for (let c = c1; c <= c2; c += 1) {
|
|
58
|
+
const v = valueAt(r, c);
|
|
59
|
+
if (typeof v === 'number' && Number.isFinite(v)) {
|
|
60
|
+
numbers.push(v);
|
|
61
|
+
sum += v;
|
|
62
|
+
}
|
|
63
|
+
if (rule.kind === 'duplicates') {
|
|
64
|
+
const shown = displayAt(r, c);
|
|
65
|
+
if (shown !== '')
|
|
66
|
+
counts.set(shown, (counts.get(shown) ?? 0) + 1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
numbers.sort((a, b) => a - b);
|
|
72
|
+
return {
|
|
73
|
+
sorted: numbers,
|
|
74
|
+
min: numbers[0] ?? 0,
|
|
75
|
+
max: numbers[numbers.length - 1] ?? 0,
|
|
76
|
+
mean: numbers.length ? sum / numbers.length : 0,
|
|
77
|
+
counts,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Evaluation
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
function numberOf(text, ctx) {
|
|
84
|
+
const v = ctx.evaluate(text);
|
|
85
|
+
if (isError(v) || typeof v === 'boolean')
|
|
86
|
+
return null;
|
|
87
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
88
|
+
return Number.isFinite(n) ? n : null;
|
|
89
|
+
}
|
|
90
|
+
function textOf(text, ctx) {
|
|
91
|
+
const t = text.trim();
|
|
92
|
+
if (!t.startsWith('='))
|
|
93
|
+
return t;
|
|
94
|
+
const v = ctx.evaluate(t);
|
|
95
|
+
return isError(v) ? '' : toText(v);
|
|
96
|
+
}
|
|
97
|
+
function cellIs(value, operator, a, b) {
|
|
98
|
+
if (a === null)
|
|
99
|
+
return false;
|
|
100
|
+
switch (operator) {
|
|
101
|
+
case 'greater': return value > a;
|
|
102
|
+
case 'less': return value < a;
|
|
103
|
+
case 'greaterOrEqual': return value >= a;
|
|
104
|
+
case 'lessOrEqual': return value <= a;
|
|
105
|
+
case 'equal': return value === a;
|
|
106
|
+
case 'notEqual': return value !== a;
|
|
107
|
+
case 'between': return b !== null && value >= Math.min(a, b) && value <= Math.max(a, b);
|
|
108
|
+
case 'notBetween': return b !== null && (value < Math.min(a, b) || value > Math.max(a, b));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Whether a styling rule matches the cell. */
|
|
112
|
+
function matches(rule, value, display, stats, ctx) {
|
|
113
|
+
switch (rule.kind) {
|
|
114
|
+
case 'cellIs': {
|
|
115
|
+
const isEquality = rule.operator === 'equal' || rule.operator === 'notEqual';
|
|
116
|
+
const a = numberOf(rule.value1, ctx);
|
|
117
|
+
if (typeof value === 'number' && (a !== null || !isEquality)) {
|
|
118
|
+
return cellIs(value, rule.operator, a, rule.value2 === undefined ? null : numberOf(rule.value2, ctx));
|
|
119
|
+
}
|
|
120
|
+
// Equal to / not equal to compare as text where either side is text,
|
|
121
|
+
// as Excel does: 5 is not equal to 'Paid', and 'paid' is equal to it.
|
|
122
|
+
if (!isEquality)
|
|
123
|
+
return false;
|
|
124
|
+
const same = display !== '' && display.toLowerCase() === textOf(rule.value1, ctx).toLowerCase();
|
|
125
|
+
return rule.operator === 'equal' ? same : display !== '' && !same;
|
|
126
|
+
}
|
|
127
|
+
case 'text': {
|
|
128
|
+
if (display === '')
|
|
129
|
+
return false;
|
|
130
|
+
const hay = display.toLowerCase();
|
|
131
|
+
const needle = textOf(rule.value, ctx).toLowerCase();
|
|
132
|
+
if (needle === '')
|
|
133
|
+
return false;
|
|
134
|
+
switch (rule.match) {
|
|
135
|
+
case 'contains': return hay.includes(needle);
|
|
136
|
+
case 'notContains': return !hay.includes(needle);
|
|
137
|
+
case 'beginsWith': return hay.startsWith(needle);
|
|
138
|
+
case 'endsWith': return hay.endsWith(needle);
|
|
139
|
+
default: return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
case 'duplicates': {
|
|
143
|
+
if (display === '')
|
|
144
|
+
return false;
|
|
145
|
+
const n = stats.counts.get(display) ?? 0;
|
|
146
|
+
return rule.unique ? n === 1 : n > 1;
|
|
147
|
+
}
|
|
148
|
+
case 'topBottom': {
|
|
149
|
+
if (typeof value !== 'number' || !stats.sorted.length)
|
|
150
|
+
return false;
|
|
151
|
+
const count = rule.percent
|
|
152
|
+
? Math.max(1, Math.round((stats.sorted.length * rule.rank) / 100))
|
|
153
|
+
: rule.rank;
|
|
154
|
+
if (count <= 0)
|
|
155
|
+
return false;
|
|
156
|
+
// The cut is the count-th value from the top (or bottom); ties keep
|
|
157
|
+
// every cell at that value in, as Excel's do.
|
|
158
|
+
const sorted = stats.sorted;
|
|
159
|
+
const cut = rule.top
|
|
160
|
+
? sorted[Math.max(sorted.length - count, 0)]
|
|
161
|
+
: sorted[Math.min(count - 1, sorted.length - 1)];
|
|
162
|
+
return rule.top ? value >= cut : value <= cut;
|
|
163
|
+
}
|
|
164
|
+
case 'average': {
|
|
165
|
+
if (typeof value !== 'number' || !stats.sorted.length)
|
|
166
|
+
return false;
|
|
167
|
+
return rule.above ? value > stats.mean : value < stats.mean;
|
|
168
|
+
}
|
|
169
|
+
default:
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A data bar's share of the cell, 0..1. Excel measures from zero when the
|
|
175
|
+
* range is non-negative (10, 20, 40 draw a quarter, a half and the whole
|
|
176
|
+
* width), so the smallest value keeps a bar; a range with negatives runs
|
|
177
|
+
* from its minimum instead, since there is no negative axis here.
|
|
178
|
+
*/
|
|
179
|
+
function barRatio(value, stats) {
|
|
180
|
+
const lo = Math.min(0, stats.min);
|
|
181
|
+
const hi = Math.max(0, stats.max);
|
|
182
|
+
if (hi === lo)
|
|
183
|
+
return 0;
|
|
184
|
+
return Math.min(1, Math.max(0, (value - lo) / (hi - lo)));
|
|
185
|
+
}
|
|
186
|
+
/** Where a number sits in a range, 0..1; 0.5 for a flat range. */
|
|
187
|
+
function ratioOf(value, stats) {
|
|
188
|
+
if (stats.max === stats.min)
|
|
189
|
+
return 0.5;
|
|
190
|
+
return Math.min(1, Math.max(0, (value - stats.min) / (stats.max - stats.min)));
|
|
191
|
+
}
|
|
192
|
+
/** Linear interpolate two hex colours. */
|
|
193
|
+
function lerp(a, b, t) {
|
|
194
|
+
const pa = parseHex(a);
|
|
195
|
+
const pb = parseHex(b);
|
|
196
|
+
if (!pa || !pb)
|
|
197
|
+
return a;
|
|
198
|
+
const k = Math.min(1, Math.max(0, t));
|
|
199
|
+
const ch = (i) => Math.round(pa[i] + (pb[i] - pa[i]) * k).toString(16).padStart(2, '0');
|
|
200
|
+
return `#${ch(0)}${ch(1)}${ch(2)}`;
|
|
201
|
+
}
|
|
202
|
+
function parseHex(hex) {
|
|
203
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
204
|
+
if (!m)
|
|
205
|
+
return null;
|
|
206
|
+
const n = parseInt(m[1], 16);
|
|
207
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
208
|
+
}
|
|
209
|
+
/** The colour a scale gives a position 0..1. */
|
|
210
|
+
export function scaleColor(colors, t) {
|
|
211
|
+
if (colors.length === 2)
|
|
212
|
+
return lerp(colors[0], colors[1], t);
|
|
213
|
+
return t <= 0.5 ? lerp(colors[0], colors[1], t * 2) : lerp(colors[1], colors[2], (t - 0.5) * 2);
|
|
214
|
+
}
|
|
215
|
+
/** Which of an icon set's three icons a position 0..1 gets: Excel's thirds. */
|
|
216
|
+
export function iconIndex(t) {
|
|
217
|
+
return t >= 2 / 3 ? 0 : t >= 1 / 3 ? 1 : 2;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* What the rules say about (r, c). Array order is priority; the first rule
|
|
221
|
+
* that decides a property (the fill, the text colour, the bar, the icon)
|
|
222
|
+
* keeps it; a matching rule with Stop If True ends the walk.
|
|
223
|
+
*/
|
|
224
|
+
export function evaluateCf(rules, r, c, value, display, statsFor, ctx) {
|
|
225
|
+
let out = null;
|
|
226
|
+
for (const rule of rules) {
|
|
227
|
+
if (!rule.rects.some((rect) => rectContains(rect, r, c)))
|
|
228
|
+
continue;
|
|
229
|
+
let hit = false;
|
|
230
|
+
if (rule.kind === 'dataBar') {
|
|
231
|
+
if (typeof value === 'number' && !(out?.dataBar)) {
|
|
232
|
+
const stats = statsFor(rule);
|
|
233
|
+
out = { ...(out ?? {}), dataBar: { ratio: barRatio(value, stats), color: rule.color } };
|
|
234
|
+
hit = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (rule.kind === 'colorScale') {
|
|
238
|
+
const current = out ?? {};
|
|
239
|
+
if (typeof value === 'number' && current.style?.fill === undefined) {
|
|
240
|
+
const stats = statsFor(rule);
|
|
241
|
+
out = { ...current, style: { ...(current.style ?? {}), fill: scaleColor(rule.colors, ratioOf(value, stats)) } };
|
|
242
|
+
hit = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else if (rule.kind === 'iconSet') {
|
|
246
|
+
if (typeof value === 'number' && !(out?.icon)) {
|
|
247
|
+
const stats = statsFor(rule);
|
|
248
|
+
out = { ...(out ?? {}), icon: { set: rule.set, index: iconIndex(ratioOf(value, stats)) } };
|
|
249
|
+
hit = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else if (hasStyle(rule) && matches(rule, value, display, statsFor(rule), ctx)) {
|
|
253
|
+
const style = { ...(out?.style ?? {}) };
|
|
254
|
+
const own = rule.style;
|
|
255
|
+
for (const key of Object.keys(own)) {
|
|
256
|
+
if (style[key] === undefined && own[key] !== undefined)
|
|
257
|
+
style[key] = own[key];
|
|
258
|
+
}
|
|
259
|
+
out = { ...(out ?? {}), style };
|
|
260
|
+
hit = true;
|
|
261
|
+
}
|
|
262
|
+
if (hit && rule.stopIfTrue)
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Bookkeeping
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
/** The rules after an insert or delete. */
|
|
271
|
+
export function shiftCf(rules, edit) {
|
|
272
|
+
return shiftRects(rules, edit);
|
|
273
|
+
}
|
|
274
|
+
/** The rules with the rectangles cut out of them: Clear Rules from Selected Cells. */
|
|
275
|
+
export function removeCf(rules, rects) {
|
|
276
|
+
const out = [];
|
|
277
|
+
for (const rule of rules) {
|
|
278
|
+
let kept = [...rule.rects];
|
|
279
|
+
for (const hole of rects)
|
|
280
|
+
kept = kept.flatMap((rect) => subtractRect(rect, hole));
|
|
281
|
+
if (kept.length)
|
|
282
|
+
out.push({ ...rule, rects: kept });
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/** The rules touching any of the rectangles. */
|
|
287
|
+
export function cfIn(rules, rects) {
|
|
288
|
+
return rules.filter((rule) => rule.rects.some((a) => rects.some((b) => rectsIntersect(a, b))));
|
|
289
|
+
}
|
|
290
|
+
const OPERATOR_WORDS = {
|
|
291
|
+
greater: 'greater than', less: 'less than', between: 'between', notBetween: 'not between',
|
|
292
|
+
equal: 'equal to', notEqual: 'not equal to', greaterOrEqual: 'greater than or equal to', lessOrEqual: 'less than or equal to',
|
|
293
|
+
};
|
|
294
|
+
const MATCH_WORDS = {
|
|
295
|
+
contains: 'contains', notContains: 'does not contain', beginsWith: 'begins with', endsWith: 'ends with',
|
|
296
|
+
};
|
|
297
|
+
/** A rule in words, the way Excel's Manage Rules lists it. */
|
|
298
|
+
export function describeCf(rule) {
|
|
299
|
+
switch (rule.kind) {
|
|
300
|
+
case 'cellIs':
|
|
301
|
+
return rule.operator === 'between' || rule.operator === 'notBetween'
|
|
302
|
+
? `Cell Value ${OPERATOR_WORDS[rule.operator]} ${rule.value1} and ${rule.value2 ?? ''}`
|
|
303
|
+
: `Cell Value ${OPERATOR_WORDS[rule.operator]} ${rule.value1}`;
|
|
304
|
+
case 'text': return `Cell Value ${MATCH_WORDS[rule.match]} '${rule.value}'`;
|
|
305
|
+
case 'duplicates': return rule.unique ? 'Unique Values' : 'Duplicate Values';
|
|
306
|
+
case 'topBottom': return `${rule.top ? 'Top' : 'Bottom'} ${rule.rank}${rule.percent ? '%' : ''}`;
|
|
307
|
+
case 'average': return rule.above ? 'Above Average' : 'Below Average';
|
|
308
|
+
case 'dataBar': return 'Data Bar';
|
|
309
|
+
case 'colorScale': return rule.colors.length === 3 ? 'Graded Color Scale' : 'Two-Color Scale';
|
|
310
|
+
case 'iconSet': return 'Icon Set';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** Whether a rule carries a style the dialog can edit. */
|
|
314
|
+
export function hasStyle(rule) {
|
|
315
|
+
return 'style' in rule;
|
|
316
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency graph, so a recalc costs what the edit actually affects.
|
|
3
|
+
*
|
|
4
|
+
* The demo engines call `computeSheet`, which resolves every cell on every
|
|
5
|
+
* keystroke. On a small budget sheet nobody notices; on demo 211's 360-row
|
|
6
|
+
* amortisation schedule, typing one digit in an input cell walks every cell in
|
|
7
|
+
* the sheet and re-parses every formula among them.
|
|
8
|
+
*
|
|
9
|
+
* This tracks which cells each formula reads, inverts that into "who reads
|
|
10
|
+
* me", and walks forward from an edit in topological order.
|
|
11
|
+
*
|
|
12
|
+
* Cycles are reported rather than thrown. A cycle is a normal thing to type on
|
|
13
|
+
* the way to something correct, and every cell in one shows #CYCLE! while the
|
|
14
|
+
* rest of the sheet keeps working.
|
|
15
|
+
*/
|
|
16
|
+
import { type Node } from './ast.js';
|
|
17
|
+
export type CellKey = string;
|
|
18
|
+
/**
|
|
19
|
+
* Cells are keyed "row col sheet", in that order deliberately.
|
|
20
|
+
*
|
|
21
|
+
* Row and column are numbers and contain no separator, so the first two fields
|
|
22
|
+
* are unambiguous and EVERYTHING after the second space is the sheet name.
|
|
23
|
+
* Putting the sheet first would let a name like `Price list` collide with a
|
|
24
|
+
* different cell, and a ':' or '!' separator has the same problem the moment
|
|
25
|
+
* someone names a sheet after a range.
|
|
26
|
+
*/
|
|
27
|
+
export declare function cellKey(sheet: string | null, row: number, col: number): CellKey;
|
|
28
|
+
export declare function parseCellKey(key: CellKey): {
|
|
29
|
+
sheet: string | null;
|
|
30
|
+
row: number;
|
|
31
|
+
col: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Every cell a formula reads. A range contributes each cell inside it, which
|
|
35
|
+
* is why `lastRow` is needed: a whole-column reference has no upper bound of
|
|
36
|
+
* its own.
|
|
37
|
+
*/
|
|
38
|
+
export declare function precedentsOf(ast: Node, self: {
|
|
39
|
+
sheet: string | null;
|
|
40
|
+
}, lastRow: (sheet: string | null) => number,
|
|
41
|
+
/**
|
|
42
|
+
* The reference behind a defined name. Without it a formula that reads
|
|
43
|
+
* `=Subtotal*Tax` records no precedents at all, so editing the cell behind
|
|
44
|
+
* `Tax` leaves the formula showing its old value until something else
|
|
45
|
+
* forces a recalculation.
|
|
46
|
+
*/
|
|
47
|
+
resolveName?: (name: string) => Node | null, depth?: number): CellKey[];
|
|
48
|
+
export type DependencyGraph = {
|
|
49
|
+
/** Record (or clear, with null) what one cell reads. */
|
|
50
|
+
setPrecedents(cell: CellKey, precedents: ReadonlyArray<CellKey> | null): void;
|
|
51
|
+
/** Cells that must recompute after `changed`, ordered so every cell comes
|
|
52
|
+
* after everything it reads. Excludes the changed cells themselves. */
|
|
53
|
+
dirtyFrom(changed: ReadonlyArray<CellKey>): CellKey[];
|
|
54
|
+
/** Every cell taking part in a dependency cycle. */
|
|
55
|
+
cycles(): CellKey[];
|
|
56
|
+
/** Direct readers of a cell. */
|
|
57
|
+
dependentsOf(cell: CellKey): CellKey[];
|
|
58
|
+
clear(): void;
|
|
59
|
+
};
|
|
60
|
+
export declare function createDependencyGraph(): DependencyGraph;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency graph, so a recalc costs what the edit actually affects.
|
|
3
|
+
*
|
|
4
|
+
* The demo engines call `computeSheet`, which resolves every cell on every
|
|
5
|
+
* keystroke. On a small budget sheet nobody notices; on demo 211's 360-row
|
|
6
|
+
* amortisation schedule, typing one digit in an input cell walks every cell in
|
|
7
|
+
* the sheet and re-parses every formula among them.
|
|
8
|
+
*
|
|
9
|
+
* This tracks which cells each formula reads, inverts that into "who reads
|
|
10
|
+
* me", and walks forward from an edit in topological order.
|
|
11
|
+
*
|
|
12
|
+
* Cycles are reported rather than thrown. A cycle is a normal thing to type on
|
|
13
|
+
* the way to something correct, and every cell in one shows #CYCLE! while the
|
|
14
|
+
* rest of the sheet keeps working.
|
|
15
|
+
*/
|
|
16
|
+
import { visit } from './ast.js';
|
|
17
|
+
/**
|
|
18
|
+
* Cells are keyed "row col sheet", in that order deliberately.
|
|
19
|
+
*
|
|
20
|
+
* Row and column are numbers and contain no separator, so the first two fields
|
|
21
|
+
* are unambiguous and EVERYTHING after the second space is the sheet name.
|
|
22
|
+
* Putting the sheet first would let a name like `Price list` collide with a
|
|
23
|
+
* different cell, and a ':' or '!' separator has the same problem the moment
|
|
24
|
+
* someone names a sheet after a range.
|
|
25
|
+
*/
|
|
26
|
+
export function cellKey(sheet, row, col) {
|
|
27
|
+
return `${row} ${col} ${sheet ?? ''}`;
|
|
28
|
+
}
|
|
29
|
+
export function parseCellKey(key) {
|
|
30
|
+
const firstGap = key.indexOf(' ');
|
|
31
|
+
const secondGap = key.indexOf(' ', firstGap + 1);
|
|
32
|
+
const sheet = key.slice(secondGap + 1);
|
|
33
|
+
return {
|
|
34
|
+
sheet: sheet === '' ? null : sheet,
|
|
35
|
+
row: Number(key.slice(0, firstGap)),
|
|
36
|
+
col: Number(key.slice(firstGap + 1, secondGap)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Every cell a formula reads. A range contributes each cell inside it, which
|
|
41
|
+
* is why `lastRow` is needed: a whole-column reference has no upper bound of
|
|
42
|
+
* its own.
|
|
43
|
+
*/
|
|
44
|
+
export function precedentsOf(ast, self, lastRow,
|
|
45
|
+
/**
|
|
46
|
+
* The reference behind a defined name. Without it a formula that reads
|
|
47
|
+
* `=Subtotal*Tax` records no precedents at all, so editing the cell behind
|
|
48
|
+
* `Tax` leaves the formula showing its old value until something else
|
|
49
|
+
* forces a recalculation.
|
|
50
|
+
*/
|
|
51
|
+
resolveName, depth = 0) {
|
|
52
|
+
const out = [];
|
|
53
|
+
visit(ast, (n) => {
|
|
54
|
+
if (n.k === 'name') {
|
|
55
|
+
// A name defined as another name is followed; a circular chain is cut
|
|
56
|
+
// after a few hops rather than recursing forever.
|
|
57
|
+
const target = depth < 8 ? resolveName?.(n.name) : null;
|
|
58
|
+
if (target)
|
|
59
|
+
out.push(...precedentsOf(target, self, lastRow, resolveName, depth + 1));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (n.k === 'ref' && n.ref.row !== null) {
|
|
63
|
+
out.push(cellKey(n.ref.sheet ?? self.sheet, n.ref.row, n.ref.col));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (n.k === 'range') {
|
|
67
|
+
const sheet = n.from.sheet ?? n.to.sheet ?? self.sheet;
|
|
68
|
+
const r1 = Math.min(n.from.row ?? 0, n.to.row ?? n.from.row ?? 0);
|
|
69
|
+
const r2 = n.to.row === null || n.from.row === null
|
|
70
|
+
? lastRow(sheet)
|
|
71
|
+
: Math.max(n.from.row, n.to.row);
|
|
72
|
+
const c1 = Math.min(n.from.col, n.to.col);
|
|
73
|
+
const c2 = Math.max(n.from.col, n.to.col);
|
|
74
|
+
for (let r = r1; r <= r2; r += 1) {
|
|
75
|
+
for (let c = c1; c <= c2; c += 1)
|
|
76
|
+
out.push(cellKey(sheet, r, c));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
export function createDependencyGraph() {
|
|
83
|
+
/** cell -> what it reads. */
|
|
84
|
+
const precedents = new Map();
|
|
85
|
+
/** cell -> who reads it. The index that makes dirtyFrom cheap. */
|
|
86
|
+
const dependents = new Map();
|
|
87
|
+
function unlink(cell) {
|
|
88
|
+
const old = precedents.get(cell);
|
|
89
|
+
if (!old)
|
|
90
|
+
return;
|
|
91
|
+
for (const p of old) {
|
|
92
|
+
const set = dependents.get(p);
|
|
93
|
+
if (!set)
|
|
94
|
+
continue;
|
|
95
|
+
set.delete(cell);
|
|
96
|
+
if (set.size === 0)
|
|
97
|
+
dependents.delete(p);
|
|
98
|
+
}
|
|
99
|
+
precedents.delete(cell);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
setPrecedents(cell, next) {
|
|
103
|
+
unlink(cell);
|
|
104
|
+
if (!next || next.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
// A self-reference is a cycle of one; it is kept so `cycles()` sees it.
|
|
107
|
+
const set = new Set(next);
|
|
108
|
+
precedents.set(cell, set);
|
|
109
|
+
for (const p of set) {
|
|
110
|
+
let back = dependents.get(p);
|
|
111
|
+
if (!back) {
|
|
112
|
+
back = new Set();
|
|
113
|
+
dependents.set(p, back);
|
|
114
|
+
}
|
|
115
|
+
back.add(cell);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
dirtyFrom(changed) {
|
|
119
|
+
// Collect everything reachable downstream, then order it so a cell only
|
|
120
|
+
// recomputes once its own inputs have. Kahn's algorithm over that
|
|
121
|
+
// subgraph, not over the whole sheet.
|
|
122
|
+
const affected = new Set();
|
|
123
|
+
const stack = [...changed];
|
|
124
|
+
while (stack.length) {
|
|
125
|
+
const cur = stack.pop();
|
|
126
|
+
for (const d of dependents.get(cur) ?? []) {
|
|
127
|
+
if (affected.has(d))
|
|
128
|
+
continue;
|
|
129
|
+
affected.add(d);
|
|
130
|
+
stack.push(d);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (affected.size === 0)
|
|
134
|
+
return [];
|
|
135
|
+
const indegree = new Map();
|
|
136
|
+
for (const cell of affected) {
|
|
137
|
+
let n = 0;
|
|
138
|
+
for (const p of precedents.get(cell) ?? [])
|
|
139
|
+
if (affected.has(p))
|
|
140
|
+
n += 1;
|
|
141
|
+
indegree.set(cell, n);
|
|
142
|
+
}
|
|
143
|
+
const ready = [...affected].filter((c) => (indegree.get(c) ?? 0) === 0);
|
|
144
|
+
const order = [];
|
|
145
|
+
const placed = new Set();
|
|
146
|
+
while (ready.length) {
|
|
147
|
+
const cur = ready.shift();
|
|
148
|
+
order.push(cur);
|
|
149
|
+
placed.add(cur);
|
|
150
|
+
for (const d of dependents.get(cur) ?? []) {
|
|
151
|
+
if (!affected.has(d))
|
|
152
|
+
continue;
|
|
153
|
+
const n = (indegree.get(d) ?? 0) - 1;
|
|
154
|
+
indegree.set(d, n);
|
|
155
|
+
if (n === 0)
|
|
156
|
+
ready.push(d);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Anything left never reached indegree zero, so it sits in a cycle.
|
|
160
|
+
// Append it rather than dropping it: those cells still need evaluating
|
|
161
|
+
// so they can report #CYCLE! instead of keeping a stale value.
|
|
162
|
+
for (const cell of affected)
|
|
163
|
+
if (!placed.has(cell))
|
|
164
|
+
order.push(cell);
|
|
165
|
+
return order;
|
|
166
|
+
},
|
|
167
|
+
cycles() {
|
|
168
|
+
// A cell is in a cycle when it can reach itself through its precedents.
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const start of precedents.keys()) {
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
const stack = [...(precedents.get(start) ?? [])];
|
|
173
|
+
let found = false;
|
|
174
|
+
while (stack.length) {
|
|
175
|
+
const cur = stack.pop();
|
|
176
|
+
if (cur === start) {
|
|
177
|
+
found = true;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (seen.has(cur))
|
|
181
|
+
continue;
|
|
182
|
+
seen.add(cur);
|
|
183
|
+
for (const p of precedents.get(cur) ?? [])
|
|
184
|
+
stack.push(p);
|
|
185
|
+
}
|
|
186
|
+
if (found)
|
|
187
|
+
out.push(start);
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
},
|
|
191
|
+
dependentsOf(cell) {
|
|
192
|
+
return [...(dependents.get(cell) ?? [])];
|
|
193
|
+
},
|
|
194
|
+
clear() {
|
|
195
|
+
precedents.clear();
|
|
196
|
+
dependents.clear();
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|