@peter.prib/renderengine 0.13.0 → 0.15.0
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/README.md +480 -302
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49217 -37484
- package/dist/renderengine.umd.js +24899 -13093
- package/documentation/REFERENCE.md +68 -1
- package/package.json +13 -3
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +269 -3
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +7 -2
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +142 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/ask/AskBar.js +388 -0
- package/src/charting/ask/AskView.js +400 -0
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +69 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +6 -4
- package/src/charting/charts/BarChart3DOrbit.js +3 -1
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +25 -3
- package/src/charting/charts/BubbleChart.js +4 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +107 -4
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DensityChart.js +1 -1
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +5 -2
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +6 -3
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HistogramChart.js +2 -2
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/IcicleChart.js +47 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/LollipopChart.js +104 -0
- package/src/charting/charts/MarimekkoChart.js +75 -0
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/OutlineChart.js +10 -8
- package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
- package/src/charting/charts/ParetoChart.js +92 -0
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PipeChart.js +5 -2
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/PopulationPyramidChart.js +4 -4
- package/src/charting/charts/RadarChart.js +12 -5
- package/src/charting/charts/RadialBarChart.js +6 -3
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +129 -6
- package/src/charting/charts/ScatterChart.js +19 -3
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +125 -11
- package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
- package/src/charting/charts/StreamGraphChart.js +18 -3
- package/src/charting/charts/SunburstChart.js +28 -8
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +31 -11
- package/src/charting/charts/ViolinChart.js +20 -2
- package/src/charting/charts/WaterfallChart.js +2 -2
- package/src/charting/charts/WordCloudChart.js +2 -2
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +310 -13
- package/src/charting/pivot/PivotGridChart.js +322 -34
- package/src/charting/pivot/PivotView.js +626 -21
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2899
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/styles.js +36 -1
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AskView — the state and next-word prediction model for an interactive "Ask" bar
|
|
3
|
+
* (question input, scrollable predictive typeahead list with semantic sense/reference/relation
|
|
4
|
+
* badges, keyboard navigation, and configurable next-word prediction), kept apart from DOM
|
|
5
|
+
* rendering so all of it can be unit-tested under Node.js without a browser.
|
|
6
|
+
*
|
|
7
|
+
* A host can supply its own `suggest(text, view)` callback (for example, semanticengine's
|
|
8
|
+
* `(text) => engine.suggest(text, { sampler: (dim) => engine.members(dim) })`), or pass
|
|
9
|
+
* declarative vocabulary hints (`measures`, `dimensions`, `members`, `vocabulary`,
|
|
10
|
+
* `sampleQuestions`) to use `AskView.nextWordPredictor(...)`.
|
|
11
|
+
*/
|
|
12
|
+
export class AskView {
|
|
13
|
+
/** Default choices for how many predicted next-word items are visible at once in the scrollable list (`0` = Off). */
|
|
14
|
+
static DEFAULT_PREDICT_CHOICES = [0, 3, 5, 8, 10];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} [options]
|
|
18
|
+
* @param {string} [options.value=''] initial question text
|
|
19
|
+
* @param {function} [options.suggest] `(text, view) => Array<string|{display, completion?, sense?, reference?, relation?, kind?}>`, or a promise of one
|
|
20
|
+
* @param {string[]} [options.measures] measure names for built-in next-word prediction
|
|
21
|
+
* @param {string[]} [options.dimensions] dimension names for built-in next-word prediction
|
|
22
|
+
* @param {function|object} [options.members] `(dimension) => Array<string|number>` or `{ dim: [...] }` for value prediction after `=`
|
|
23
|
+
* @param {Array<string|{display, completion?, sense?, reference?, relation?, kind?}>} [options.vocabulary] custom vocabulary entries
|
|
24
|
+
* @param {string[]} [options.sampleQuestions] sample questions used both as clickable pills and to learn next-word transitions
|
|
25
|
+
* @param {boolean} [options.predict=true] whether next-word prediction is initially enabled
|
|
26
|
+
* @param {number} [options.visibleCount=5] how many items are visible at once in the scrollable suggestion viewport
|
|
27
|
+
* @param {number[]} [options.predictChoices=[0, 3, 5, 8, 10]] configurable choices for the predict-next-word selector (`0` = Off)
|
|
28
|
+
* @param {number} [options.maxSuggestions=50] maximum number of suggestions kept in the scrollable list
|
|
29
|
+
* @param {function} [options.onAsk] `(question, view) => *` called when the user submits the question
|
|
30
|
+
* @param {function} [options.onComplete] `(item, view) => void` called when a suggestion is accepted
|
|
31
|
+
* @param {function} [options.onChange] `(view) => void` called whenever view state changes
|
|
32
|
+
*/
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.value = options.value ?? '';
|
|
35
|
+
this.samples = [...(options.sampleQuestions ?? options.samples ?? [])];
|
|
36
|
+
this.predictChoices = Array.isArray(options.predictChoices) && options.predictChoices.length > 0
|
|
37
|
+
? [...options.predictChoices]
|
|
38
|
+
: [...AskView.DEFAULT_PREDICT_CHOICES];
|
|
39
|
+
const initialVisible = Number.isFinite(options.visibleCount) && options.visibleCount > 0 ? Math.floor(options.visibleCount) : 5;
|
|
40
|
+
this.visibleCount = initialVisible;
|
|
41
|
+
this.predict = options.predict !== false && options.visibleCount !== 0;
|
|
42
|
+
if (this.predict && !this.predictChoices.includes(this.visibleCount)) {
|
|
43
|
+
this.predictChoices = [...new Set([...this.predictChoices, this.visibleCount])].sort((a, b) => a - b);
|
|
44
|
+
}
|
|
45
|
+
this.maxSuggestions = Number.isFinite(options.maxSuggestions) && options.maxSuggestions > 0
|
|
46
|
+
? Math.floor(options.maxSuggestions)
|
|
47
|
+
: 50;
|
|
48
|
+
this.suggestFn = typeof options.suggest === 'function'
|
|
49
|
+
? options.suggest
|
|
50
|
+
: AskView.nextWordPredictor({
|
|
51
|
+
measures: options.measures,
|
|
52
|
+
dimensions: options.dimensions,
|
|
53
|
+
members: options.members,
|
|
54
|
+
vocabulary: options.vocabulary,
|
|
55
|
+
sampleQuestions: this.samples,
|
|
56
|
+
});
|
|
57
|
+
this.onAsk = options.onAsk ?? null;
|
|
58
|
+
this.onComplete = options.onComplete ?? null;
|
|
59
|
+
this.onChange = options.onChange ?? null;
|
|
60
|
+
this.items = [];
|
|
61
|
+
this.selectedIndex = -1;
|
|
62
|
+
this.open = false;
|
|
63
|
+
this.history = [];
|
|
64
|
+
this._ticket = 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Current predict-next-word setting as a single number (`0` when Off, else `visibleCount`),
|
|
69
|
+
* matching `predictChoices`.
|
|
70
|
+
*/
|
|
71
|
+
get predictCount() {
|
|
72
|
+
return this.predict ? this.visibleCount : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Enables or disables next-word prediction, or sets how many items show at once in the
|
|
77
|
+
* scrollable list (`0` or `false` turns prediction off; a positive integer enables it with
|
|
78
|
+
* that viewport size).
|
|
79
|
+
*/
|
|
80
|
+
setPredictCount(countOrEnabled) {
|
|
81
|
+
if (countOrEnabled === false || countOrEnabled === 0 || countOrEnabled === '0' || countOrEnabled === 'off') {
|
|
82
|
+
this.predict = false;
|
|
83
|
+
this.items = [];
|
|
84
|
+
this.selectedIndex = -1;
|
|
85
|
+
this.open = false;
|
|
86
|
+
this._changed();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (countOrEnabled === true) {
|
|
90
|
+
this.predict = true;
|
|
91
|
+
if (this.visibleCount <= 0) this.visibleCount = 5;
|
|
92
|
+
} else {
|
|
93
|
+
const n = Number(countOrEnabled);
|
|
94
|
+
if (Number.isFinite(n) && n > 0) {
|
|
95
|
+
this.predict = true;
|
|
96
|
+
this.visibleCount = Math.floor(n);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.refresh();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Updates the input text and refreshes next-word predictions if enabled. */
|
|
103
|
+
setInput(text, { suggest = true } = {}) {
|
|
104
|
+
this.value = String(text ?? '');
|
|
105
|
+
if (suggest && this.predict) {
|
|
106
|
+
return this.refresh();
|
|
107
|
+
} else {
|
|
108
|
+
if (!suggest) {
|
|
109
|
+
this.open = false;
|
|
110
|
+
this.selectedIndex = -1;
|
|
111
|
+
}
|
|
112
|
+
this._changed();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Swaps the source of suggestions (a host whose vocabulary changed — another model or
|
|
118
|
+
* ontology) and refreshes the list.
|
|
119
|
+
*/
|
|
120
|
+
setSuggest(suggest) {
|
|
121
|
+
this.suggestFn = typeof suggest === 'function' ? suggest : null;
|
|
122
|
+
this.refresh();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Recomputes suggestions for the current `value` and opens the list if any match. Nothing is
|
|
127
|
+
* selected until the user moves into the list, so Enter asks the question as typed.
|
|
128
|
+
*
|
|
129
|
+
* `suggest` may answer with a promise (an engine still loading, a server's vocabulary): the
|
|
130
|
+
* list keeps what it shows until it settles, and an answer for text since changed is dropped.
|
|
131
|
+
* Returns the items, or a promise of them.
|
|
132
|
+
*/
|
|
133
|
+
refresh() {
|
|
134
|
+
const ticket = ++this._ticket;
|
|
135
|
+
if (!this.predict || !this.suggestFn) {
|
|
136
|
+
this._apply([]);
|
|
137
|
+
return this.items;
|
|
138
|
+
}
|
|
139
|
+
let raw;
|
|
140
|
+
try {
|
|
141
|
+
raw = this.suggestFn(this.value, this);
|
|
142
|
+
} catch {
|
|
143
|
+
this._apply([]);
|
|
144
|
+
return this.items;
|
|
145
|
+
}
|
|
146
|
+
if (raw && typeof raw.then === 'function') {
|
|
147
|
+
const asked = this.value;
|
|
148
|
+
return Promise.resolve(raw).then(
|
|
149
|
+
(items) => (ticket === this._ticket && asked === this.value ? this._apply(items) : this.items),
|
|
150
|
+
() => (ticket === this._ticket ? this._apply([]) : this.items),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return this._apply(raw);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Shows `raw` suggestions (anything not a list shows none). */
|
|
157
|
+
_apply(raw) {
|
|
158
|
+
this.items = Array.isArray(raw)
|
|
159
|
+
? raw.slice(0, this.maxSuggestions).map((item) => AskView.normalizeItem(item, this.value))
|
|
160
|
+
: [];
|
|
161
|
+
this.selectedIndex = -1;
|
|
162
|
+
this.open = this.items.length > 0;
|
|
163
|
+
this._changed();
|
|
164
|
+
return this.items;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Moves the active highlight up (`-1`) or down (`+1`) in the scrollable list, wrapping around.
|
|
169
|
+
* From no selection, down goes to the first item and up to the last.
|
|
170
|
+
*/
|
|
171
|
+
move(delta) {
|
|
172
|
+
if (!this.open || this.items.length === 0) {
|
|
173
|
+
this.refresh();
|
|
174
|
+
return this.selectedIndex;
|
|
175
|
+
}
|
|
176
|
+
const len = this.items.length;
|
|
177
|
+
this.selectedIndex = this.selectedIndex < 0
|
|
178
|
+
? (delta > 0 ? 0 : len - 1)
|
|
179
|
+
: (this.selectedIndex + delta + len) % len;
|
|
180
|
+
this._changed();
|
|
181
|
+
return this.selectedIndex;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Applies the suggestion at `index` (defaults to `selectedIndex`, or `0`), replacing `value`
|
|
186
|
+
* with `item.completion` and immediately predicting the next word.
|
|
187
|
+
*/
|
|
188
|
+
accept(index = this.selectedIndex >= 0 ? this.selectedIndex : 0) {
|
|
189
|
+
if (index < 0 || index >= this.items.length) return null;
|
|
190
|
+
const item = this.items[index];
|
|
191
|
+
this.value = item.completion;
|
|
192
|
+
this.onComplete?.(item, this);
|
|
193
|
+
if (this.predict) {
|
|
194
|
+
this.refresh();
|
|
195
|
+
} else {
|
|
196
|
+
this.close();
|
|
197
|
+
}
|
|
198
|
+
return item;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Closes the suggestion popup without changing `value`. */
|
|
202
|
+
close() {
|
|
203
|
+
if (!this.open && this.selectedIndex === -1) return;
|
|
204
|
+
this.open = false;
|
|
205
|
+
this.selectedIndex = -1;
|
|
206
|
+
this._changed();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Submits the current question (`value`), closes the suggestion list, and invokes `onAsk`. */
|
|
210
|
+
submit(question = this.value) {
|
|
211
|
+
this.value = String(question ?? '');
|
|
212
|
+
this.open = false;
|
|
213
|
+
this.selectedIndex = -1;
|
|
214
|
+
const trimmed = this.value.trim();
|
|
215
|
+
if (trimmed && this.history.at(-1) !== trimmed) {
|
|
216
|
+
this.history.push(trimmed);
|
|
217
|
+
}
|
|
218
|
+
this._changed();
|
|
219
|
+
return this.onAsk?.(this.value, this);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_changed() {
|
|
223
|
+
this.onChange?.(this);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Normalizes a string or suggestion object into `{ display, completion, sense, reference, relation, kind }`. */
|
|
227
|
+
static normalizeItem(item, currentText = '') {
|
|
228
|
+
if (typeof item === 'string') {
|
|
229
|
+
return {
|
|
230
|
+
display: item,
|
|
231
|
+
completion: AskView._completeWord(currentText, item),
|
|
232
|
+
sense: null,
|
|
233
|
+
reference: null,
|
|
234
|
+
relation: null,
|
|
235
|
+
kind: 'suggestion',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const display = String(item.display ?? item.word ?? item.label ?? item.completion ?? '');
|
|
239
|
+
const completion = item.completion !== undefined
|
|
240
|
+
? String(item.completion)
|
|
241
|
+
: AskView._completeWord(currentText, display);
|
|
242
|
+
return {
|
|
243
|
+
display,
|
|
244
|
+
completion,
|
|
245
|
+
sense: item.sense ?? null,
|
|
246
|
+
reference: item.reference ?? null,
|
|
247
|
+
relation: item.relation ?? null,
|
|
248
|
+
kind: item.kind ?? 'suggestion',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Replaces the trailing partial token of `currentText` with `word` (followed by a space). */
|
|
253
|
+
static _completeWord(currentText, word) {
|
|
254
|
+
const text = String(currentText ?? '');
|
|
255
|
+
if (word.includes(' ') && word.toLowerCase().startsWith(text.trim().toLowerCase()) && text.trim().length > 0) {
|
|
256
|
+
return word;
|
|
257
|
+
}
|
|
258
|
+
const match = text.match(/^(.*?)(\S*)$/);
|
|
259
|
+
const prefix = match ? match[1] : '';
|
|
260
|
+
const cleanWord = String(word).trim();
|
|
261
|
+
return `${prefix}${cleanWord} `;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Builds a context-aware next-word predictor from declarative cube/table metadata and/or
|
|
266
|
+
* sample questions. Predicts metrics, dimensions, clauses (`by`, `and`, `where`, `top`, `vs`),
|
|
267
|
+
* sampled dimension members after `=`, and learned next-word bigrams from `sampleQuestions`.
|
|
268
|
+
*/
|
|
269
|
+
static nextWordPredictor({
|
|
270
|
+
measures = [],
|
|
271
|
+
dimensions = [],
|
|
272
|
+
members = null,
|
|
273
|
+
vocabulary = [],
|
|
274
|
+
sampleQuestions = [],
|
|
275
|
+
} = {}) {
|
|
276
|
+
const catalog = [];
|
|
277
|
+
const addEntry = (display, kind, sense = null, reference = null, relation = null) => {
|
|
278
|
+
if (!display) return;
|
|
279
|
+
if (catalog.some(c => c.display.toLowerCase() === String(display).toLowerCase() && c.kind === kind)) return;
|
|
280
|
+
catalog.push({ display: String(display), kind, sense, reference, relation });
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
for (const m of measures) addEntry(m, 'metric', `Measure ${m}`, m, 'canonical');
|
|
284
|
+
for (const d of dimensions) addEntry(d, 'dimension', `Dimension ${d}`, d, 'canonical');
|
|
285
|
+
for (const v of vocabulary) {
|
|
286
|
+
if (typeof v === 'string') addEntry(v, 'term');
|
|
287
|
+
else addEntry(v.display ?? v.word ?? v.label, v.kind ?? 'term', v.sense ?? null, v.reference ?? null, v.relation ?? null);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const keywords = [
|
|
291
|
+
{ display: 'by', kind: 'keyword', sense: 'Group or slice by dimension' },
|
|
292
|
+
{ display: 'and', kind: 'keyword', sense: 'Add another measure or dimension' },
|
|
293
|
+
{ display: 'where', kind: 'keyword', sense: 'Filter by dimension member' },
|
|
294
|
+
{ display: 'top', kind: 'keyword', sense: 'Rank top N members' },
|
|
295
|
+
{ display: '=', kind: 'operator', sense: 'Equals member value' },
|
|
296
|
+
{ display: 'in', kind: 'keyword', sense: 'Match any of several members' },
|
|
297
|
+
{ display: 'vs', kind: 'keyword', sense: 'Compare against another measure or period' },
|
|
298
|
+
{ display: 'descending', kind: 'keyword', sense: 'Sort highest first' },
|
|
299
|
+
{ display: 'ascending', kind: 'keyword', sense: 'Sort lowest first' },
|
|
300
|
+
];
|
|
301
|
+
for (const kw of keywords) addEntry(kw.display, kw.kind, kw.sense);
|
|
302
|
+
|
|
303
|
+
// Learn next-word transitions (bigrams) from sampleQuestions.
|
|
304
|
+
const bigrams = new Map();
|
|
305
|
+
for (const q of sampleQuestions) {
|
|
306
|
+
const tokens = String(q).trim().split(/\s+/).filter(Boolean);
|
|
307
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
308
|
+
const prev = tokens[i].replace(/,+$/, '').toLowerCase();
|
|
309
|
+
const next = tokens[i + 1].replace(/,+$/, '');
|
|
310
|
+
if (!bigrams.has(prev)) bigrams.set(prev, new Set());
|
|
311
|
+
bigrams.get(prev).add(next);
|
|
312
|
+
addEntry(tokens[i].replace(/,+$/, ''), 'term');
|
|
313
|
+
}
|
|
314
|
+
if (tokens.length > 0) addEntry(tokens.at(-1).replace(/,+$/, ''), 'term');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const getMembers = (dim) => {
|
|
318
|
+
if (!dim || !members) return [];
|
|
319
|
+
if (typeof members === 'function') return members(dim) ?? [];
|
|
320
|
+
const foundKey = Object.keys(members).find(k => k.toLowerCase() === dim.toLowerCase());
|
|
321
|
+
return foundKey ? members[foundKey] ?? [] : [];
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
return (text) => {
|
|
325
|
+
const raw = String(text ?? '');
|
|
326
|
+
const match = raw.match(/^(.*?)(\S*)$/);
|
|
327
|
+
const before = match ? match[1] : '';
|
|
328
|
+
const partial = (match ? match[2] : '').toLowerCase();
|
|
329
|
+
const priorTokens = before.trim().split(/\s+/).filter(Boolean);
|
|
330
|
+
const lastToken = (priorTokens.at(-1) ?? '').replace(/,+$/, '').toLowerCase();
|
|
331
|
+
const prevToken = (priorTokens.at(-2) ?? '').replace(/,+$/, '').toLowerCase();
|
|
332
|
+
|
|
333
|
+
const results = [];
|
|
334
|
+
const push = (entry, bonus = 0) => {
|
|
335
|
+
const word = String(entry.display);
|
|
336
|
+
if (partial && !word.toLowerCase().startsWith(partial)) return;
|
|
337
|
+
if (results.some(r => r.display.toLowerCase() === word.toLowerCase())) return;
|
|
338
|
+
results.push({
|
|
339
|
+
display: word,
|
|
340
|
+
completion: AskView._completeWord(raw, word),
|
|
341
|
+
sense: entry.sense ?? null,
|
|
342
|
+
reference: entry.reference ?? null,
|
|
343
|
+
relation: entry.relation ?? null,
|
|
344
|
+
kind: entry.kind ?? 'suggestion',
|
|
345
|
+
_score: bonus + (word.toLowerCase() === partial ? 0 : 10),
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// 1. After `<dimension> =`, suggest sampled member values of that dimension.
|
|
350
|
+
if (lastToken === '=' && prevToken) {
|
|
351
|
+
const dimName = dimensions.find(d => d.toLowerCase() === prevToken) ?? prevToken;
|
|
352
|
+
for (const val of getMembers(dimName)) {
|
|
353
|
+
push({
|
|
354
|
+
display: String(val),
|
|
355
|
+
kind: 'value',
|
|
356
|
+
sense: `Member of ${dimName}`,
|
|
357
|
+
reference: `${dimName} = ${val}`,
|
|
358
|
+
}, 100);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 2. Context-sensitive grammar transitions.
|
|
363
|
+
if (!lastToken) {
|
|
364
|
+
for (const c of catalog.filter(e => e.kind === 'metric')) push(c, 80);
|
|
365
|
+
for (const c of catalog.filter(e => e.display === 'top')) push(c, 70);
|
|
366
|
+
for (const c of catalog.filter(e => e.kind === 'dimension')) push(c, 60);
|
|
367
|
+
} else if (lastToken === 'by' || lastToken === 'where') {
|
|
368
|
+
for (const c of catalog.filter(e => e.kind === 'dimension')) push(c, 90);
|
|
369
|
+
} else if (lastToken === 'and') {
|
|
370
|
+
const inByClause = priorTokens.some(t => t.toLowerCase() === 'by');
|
|
371
|
+
for (const c of catalog.filter(e => e.kind === (inByClause ? 'dimension' : 'metric'))) push(c, 90);
|
|
372
|
+
for (const c of catalog.filter(e => e.kind === (inByClause ? 'metric' : 'dimension'))) push(c, 70);
|
|
373
|
+
} else if (dimensions.some(d => d.toLowerCase() === lastToken)) {
|
|
374
|
+
const inWhere = priorTokens.some(t => t.toLowerCase() === 'where');
|
|
375
|
+
if (inWhere) {
|
|
376
|
+
for (const c of catalog.filter(e => e.display === '=')) push(c, 95);
|
|
377
|
+
} else {
|
|
378
|
+
for (const c of catalog.filter(e => ['and', 'where', 'descending', 'ascending'].includes(e.display))) push(c, 85);
|
|
379
|
+
}
|
|
380
|
+
} else if (measures.some(m => m.toLowerCase() === lastToken)) {
|
|
381
|
+
for (const c of catalog.filter(e => ['by', 'and', 'where', 'vs'].includes(e.display))) push(c, 90);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 3. Learned bigram transitions from sample questions.
|
|
385
|
+
if (lastToken && bigrams.has(lastToken)) {
|
|
386
|
+
for (const nextWord of bigrams.get(lastToken)) {
|
|
387
|
+
const existing = catalog.find(c => c.display.toLowerCase() === nextWord.toLowerCase());
|
|
388
|
+
push(existing ?? { display: nextWord, kind: 'next word', sense: `Follows "${lastToken}"` }, 88);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// 4. All remaining catalog entries matching the partial prefix.
|
|
393
|
+
for (const c of catalog) push(c, 20);
|
|
394
|
+
|
|
395
|
+
return results
|
|
396
|
+
.sort((a, b) => b._score - a._score)
|
|
397
|
+
.map(({ _score, ...rest }) => rest);
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|