@peter.prib/renderengine 0.12.0 → 0.14.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.
Files changed (67) hide show
  1. package/README.md +454 -296
  2. package/dist/renderengine-workspace.umd.js +4122 -1167
  3. package/dist/renderengine.umd.js +4103 -1164
  4. package/documentation/REFERENCE.md +96 -6
  5. package/package.json +8 -3
  6. package/src/chartCatalog.js +50 -3
  7. package/src/charting/Axis.js +47 -5
  8. package/src/charting/Chart3D.js +4 -2
  9. package/src/charting/DiagramEditor.js +239 -34
  10. package/src/charting/Hierarchy.js +28 -0
  11. package/src/charting/ModelFileFormat.js +47 -0
  12. package/src/charting/archimate/ArchimateCanvasEditor.js +1108 -1012
  13. package/src/charting/archimate/deleteFromModel.js +57 -0
  14. package/src/charting/archimate/elementMenuItems.js +3 -1
  15. package/src/charting/archimate/heatMaps.js +169 -0
  16. package/src/charting/archimate/index.js +1 -0
  17. package/src/charting/ask/AskBar.js +388 -0
  18. package/src/charting/ask/AskView.js +400 -0
  19. package/src/charting/charts/AreaChart.js +20 -3
  20. package/src/charting/charts/BarChart.js +5 -5
  21. package/src/charting/charts/BarChart3D.js +2 -2
  22. package/src/charting/charts/BarChart3DOrbit.js +3 -1
  23. package/src/charting/charts/BarChartBase.js +21 -3
  24. package/src/charting/charts/BoxPlotChart.js +2 -2
  25. package/src/charting/charts/BubbleChart.js +4 -1
  26. package/src/charting/charts/ChordDiagram.js +4 -4
  27. package/src/charting/charts/DensityChart.js +1 -1
  28. package/src/charting/charts/FunnelChart.js +1 -1
  29. package/src/charting/charts/GroupedBarChart.js +7 -9
  30. package/src/charting/charts/HeatmapChart.js +4 -1
  31. package/src/charting/charts/HistogramChart.js +2 -2
  32. package/src/charting/charts/IcicleChart.js +47 -0
  33. package/src/charting/charts/LollipopChart.js +104 -0
  34. package/src/charting/charts/MarimekkoChart.js +75 -0
  35. package/src/charting/charts/OutlineChart.js +10 -8
  36. package/src/charting/charts/ParallelCoordinatesChart.js +2 -2
  37. package/src/charting/charts/ParetoChart.js +92 -0
  38. package/src/charting/charts/PipeChart.js +5 -2
  39. package/src/charting/charts/PopulationPyramidChart.js +4 -4
  40. package/src/charting/charts/RadarChart.js +5 -4
  41. package/src/charting/charts/RadialBarChart.js +2 -2
  42. package/src/charting/charts/SankeyDiagram.js +4 -4
  43. package/src/charting/charts/ScatterChart.js +6 -3
  44. package/src/charting/charts/StackedBarChart.js +82 -11
  45. package/src/charting/charts/StackedBarChart3DOrbit.js +5 -2
  46. package/src/charting/charts/StreamGraphChart.js +8 -0
  47. package/src/charting/charts/SunburstChart.js +2 -2
  48. package/src/charting/charts/TableChart.js +18 -2
  49. package/src/charting/charts/TreemapChart.js +4 -4
  50. package/src/charting/charts/ViolinChart.js +2 -2
  51. package/src/charting/charts/WaterfallChart.js +2 -2
  52. package/src/charting/charts/WordCloudChart.js +2 -2
  53. package/src/charting/diagrams/ArchimateModel.js +33 -0
  54. package/src/charting/diagrams/DeleteFromModelCommand.js +81 -0
  55. package/src/charting/diagrams/EdgeGeometry.js +16 -0
  56. package/src/charting/edgeStyle/EdgeStyleSchema.js +6 -2
  57. package/src/charting/pivot/PivotChart.js +205 -13
  58. package/src/charting/pivot/PivotGridChart.js +1 -1
  59. package/src/charting/pivot/PivotView.js +555 -23
  60. package/src/charting/table/TableView.js +10 -1
  61. package/src/charting/table/TableViewChart.js +1 -1
  62. package/src/charting/templates/Template.js +10 -0
  63. package/src/exportImage.js +45 -18
  64. package/src/index.js +115 -6
  65. package/src/workspace/parts/ArchimateCanvasPart.js +3 -0
  66. package/src/workspace/parts/DiagramEditorPart.js +2 -0
  67. package/src/workspace/parts/ModelHosted.js +15 -4
@@ -74,7 +74,7 @@ catalog `key` for you.
74
74
  | `sankey` | `sankeyDiagram(data, opts)` | `{nodes?: Array<{id,label?,color?}>, links: Array<{source,target,value}>}` | proportional-width flow links; `nodeWidth`, `linkOpacity` |
75
75
  | `chord` | `chordDiagram(data, opts)` | same as `sankey` | nodes on a circle with curved chords of proportional thickness |
76
76
  | `candlestick` | `candlestick(data, opts)` | `Array<{label, open, high, low, close}>` | `upColor`, `downColor`, `markers` (tooltip annotations) |
77
- | `stackedBar` | `stackedBarChart(data, opts)` | `Array<{label, values:number[]}>` | `colors` (no built-in legend — a known gap) |
77
+ | `stackedBar` | `stackedBarChart(data, opts)` | multi-series `Array<{name, values:Array<{label,value}>}>` or `Array<{label, values:number[]}>` | `colors`, `legend`, `seriesLabels`; draws Y-axis ticks and legend |
78
78
  | `pipe` | `pipeChart(data, opts)` | `Array<{label, value, metric?}>` or multi-series | `metric` tapers line width; `legend` |
79
79
  | `radar` | `radarChart(data, opts)` | `Array<{axis, value}>` or multi-series | `colors`, `legend` |
80
80
  | `funnel` | `funnelChart(data, opts)` | `Array<{label, value}>` (largest first) | `colors` |
@@ -1057,7 +1057,10 @@ types — see `ARCHITECTURE.md`):
1057
1057
 
1058
1058
  **In the workbench:** `DataModelPart` (type `data-model`, `RenderEngineWorkspace.DataModelPart`) shows one
1059
1059
  model: File ▸ Open…, Save, Save As… (Ctrl+O, Ctrl+S; `part.file`, the `ModelFile` editors use, • while changed,
1060
- asking before the page is left with changes unsaved), and hosted by a `ModelsPart` (`editorType: 'data-model'`)
1060
+ asking before the page is left with changes unsaved), File ▸ **Import from pasted text (.datamodel.json)…** (a model from
1061
+ pasted text rather than a file: a file's contents, or an assistant's answer or a page with the JSON in a ``` block
1062
+ or between other words, found by `findPayload(format, text)`; `askPasteImport(format, { onImport })`; every
1063
+ `ModelHosted` part offers it for its text formats, `part.pasteItems()`; it opens as Open… does), and hosted by a `ModelsPart` (`editorType: 'data-model'`)
1061
1064
  New ▸ (New model, From base: …), Duplicate model, Close model and This browser ▸ (with `keep`: kept in this browser
1062
1065
  as it changes, reopened next time, backed up to a folder chosen once, as the modeller's models are); Edit ▸ Undo
1063
1066
  and Redo; Tools ▸ Compare with and Merge from (the other open models and the bases; a merge is one undo step); Model ▸ New / Edit / Delete perspective and context (those dialogs), Perspective and Context selects (when
@@ -1081,6 +1084,13 @@ this to `line`/`area`/`stackedBar`/etc. is the same three-line change: call
1081
1084
  `this._drawYAxisTicks(x, y, width, height, rawMax)` before drawing marks, and scale against its
1082
1085
  return value instead of the raw max — not done yet for the rest, tracked as a follow-up.
1083
1086
 
1087
+ **Negative values.** `bar` and `groupedBar` (and so `pivotChart`, which charts through the grouped
1088
+ bar) take in the smallest value as well as the largest: when one is negative the axis goes below zero
1089
+ on one common step (`Axis.niceRange(min, max)`, drawn by `Axis.drawYRange`), a zero line is drawn
1090
+ across the plot where it crosses, and a negative bar hangs down from it, its value written below it.
1091
+ Money signed both ways (income positive, spending negative) charts as it is, and a total over both is
1092
+ the net. Without a negative value nothing changes: the axis starts at 0 at the bottom.
1093
+
1084
1094
  ### Choosing a chart for result rows — `ChartChooser`
1085
1095
 
1086
1096
  `new ChartChooser().choose(rows, columns?, { complete? })` says which chart types fit a set of result rows (a query
@@ -1138,6 +1148,7 @@ const view = new RenderEngine(container, { rendererType: 'svg' }).resultView(row
1138
1148
  pageSize: 50,
1139
1149
  menus: { row: ({ row }) => [{ label: 'Open order', action: () => open(row.id) }] },
1140
1150
  highlight: (row) => row.id === selectedId, // rows drawn highlighted: the host's selection
1151
+ format: { Revenue: new Intl.NumberFormat('en-AU', { style: 'currency', currency: 'AUD' }) },
1141
1152
  });
1142
1153
  ```
1143
1154
 
@@ -1159,6 +1170,12 @@ or `chartHeight`, 360); pass `fitHeight: false` to size it yourself. A host adds
1159
1170
  any menu with `menus: { cell, row, column, table }`, each `(context) → PopupMenu items`, shown after
1160
1171
  a separator.
1161
1172
 
1173
+ **Number format.** `format` says how numbers show: a function (number → text) or an
1174
+ `Intl.NumberFormat` for every number, or an object naming either per column, where a column it does
1175
+ not name stays plain (116028; a fraction to at most 4 decimals). Only the cells' text changes: sorting
1176
+ and filtering use the values, and every copy (the table, a column, a row, a value) keeps the raw
1177
+ numbers, so a pasted copy still adds up. `pivotView` takes the same `format`, per measure.
1178
+
1162
1179
  Interaction needs the SVG renderer (its shapes take clicks); on canvas the same table is drawn
1163
1180
  without it. `TableView` (`src/charting/table/TableView.js`) holds all of the state and builds every
1164
1181
  menu as data, so it can be driven and tested without a DOM; `TableViewChart` draws it.
@@ -1211,6 +1228,10 @@ grid, as `resultView`'s does. `PivotView` (`src/charting/pivot/`) holds all the
1211
1228
  menus as data, so it can be tested without a DOM; `PivotGridChart` draws it, with the interactive
1212
1229
  table's click handling (SVG for clicks; canvas draws the grid without them).
1213
1230
 
1231
+ `format` (as `resultView`'s, per measure: `{ Revenue: new Intl.NumberFormat(…) }`, or one for every
1232
+ measure) writes the cells, the totals and the traffic lights' cut-offs ("Revenue ≥ $3,000.00"); the
1233
+ grid copy keeps the raw numbers.
1234
+
1214
1235
  #### Saved views
1215
1236
 
1216
1237
  A saved view is plain data: the layout (`view.layout()`) and how the grid shows it
@@ -2098,8 +2119,32 @@ colour by, then a colour for each of its values (a soft palette by default, `Dia
2098
2119
  colour" leaves a value's shapes as they are), and whether to add a legend. Each shape takes its value's colour as
2099
2120
  its own fill, so the colours are kept like any other (an Archi file included), and one undo puts them back. The
2100
2121
  template editor colours by the kind of shape; the ArchiMate canvas by **Layer**, **Type**, **Specialization** or any
2101
- **property** its elements have (notes and pictures have no value and stay as they are). A legend is a title and a
2102
- box per value in its colour, in a row under the drawing (notes; on a canvas, stickies). **Add legend of the kinds
2122
+ **property** its elements have (notes and pictures have no value and stay as they are), and two more:
2123
+
2124
+ - **Change (create, update, delete)**, offered when the model has a deliverable or work package: each element related
2125
+ to one (either way round) takes the change the relationship's name says, or, when the relationship has no name, the
2126
+ deliverable's or work package's name. Synonyms count, in any form: delete, remove, retire, decommission, drop,
2127
+ sunset, phase out, shut down, …; create, add, new, introduce, build, implement, set up, launch, …; update, change,
2128
+ modify, upgrade, enhance, migrate, replace, … (`CHANGE_WORDS`, whole words only). Where an element is given more
2129
+ than one, the one with most consequence wins: delete, then create, then update. The colours offered are
2130
+ **Create** yellow #FDD835, **Update** blue #1E88E5 and **Delete** magenta #D81B60 (`CHANGE_COLOURS`), each still
2131
+ changeable (`changeAction(text)`, `elementChange(model, elementId)`). The lines are coloured too: each relationship
2132
+ to a deliverable or work package is drawn in the colour of the change it says itself (`relationshipChange(model,
2133
+ rel)`), so an element changed two ways shows both on its lines; other lines keep their colour. Lines only drawn
2134
+ (derived ones) are left as they are (`editor.colourEdgeValue(edge, key)`, which a subclass overrides to colour
2135
+ lines by a key).
2136
+ - **A formula over properties…**, offered when elements have properties: a number from each element's numeric
2137
+ properties, in the formula language `colorCol` uses (`kernelExpr.js`: arithmetic, comparisons, `a ? b : c`,
2138
+ maths functions), each property by its name with anything but letters, digits and _ made _ ("Risk score" is
2139
+ `Risk_score`), e.g. `Cost * Risk_score` or `Cost > 10000 ? 1 : 0`. A formula naming no property is refused
2140
+ with what the names are. An element without a number for a property the formula uses is left as it is. Up to
2141
+ eight different results are each a value; more are put in five equal bands, smallest first (a band with nothing in
2142
+ it left out), and the colours offered run light to dark (`expressionValues`, `resultBands`,
2143
+ `sequentialColours`; the key is `expression:<formula>`).
2144
+
2145
+ A legend is a box under the
2146
+ drawing, titled at its top-left, holding a box per value in its colour (in rows of up to four,
2147
+ `DiagramEditor.legendLayout`), all grouped so they move as one (notes; on a canvas, stickies). **Add legend of the kinds
2103
2148
  shown** adds one for the kinds of shape (ArchiMate: the element types) in the colour of each's first shape. A
2104
2149
  locked shape keeps its colour. `colourKeys()`, `colourValue(template, key)`, `colourValues(key)`,
2105
2150
  `colourByCommand(key, colours, { legend })`, `kindLegendEntries()` and `legendCommand(entries, title)`, which a
@@ -2224,7 +2269,23 @@ label shown, in the same box (or none), with the same lines drawn (each relation
2224
2269
  thing at the other end), as one undo step with its lines (`view.repeatedBoxes()`, `editor.removeRepeatedBoxes()`).
2225
2270
  The first box stays; a box holding others is never taken off. The **File** menu (`EditorPart`'s, and the ArchiMate editor's) exports the current view
2226
2271
  as an image: PNG, SVG or a copy to the clipboard (`editor.exportImage('png' | 'svg' | 'copy')`, without the grid
2227
- or selection; the ArchiMate editor's is off with `features.exportImage: false`). **Generate View For This**, on
2272
+ or selection; the ArchiMate editor's is off with `features.exportImage: false`), and so does a right-click on empty
2273
+ canvas (**Export image**, `editor.canvasContextMenu(at)`). By default a picture takes in **the whole drawing**,
2274
+ wherever the view is scrolled: every shape and line, line labels and arrowheads included, with 20px clear all round
2275
+ so nothing touches the edge (`editor.drawingBounds({ margin })`), at **100%**. **Export image…** asks the format,
2276
+ the area (the whole drawing, or only what is in sight) and the scale in percent, and the quick items use its last
2277
+ choice (`editor.exportOptions`, `setExportOptions({ area, scale })`; `exportImage(format, { area, scale })`).
2278
+ **⚙ Settings ▸ Export area** shows where the whole drawing's picture ends as a faint dashed box, never itself
2279
+ exported (`editor.showExportArea`). An SVG cropped to a viewBox keeps a white background over all of it.
2280
+
2281
+ **🗑 Delete from model…**, on an element's menu in the tree and on its box's menu in the diagram (the elements
2282
+ selected, when it is one of them), takes it out of the model rather than off one view, as Archi's "Delete from
2283
+ Model". First a dialog says where it is used: each view it is on (with how many boxes), and each relationship that
2284
+ goes too (those ending on it, and those ending on such a relationship); **Delete from model** goes ahead. Every
2285
+ box and line of them goes from every view, with any view's own connection to one of its boxes, as one undo step
2286
+ that puts it all back, ids and all (`model.usage(elementIds)`, `model.deleteFromModelCommand(elementIds)`,
2287
+ `editor.askDeleteFromModel(ids)` / `deleteFromModel(ids)`, `deleteFromModelMessage(model, ids)`;
2288
+ `elementMenuItems` offers it when the host gives `deleteFromModel`). **Generate View For This**, on
2228
2289
  an element's menu in the diagram or the tree, makes a view of the element with everything related to it
2229
2290
  around it and every relationship between them (`model.generateView(elementIds, { name, related })`, Archi's
2230
2291
  "Generate View For").
@@ -2587,7 +2648,7 @@ single list of cascading properties:
2587
2648
  | Key | Values (default first) |
2588
2649
  |---|---|
2589
2650
  | `routing` | `straight`, `orthogonal`, `curved`, `polyline` (bends around other shapes) |
2590
- | `attach` | `floating` (outline, aimed at the other end), `sides` (middle of the facing side) |
2651
+ | `attach` | `shortest` (the default, as Archi draws: where two shapes face each other across a gap with their widths or heights overlapping, a straight line straight across the middle of the overlap, outline to outline; otherwise as `floating`), `floating` (outline, aimed at the other end), `sides` (middle of the facing side). `shortest` leaves a line with bends, a pinned end, routing other than straight, or a rotated shape as `floating` draws it |
2591
2652
  | `lineStyle` | `solid`, `dashed`, `dotted` (same dash patterns as `GraphDiagram`, via `LINE_STYLE_DASH`) |
2592
2653
  | `strokeColor`, `strokeWidth` | `#666666`, `2` |
2593
2654
  | `startMarker`, `endMarker` | `none`, or an `EdgeMarker` key |
@@ -3221,6 +3282,35 @@ const mine = ICONS.with({ // a host's own icons over
3221
3282
  the workspace bundle), until a `foreignObject`, whose children are HTML again. A spec's `ns` sets the namespace
3222
3283
  explicitly.
3223
3284
 
3285
+ ## Ask Bar & Next-Word Typeahead (`AskBar` / `AskView`)
3286
+
3287
+ `AskBar` (`src/charting/ask/AskBar.js`) and its DOM-free state model `AskView` (`src/charting/ask/AskView.js`) provide the interactive question bar and scrollable next-word prediction dropdown (as used in `semanticengine/examples/browser/index.html` and the **Cubing** tab in `test/charts.html`).
3288
+
3289
+ ```javascript
3290
+ import { RenderEngine, AskBar, AskView } from '@peter.prib/renderengine';
3291
+
3292
+ const bar = RenderEngine.askBar(document.getElementById('ask-bar'), {
3293
+ value: 'top 3 customers by revenue',
3294
+ // Either pass a custom suggestion callback (e.g. semanticengine's engine.suggest):
3295
+ suggest: (text) => semanticEngine.suggest(text, { sampler: (dim) => semanticEngine.members(dim) }),
3296
+ // Or pass declarative cube/table metadata for built-in next-word prediction:
3297
+ // measures: ['Revenue', 'Cost'],
3298
+ // dimensions: ['Region', 'Quarter'],
3299
+ // members: (dim) => [...],
3300
+ sampleQuestions: ['revenue by region', 'top 3 customers by revenue'],
3301
+ visibleCount: 5, // items visible at once in the scrollable list
3302
+ predictChoices: [0, 3, 5, 8, 10], // 0 = Off; user can switch in the bar's selector
3303
+ onAsk: (question) => runQuery(question),
3304
+ });
3305
+ ```
3306
+
3307
+ - **Configurable scrollable next-word prediction:** `visibleCount` (default `5`) sets how many predicted items show at once before scrolling (`max-height` on `.re-ask-suggest-box` with `overflow-y: auto`), and `predictChoices` (default `[0, 3, 5, 8, 10]`) populates the **Predict next word** selector (`Off`, `3 visible (scrollable)`, `5 visible (scrollable)`, …). Set `showPredictControl: false` to hide the selector.
3308
+ - **Semantic badges:** each suggestion item (`{ display, completion, sense?, reference?, relation?, kind? }`) renders its `display` token, optional `sense → reference` subtitle, lexical `relation` badge (`synonym`, `hypernym`, `hyponym`, `antonym`), and `kind` badge (`metric`, `dimension`, `keyword`, `value`).
3309
+ - **Keyboard navigation:** nothing in the list is selected until you move into it, so `Enter` asks the question as typed. `↑` / `↓` move through the scrollable list, starting at the last or first item and scrolling the active item into view. `Enter` completes the selected item; `Tab` completes the selected item, or the first. Either one immediately predicts the following word. `Escape` closes the popup.
3310
+ - **Async sources and answers:** `suggest` can return a promise, for example from an engine that is still loading or a vocabulary on a server. The list keeps showing what it had until the promise settles. An answer for text the user has since changed is dropped, and a rejected promise shows no suggestions. `onAsk` can also return a promise of `{ rows, columns, complete }`, which `resultEngine` shows once it settles.
3311
+ - **For a host:** `bar.setSuggest(fn)` swaps where suggestions come from, for example when the user picks another model or ontology. `bar.setValue(text)` sets the question without suggesting. `bar.focus()` puts the cursor in the question, and `bar.destroy()` removes the bar and cancels any pending close.
3312
+ - **Accessible:** the input is a WAI-ARIA combobox over a `listbox` of `option`s. `aria-expanded` follows the popup and `aria-activedescendant` follows the active item. Each bar's list has its own id, so several bars can share a page.
3313
+
3224
3314
  ## Where to go next
3225
3315
 
3226
3316
  - **`ARCHITECTURE.md`** — module layout, the chart-type catalog mechanics, how to add a new chart type.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@peter.prib/renderengine",
3
- "version": "0.12.0",
4
- "description": "ArchiMate modeller, diagram editor and chart library for the browser, with no runtime dependencies.",
3
+ "version": "0.14.0",
4
+ "description": "ArchiMate modeller, data modeller, diagram editor, OLAP cube & pivot chart studio, and 50+ type chart library for the browser, with no runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "module": "src/index.js",
@@ -28,7 +28,7 @@
28
28
  "archimate": "node scripts/open.mjs examples/archimate.html",
29
29
  "diagram": "node scripts/open.mjs examples/diagram-editor.html",
30
30
  "datamodel": "node scripts/open.mjs examples/data-model.html",
31
- "test": "node --test --test-timeout=10000 ./test/*.js ./test/charting/*.js ./test/charting/charts/*.js ./test/charting/table/*.js ./test/charting/pivot/*.js ./test/charting/diagrams/*.js ./test/charting/shapes/*.js",
31
+ "test": "node --test --test-timeout=10000 ./test/*.js ./test/charting/*.js ./test/charting/charts/*.js ./test/charting/table/*.js ./test/charting/pivot/*.js ./test/charting/ask/*.js ./test/charting/diagrams/*.js ./test/charting/shapes/*.js",
32
32
  "build": "rollup -c",
33
33
  "prepublishOnly": "npm test && npm run build",
34
34
  "screenshots": "node scripts/screenshots/screenshots.mjs"
@@ -38,6 +38,11 @@
38
38
  "archi",
39
39
  "diagram",
40
40
  "diagram-editor",
41
+ "data-modelling",
42
+ "bian",
43
+ "olap",
44
+ "pivot",
45
+ "cube",
41
46
  "modelling",
42
47
  "charts",
43
48
  "svg",
@@ -318,12 +318,15 @@ export const CHART_CATALOG = [
318
318
  },
319
319
  {
320
320
  key: 'stackedBar', label: 'Stacked Bar', method: 'stackedBarChart', category: '2d',
321
- coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: false, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.NONE,
321
+ coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.NONE,
322
+ seriesCapable: true,
322
323
  axes: [
323
324
  { role: 'category', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.POSITION_X, required: true },
324
- { role: 'values', kind: AXIS_KIND.NUMERIC_LIST, channel: ENCODING_CHANNEL.POSITION_Y_STACKED, required: true },
325
+ { role: 'values', kind: AXIS_KIND.NUMERIC_LIST, channel: ENCODING_CHANNEL.POSITION_Y_STACKED, required: false },
326
+ { role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.POSITION_Y_STACKED, required: false },
327
+ { role: 'series', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
325
328
  ],
326
- dataShape: "Array<{label, values:number[]}> e.g. [{label:'Jan', values:[10,20]}] — no built-in legend option despite being multi-series (a real gap; areaChart's stacked mode has one, this doesn't)",
329
+ dataShape: "Array<{label, values:number[]}> or multi-series Array<{name, values:Array<{label,value}>}> — segments stacked from zero, with optional series legend",
327
330
  streamShape: 'flat', // a new stacked bar is a whole new {label,values} record appended to the outer array, not growth of an existing record's values
328
331
  },
329
332
  {
@@ -564,6 +567,50 @@ export const CHART_CATALOG = [
564
567
  dataShape: 'Array<{label, x, y, z, size?, category?}> — real orbit camera (drag-rotate/wheel-zoom); a future sphere/ellipsoid variant is this same shape with a second shape-size-channeled role, not a new chart type',
565
568
  streamShape: 'flat',
566
569
  },
570
+ {
571
+ key: 'pareto', label: 'Pareto', method: 'paretoChart', category: '2d',
572
+ coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: false, colorMode: COLOR_MODE.SEMANTIC, camera: CAMERA_MODE.NONE,
573
+ axes: [
574
+ { role: 'category', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.POSITION_X, required: true },
575
+ { role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.POSITION_Y, required: true },
576
+ ],
577
+ dataShape: 'Array<{label, value}> — sorted descending with a right-hand 0-100% cumulative percentage line and 80% cutoff reference line',
578
+ streamShape: 'flat',
579
+ },
580
+ {
581
+ key: 'lollipop', label: 'Lollipop', method: 'lollipopChart', category: '2d',
582
+ coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.NONE,
583
+ seriesCapable: true,
584
+ axes: [
585
+ { role: 'category', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.POSITION_X, required: true },
586
+ { role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.POSITION_Y, required: true },
587
+ { role: 'series', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
588
+ ],
589
+ dataShape: 'Array<{label, value}> or multi-series Array<{name, values:Array<{label,value}>}> — stem-and-dot alternative to bars',
590
+ streamShape: 'flat',
591
+ },
592
+ {
593
+ key: 'marimekko', label: 'Marimekko', method: 'marimekkoChart', category: '2d',
594
+ coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.NONE,
595
+ seriesCapable: true,
596
+ axes: [
597
+ { role: 'category', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.SHAPE_WIDTH, required: true },
598
+ { role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.POSITION_Y_HEIGHT, required: true },
599
+ { role: 'series', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: true },
600
+ ],
601
+ dataShape: 'Multi-series Array<{name, values:Array<{label,value}>}> or Array<{label, values:number[]}> — variable-width 100%-stacked mosaic columns',
602
+ streamShape: 'series',
603
+ },
604
+ {
605
+ key: 'icicle', label: 'Icicle', method: 'icicleChart', category: '2d',
606
+ coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: false, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.NONE,
607
+ axes: [
608
+ { role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.ANGLE_OR_AREA, required: true },
609
+ { role: 'group', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.POSITION_ORDER, required: false },
610
+ ],
611
+ dataShape: 'Array<{label, value?, children?}> — rectangular hierarchical partition chart (top-down bands by depth, width proportional to value)',
612
+ streamShape: 'flat',
613
+ },
567
614
  {
568
615
  key: 'bar3DOrbit', label: 'Bar (3D Orbit)', method: 'barChart3DOrbit', category: '3d',
569
616
  coordSystem: COORD_SYSTEM.ORBIT_3D, title: true, legend: false, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.ORBIT,
@@ -27,6 +27,30 @@ export class Axis {
27
27
  return ticks;
28
28
  }
29
29
 
30
+ /**
31
+ * Ticks over a range that may go below zero: one common "nice" step, from at or below `minVal`
32
+ * to at or above `maxVal`, always taking in 0 so bars have a baseline. Without a negative it is
33
+ * `niceTicks(maxVal)`.
34
+ * @param {number} minVal
35
+ * @param {number} maxVal
36
+ * @param {number} [count=4] desired number of intervals
37
+ * @returns {number[]}
38
+ */
39
+ static niceRange(minVal, maxVal, count = 4) {
40
+ const lo = Math.min(0, minVal || 0);
41
+ const hi = Math.max(0, maxVal || 0);
42
+ if (lo === 0) return Axis.niceTicks(hi, count);
43
+ const raw = (hi - lo) / count;
44
+ const mag = Math.pow(10, Math.floor(Math.log10(raw)));
45
+ const frac = raw / mag;
46
+ const step = frac < 1.5 ? mag : frac < 3.5 ? 2 * mag : frac < 7.5 ? 5 * mag : 10 * mag;
47
+ const first = Math.floor(lo / step + 1e-9);
48
+ const last = Math.ceil(hi / step - 1e-9);
49
+ const ticks = [];
50
+ for (let i = first; i <= last; i++) ticks.push(parseFloat((i * step).toFixed(10)));
51
+ return ticks;
52
+ }
53
+
30
54
  /**
31
55
  * How often a category label fits along an axis `width` wide: 1 (every
32
56
  * label), 2 (every other), … — so a long series (a minute of samples, a
@@ -71,18 +95,36 @@ export class Axis {
71
95
  * @returns {number} the nice domain max the ticks actually span
72
96
  */
73
97
  drawYAxis(renderer, box, rawMax) {
74
- const ticks = Axis.niceTicks(rawMax);
75
- const niceMax = ticks[ticks.length - 1] || 1;
98
+ return this.drawYRange(renderer, box, 0, rawMax).max;
99
+ }
100
+
101
+ /**
102
+ * Draw a y-axis over a range that may go below zero (`niceRange`): ticks, labels, gridlines,
103
+ * and, when the range does go below zero, a zero line across the plot where it crosses.
104
+ *
105
+ * @param {*} renderer a renderer implementing .path() and .text()
106
+ * @param {{x:number, y:number, width:number, height:number}} box plot-area box
107
+ * @param {number} rawMin the raw data minimum (0 or more: the axis starts at 0)
108
+ * @param {number} rawMax the raw data maximum
109
+ * @returns {{min:number, max:number}} the nice domain the ticks actually span
110
+ */
111
+ drawYRange(renderer, box, rawMin, rawMax) {
112
+ const ticks = Axis.niceRange(rawMin, rawMax);
113
+ const min = ticks[0];
114
+ const max = ticks.length > 1 ? ticks[ticks.length - 1] : 1;
115
+ const yOf = (v) => box.y + box.height - ((v - min) / (max - min)) * box.height;
76
116
  for (const t of ticks) {
77
- const ty = box.y + box.height - (t / niceMax) * box.height;
117
+ const ty = yOf(t);
78
118
  renderer.path(`M${box.x - 5},${ty} L${box.x},${ty}`, 'none', { color: '#999', width: 1 });
79
- if (t > 0) {
119
+ if (t !== 0) {
80
120
  renderer.path(`M${box.x},${ty} L${box.x + box.width},${ty}`, 'none', { color: '#eee', width: 1 });
121
+ } else if (min < 0) {
122
+ renderer.path(`M${box.x},${ty} L${box.x + box.width},${ty}`, 'none', { color: '#666', width: 1, zeroLine: true });
81
123
  }
82
124
  renderer.text(Axis.formatValue(t), box.x - 10, ty, {
83
125
  textAnchor: 'end', verticalAlign: 'middle', fontSize: '10px', fill: '#666',
84
126
  });
85
127
  }
86
- return niceMax;
128
+ return { min, max };
87
129
  }
88
130
  }
@@ -72,8 +72,9 @@ export class Chart3D extends Chart {
72
72
 
73
73
  /**
74
74
  * Draw one 3D box through the orbit camera. Returns average corner depth.
75
+ * When `datum` is supplied (`{ label, series? }`), each face is recorded via `_datum()`.
75
76
  */
76
- drawBox(renderer, x0, x1, y0, y1, z0, z1, color, toScreen) {
77
+ drawBox(renderer, x0, x1, y0, y1, z0, z1, color, toScreen, datum = null) {
77
78
  const camera = this.camera;
78
79
  const faceXSign = Math.sin(camera.theta) >= 0 ? 1 : -1;
79
80
  const faceZSign = Math.cos(camera.theta) >= 0 ? 1 : -1;
@@ -87,7 +88,8 @@ export class Chart3D extends Chart {
87
88
 
88
89
  const drawFace = (pts, shade) => {
89
90
  const d = `M${pts[0].x},${pts[0].y} L${pts[1].x},${pts[1].y} L${pts[2].x},${pts[2].y} L${pts[3].x},${pts[3].y} Z`;
90
- renderer.path(d, ColorPalette.lighten(color, shade), { color: '#333', width: 1 });
91
+ const el = renderer.path(d, ColorPalette.lighten(color, shade), { color: '#333', width: 1 });
92
+ if (datum) this._datum(el, datum);
91
93
  };
92
94
  drawFace(top, 0.15);
93
95
  drawFace(xSide, faceXSign > 0 ? -0.05 : -0.2);