ooxml-core 0.20.0 → 0.21.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 +21 -43
- package/dist/chart/axis-nice.d.ts +97 -0
- package/dist/chart/axis-nice.d.ts.map +1 -0
- package/dist/chart/histogram-binning.d.ts +40 -0
- package/dist/chart/histogram-binning.d.ts.map +1 -0
- package/dist/chart/index.cjs +167 -0
- package/dist/chart/index.d.ts +2 -0
- package/dist/chart/index.d.ts.map +1 -1
- package/dist/chart/index.mjs +162 -1
- package/dist/index.cjs +167 -0
- package/dist/index.mjs +167 -0
- package/dist/pptx/automation/index.cjs +1572 -863
- package/dist/pptx/automation/index.d.ts +4 -3
- package/dist/pptx/automation/index.d.ts.map +1 -1
- package/dist/pptx/automation/index.mjs +1572 -863
- package/dist/pptx/chunks/color-ref-BM66NGFP.d.ts +42 -0
- package/dist/pptx/chunks/color-ref-BM66NGFP.d.ts.map +1 -0
- package/dist/pptx/chunks/{index-D0yGoLyw.d.ts → index-CcQTCBnT.d.ts} +63 -479
- package/dist/pptx/chunks/index-CcQTCBnT.d.ts.map +1 -0
- package/dist/pptx/chunks/{index-BtBj3VG1.d.ts → index-DHoPgFcH.d.ts} +529 -11
- package/dist/pptx/chunks/index-DHoPgFcH.d.ts.map +1 -0
- package/dist/pptx/chunks/{text-operations-GuisjlbR.d.ts → text-operations-BzdehTeY.d.ts} +50 -51
- package/dist/pptx/chunks/text-operations-BzdehTeY.d.ts.map +1 -0
- package/dist/pptx/cli/index.cjs +1204 -489
- package/dist/pptx/cli/index.d.ts +1 -1
- package/dist/pptx/cli/index.mjs +1204 -489
- package/dist/pptx/converter/index.cjs +8 -5
- package/dist/pptx/converter/index.d.ts +1 -1
- package/dist/pptx/converter/index.mjs +8 -5
- package/dist/pptx/index.cjs +1699 -989
- package/dist/pptx/index.d.ts +5 -4
- package/dist/pptx/index.mjs +1698 -990
- package/dist/pptx/ui/index.cjs +510 -0
- package/dist/pptx/ui/index.d.ts +80 -0
- package/dist/pptx/ui/index.d.ts.map +1 -0
- package/dist/pptx/ui/index.mjs +499 -0
- package/package.json +15 -4
- package/dist/pptx/chunks/index-BtBj3VG1.d.ts.map +0 -1
- package/dist/pptx/chunks/index-D0yGoLyw.d.ts.map +0 -1
- package/dist/pptx/chunks/text-operations-GuisjlbR.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -17,14 +17,13 @@ One package, one XML model, every format: Word, PowerPoint, Excel and the shared
|
|
|
17
17
|
[**Packages and areas**](#features-and-api-one-package-many-areas) ·
|
|
18
18
|
[**Install**](#install) ·
|
|
19
19
|
[**Examples**](#quick-start) ·
|
|
20
|
-
[**Roadmap**](#roadmap) ·
|
|
21
20
|
[**Contributing**](CONTRIBUTING.md)
|
|
22
21
|
|
|
23
22
|
</div>
|
|
24
23
|
|
|
25
24
|
## Why ooxml-core?
|
|
26
25
|
|
|
27
|
-
- **No UI, no framework.** Everything runs in browsers, Node.js, Bun, workers and serverless functions. The viewer apps ([docx
|
|
26
|
+
- **No UI, no framework.** Everything runs in browsers, Node.js, Bun, workers and serverless functions. The viewer apps ([docx](https://christophervr.github.io/ooxml/docx/), [xlsx](https://christophervr.github.io/ooxml/xlsx/), [visio](https://christophervr.github.io/ooxml/visio/), [teams](https://christophervr.github.io/ooxml/teams/) and [pptx-viewer](https://github.com/ChristopherVR/pptx-viewer)) are thin interfaces on top of it, and so can your application be.
|
|
28
27
|
- **One package, one structure.** All formats share the same XML model, packaging layer, units, colours and geometry. Each format is an _area_ of this one package, not a separate dependency to keep in step.
|
|
29
28
|
- **Round-trips without losing what it does not understand.** Documents are loaded into a model, edited, and written back. Untouched parts stay byte-for-byte, unknown markup is preserved, and edits that would damage unsupported content are rejected instead of silently dropped.
|
|
30
29
|
- **Strict by default.** New code is strict TypeScript with branded measurement units, the shared `xml` area parses strictly (no DTD or entity expansion), and the `docx` area is checked against the ECMA-376 schemas in the test suite.
|
|
@@ -39,20 +38,24 @@ import { parseXml } from 'ooxml-core/xml'; // one area
|
|
|
39
38
|
import { xml, opc } from 'ooxml-core'; // or by namespace (shared building blocks)
|
|
40
39
|
```
|
|
41
40
|
|
|
42
|
-
| Area
|
|
43
|
-
|
|
|
44
|
-
| `xml`
|
|
45
|
-
| `opc`
|
|
46
|
-
| `units`
|
|
47
|
-
| `color`
|
|
48
|
-
| `geometry`
|
|
49
|
-
| `diagram`
|
|
50
|
-
| `digest`
|
|
51
|
-
| `crypto`
|
|
52
|
-
| `collab`
|
|
53
|
-
| `docx`
|
|
54
|
-
| `pptx`
|
|
55
|
-
| `xlsx`
|
|
41
|
+
| Area | What it is |
|
|
42
|
+
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `xml` | The shared XML model: strict DOM parsing and serialization, namespaces, namespace-aware helpers. |
|
|
44
|
+
| `opc` | Open Packaging Conventions: relationships, content types, part paths, zip helpers, the hyperlink safety policy (`hyperlinkPolicy`) and document properties (core, app and custom, `opc/properties`) and digital signatures (`signature`: detection, the parts a writer strips, XML-DSig parsing and digest checks; the Node verifier is `/pptx/signature-node`). |
|
|
45
|
+
| `units` | Branded EMU, twip and point types, constants and conversions. |
|
|
46
|
+
| `color` | Hex, RGB, HSL and linear colour primitives, OOXML percent and angle parsing. |
|
|
47
|
+
| `geometry` | DrawingML preset shapes, connection sites, clip paths, callouts and boolean shape operations. |
|
|
48
|
+
| `diagram` | SmartArt (DiagramML), format-neutral: data model, layout/colour/quick-style parts, the cached `dsp:drawing` shape tree, relationship resolution and a loader. Used by `docx`; `pptx` re-imports its parsers. |
|
|
49
|
+
| `digest` | Synchronous pure-TypeScript SHA-1, SHA-256, SHA-384 and SHA-512, ECMA-376 digest name normalisation (`SHA-512`, `SHA512`, `sha_512`, ...) and the agile password hash Office protection elements store (`hashPassword`, `verifyPasswordHash`). No Web Crypto, so it works on `http://`. Used by `xlsx` sheet and workbook protection. |
|
|
50
|
+
| `crypto` | ECMA-376 package encryption ([MS-OFFCRYPTO]) for every format: `decryptOoxmlPackage`, `encryptOoxmlPackage` (agile AES-256/SHA-512 by default, or Standard), `isEncryptedOoxmlPackage`, and typed errors with a `code` (`password-required`, `incorrect-password`, `data-integrity`). The compound-file container comes from ole2 (inlined); not in the root entry. |
|
|
51
|
+
| `collab` | Format-neutral real-time collaboration on Yjs: session and provider lifecycle, awareness/presence, transport-neutral sync (WebSocket, in-memory, any byte channel), update codecs, ordering helpers, asset sync and the product adapter seam. See `docs/collab-area.md`. |
|
|
52
|
+
| `docx` | WordprocessingML: model, parser, preserving serializer, editing, validation. Also `/docx/embedded`. `/docx/layout` is the DOM-free pagination engine (an approximation of Word, not parity) and `/docx/load` detects and loads DOCX (also password-protected, `{ password }`) and legacy .doc (ole2 inlined). |
|
|
53
|
+
| `pptx` | PresentationML: model, parser, serializer, editing, charts, SmartArt, converters, CLI, signatures. Subpaths `/pptx/converter`, `/pptx/cli`, `/pptx/signature-node`. |
|
|
54
|
+
| `xlsx` | SpreadsheetML: workbook model, `.xlsx`/`.xlsm` reader and preserving writer, formula engine (dependency graph, dynamic arrays, 380+ functions), Excel number formats, editing commands with undo, and the DOM-free grid layout (sizes, colours, conditional formats, charts as SVG, drawing anchors). SmartArt is shown from the cached drawing (not re-laid out) and kept verbatim on save. `/xlsx/load` detects and loads `.xlsx`, `.xlsm` (also password-protected: `loadWorkbook(bytes, { password })`, `saveWorkbook(workbook, 'xlsx', { password })`), legacy `.xls` (ole2 inlined) and CSV, and refuses `.xlsb` with a typed error. Digital signatures are detected and reported, never kept on save. An approximation of Excel, not parity. |
|
|
55
|
+
| `visio` | Visio: `.vsdx` parsing, loading and bounded editing, page layers and visibility, and a conservative preview of legacy `.vsd` (through ole2). See `docs/visio-legacy-vsd.md`. |
|
|
56
|
+
| `math` | Office Math: OMML to and from LaTeX and MathML, with colour and size helpers. |
|
|
57
|
+
| `teams` | The logic of the OpenTeams workspace: channels and chat on Yjs, presence, WebRTC calls with pluggable signalling, server configuration and the `createTeamsClient` store every UI binds to. Built on `collab`. See `docs/teams-area.md`. |
|
|
58
|
+
| `automation` | Headless document operations and filesystem execution shared by the MCP servers (`/automation`, `/automation/node`). |
|
|
56
59
|
|
|
57
60
|
Legacy binary formats (`.doc`, `.xls`, `.ppt`) and the compound-file container live in the sibling package [`ole2`](https://github.com/ChristopherVR/ole2); this package never contains binary codecs, and `ole2` never contains modern OOXML.
|
|
58
61
|
|
|
@@ -105,34 +108,9 @@ const rels = parseRelationships(relsXml);
|
|
|
105
108
|
const part = resolvePartPath('word/document.xml', rels.get('rId5')!.target);
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
##
|
|
111
|
+
## Contributing and roadmap
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- **Shared layers:** DrawingML (fills, lines, effects, text, theme), charts, diagrams (SmartArt), maths, encryption primitives and schema-generated types, each as an area, written once for every format.
|
|
113
|
-
- **More formats:** Visio (`.vsdx`, also an OPC package), and further Office Open XML parts as they are needed. Each arrives as its own area.
|
|
114
|
-
- **One XML model:** the `pptx` area still uses its own XML object model and is compiled with relaxed TypeScript flags while it is migrated onto the shared `xml` area and tightened. New code is strict.
|
|
115
|
-
- **Collaboration:** the Yjs and sync protocol that the viewers share will live here as a `collab` area.
|
|
116
|
-
|
|
117
|
-
## Development
|
|
118
|
-
|
|
119
|
-
You need [Bun](https://bun.sh/) and Node.js 22 or newer.
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
bun install
|
|
123
|
-
bun run typecheck # strict project and the pptx project
|
|
124
|
-
bun run test
|
|
125
|
-
bun run build
|
|
126
|
-
bun run test:package # packs the build and imports every entry point from a clean install
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
The working agreements are in [AGENTS.md](AGENTS.md), and [PROVENANCE.md](PROVENANCE.md) records where each module came from.
|
|
130
|
-
|
|
131
|
-
## Documentation and related projects
|
|
132
|
-
|
|
133
|
-
- [docx-viewer](https://github.com/ChristopherVR/docx-viewer) and [pptx-viewer](https://github.com/ChristopherVR/pptx-viewer): the editors and viewers built on this package.
|
|
134
|
-
- [OOXML Office](https://christophervr.github.io/ooxml/): the suite's launcher page (`site/`), which opens the demos those viewers deploy to their own GitHub Pages sites.
|
|
135
|
-
- [ole2](https://github.com/ChristopherVR/ole2): the compound-file container and legacy binary Office codecs.
|
|
113
|
+
`ooxml-core` is developed in the [ChristopherVR/ooxml](https://github.com/ChristopherVR/ooxml) repository, which also holds [`ooxml-ui`](https://www.npmjs.com/package/ooxml-ui), the MCP servers and the Word, Excel, Visio and OpenTeams viewers. Its README covers the repository layout, the development workflow, releases and the roadmap; the working agreements are in [AGENTS.md](AGENTS.md) and [CONTRIBUTING.md](CONTRIBUTING.md), and [PROVENANCE.md](PROVENANCE.md) records where each module came from.
|
|
136
114
|
|
|
137
115
|
## License
|
|
138
116
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PowerPoint's automatic value-axis scale.
|
|
3
|
+
*
|
|
4
|
+
* A chart that declares no `c:min` / `c:max` / `c:majorUnit` leaves the bounds
|
|
5
|
+
* to the application, and PowerPoint does NOT simply run the axis to the largest
|
|
6
|
+
* data point. It anchors the axis at zero, pads the data, then rounds out to a
|
|
7
|
+
* round number and labels it in round steps. The reporter's deck (issue #132)
|
|
8
|
+
* shows the difference plainly: a percentage chart whose bars top out at 52%
|
|
9
|
+
* gets `0% 20% 40% 60%` from PowerPoint and got `0% 10% 21% 31% 42% 52%` here,
|
|
10
|
+
* because the axis ran to the data maximum and was then divided into five.
|
|
11
|
+
*
|
|
12
|
+
* The rules, from Microsoft's documented automatic-scaling behaviour:
|
|
13
|
+
*
|
|
14
|
+
* - **Zero anchor.** With all-positive data, the axis starts at zero unless the
|
|
15
|
+
* smallest value is at least 5/6 of the largest, in which case the data
|
|
16
|
+
* occupies a narrow band high up and a zero-anchored axis would flatten it.
|
|
17
|
+
* All-negative data mirrors this, ending at zero.
|
|
18
|
+
* - **Headroom.** The far end is pushed out by 5% of the data span so the
|
|
19
|
+
* tallest bar does not touch the top of the plot.
|
|
20
|
+
* - **Round steps.** The major unit is the first "nice" number (1, 2, 2.5 or 5
|
|
21
|
+
* times a power of ten) at or above `span / targetIntervals`, and the bounds
|
|
22
|
+
* are then snapped outwards to whole multiples of it.
|
|
23
|
+
*
|
|
24
|
+
* The snap is what makes the interval count differ from `targetIntervals`:
|
|
25
|
+
* rounding 0.544 up to a multiple of 0.2 lands on 0.6, which is three steps, not
|
|
26
|
+
* four. That is the intent, and it is why PowerPoint's axes read in round
|
|
27
|
+
* numbers while an evenly-divided one does not.
|
|
28
|
+
*/
|
|
29
|
+
/** A resolved automatic axis scale. */
|
|
30
|
+
export interface NiceAxisBounds {
|
|
31
|
+
min: number;
|
|
32
|
+
max: number;
|
|
33
|
+
/** Step between major gridlines; `max - min` is always a whole multiple. */
|
|
34
|
+
majorUnit: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* How many gridline intervals PowerPoint's automatic value-axis scale aims
|
|
38
|
+
* for, given the chart's plot-area height in the "px" unit `computePlotLayout`
|
|
39
|
+
* returns (96 px/inch; see `PX_PER_POINT` above).
|
|
40
|
+
*
|
|
41
|
+
* This is NOT a universal constant (see this module's original doc comment,
|
|
42
|
+
* which admitted as much: "matches the axes PowerPoint draws for a chart of
|
|
43
|
+
* ORDINARY height"): a short chart gets fewer, coarser gridlines than a tall
|
|
44
|
+
* one, up to a point, because PowerPoint only has room to label so many
|
|
45
|
+
* without the axis text crowding.
|
|
46
|
+
*
|
|
47
|
+
* Measured via PowerPoint COM automation (`New-Object -ComObject
|
|
48
|
+
* PowerPoint.Application`, `Shapes.AddChart2`, reading back
|
|
49
|
+
* `chart.Axes(2).MinimumScale/.MaximumScale/.MajorUnit`): building a fresh
|
|
50
|
+
* clustered-column chart at increasing shape heights with the SAME 8-point,
|
|
51
|
+
* single-category data (values 1.2-3.2, the exact case reported against this
|
|
52
|
+
* engine, which this engine rendered 0-4 while PowerPoint drew 0-3.5):
|
|
53
|
+
*
|
|
54
|
+
* | chart height | plot height | PowerPoint axis | gridlines |
|
|
55
|
+
* |--------------|-------------|------------------|-----------|
|
|
56
|
+
* | 100pt | ~33pt | 0 to 5, unit 5 | 1 |
|
|
57
|
+
* | 150pt | ~83pt | 0 to 4, unit 1 | 4 |
|
|
58
|
+
* | 200pt | ~133pt | 0 to 3.5, unit 0.5 | 7 |
|
|
59
|
+
* | 300pt | ~233pt | 0 to 3.5, unit 0.5 | 7 |
|
|
60
|
+
* | 1000pt | ~933pt | 0 to 3.5, unit 0.5 | 7 (unchanged) |
|
|
61
|
+
*
|
|
62
|
+
* Cross-checked against two more datasets (0-52 and 0-9) at the same set of
|
|
63
|
+
* heights to separate "the target interval count" from "which nice-number
|
|
64
|
+
* bucket a particular span happens to round into": PowerPoint never asked for
|
|
65
|
+
* more than about 10 intervals even at a 1000pt-tall chart, it only asks for
|
|
66
|
+
* FEWER on a plot area too short to fit that many labels. That saturation
|
|
67
|
+
* (not a straight line) is why this is clamped at the top, not just scaled.
|
|
68
|
+
*
|
|
69
|
+
* Modelled as one gridline interval per ~20pt of plot height (roughly one
|
|
70
|
+
* axis-label line plus padding), clamped to `[1, 10]`. This will not be exact
|
|
71
|
+
* for every data range: the actual PowerPoint algorithm also nudges the count
|
|
72
|
+
* by how the span happens to snap to a nice number (see the module doc
|
|
73
|
+
* comment), which this simplified model does not attempt to reproduce. It is
|
|
74
|
+
* a large improvement over hard-coding the short-chart answer (4) for every
|
|
75
|
+
* chart regardless of size, which is the bug this was added to fix.
|
|
76
|
+
*/
|
|
77
|
+
export declare function axisTargetIntervals(plotHeightPx: number): number;
|
|
78
|
+
/** The smallest nice step at or above `value`. */
|
|
79
|
+
export declare function niceAxisStep(value: number): number;
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the automatic bounds and major unit for a value axis.
|
|
82
|
+
*
|
|
83
|
+
* @param dataMin Smallest plotted value.
|
|
84
|
+
* @param dataMax Largest plotted value.
|
|
85
|
+
* @param targetIntervals Roughly how many gridline steps to aim for. The snap to
|
|
86
|
+
* a round unit means the result is usually one fewer. Defaults to 4 only
|
|
87
|
+
* when the caller has no idea how tall the chart's plot area is; pass
|
|
88
|
+
* {@link axisTargetIntervals}`(plotHeightPx)` instead whenever that height is
|
|
89
|
+
* known; see that function's doc comment for why a fixed constant undershot
|
|
90
|
+
* real PowerPoint on any chart of ordinary-or-larger size.
|
|
91
|
+
* @param headroom Fraction of the data span added beyond the far end. 2D
|
|
92
|
+
* charts get PowerPoint's 5%; a right-angle-axes 3D bar chart gets none
|
|
93
|
+
* (its axis on data topping out at exactly 5 ends at 5, see
|
|
94
|
+
* `chart-3d-oblique-layout.ts`).
|
|
95
|
+
*/
|
|
96
|
+
export declare function niceValueAxisBounds(dataMin: number, dataMax: number, targetIntervals?: number, headroom?: number): NiceAxisBounds;
|
|
97
|
+
//# sourceMappingURL=axis-nice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"axis-nice.d.ts","sourceRoot":"","sources":["../../src/chart/axis-nice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;CAClB;AAwCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAOhE;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASlD;AAUD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,eAAe,SAA2B,EAC1C,QAAQ,SAAW,GACjB,cAAc,CA6ChB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The `cx:layoutPr/cx:binning` properties that drive numeric binning. */
|
|
2
|
+
export interface HistogramOptions {
|
|
3
|
+
binSize?: number | undefined;
|
|
4
|
+
binCount?: number | undefined;
|
|
5
|
+
intervalClosed?: 'l' | 'r' | undefined;
|
|
6
|
+
underflow?: number | 'auto' | undefined;
|
|
7
|
+
overflow?: number | 'auto' | undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Formats a bin edge for a label. */
|
|
10
|
+
export type HistogramValueFormatter = (value: number) => string;
|
|
11
|
+
export interface HistogramBin {
|
|
12
|
+
value: number;
|
|
13
|
+
label: string;
|
|
14
|
+
sourceIndices: number[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Excel/PowerPoint's automatic histogram bin count when the author specifies
|
|
18
|
+
* neither `binSize` nor `binCount` (Scott's normal reference rule: bin width
|
|
19
|
+
* = 3.49 * sample standard deviation * n^(-1/3), then the count needed to
|
|
20
|
+
* span the data at that width). COM-verified against charts-com.pptx slide
|
|
21
|
+
* 30 / chartEx5.xml: 76 raw values spanning 1-24 (sample sd ~4.898) gave
|
|
22
|
+
* PowerPoint's own 6 bins of width 4 each; Scott's rule on the same data
|
|
23
|
+
* gives a raw width of ~4.035, which needs exactly 6 bins to span the range,
|
|
24
|
+
* matching PowerPoint bin-for-bin. The previous `ceil(sqrt(n))` fallback
|
|
25
|
+
* gave 9 bins on the same data, visibly narrower than PowerPoint's own.
|
|
26
|
+
*/
|
|
27
|
+
export declare function scottBinCount(values: ReadonlyArray<number>, min: number, max: number): number;
|
|
28
|
+
/** Bin raw observations according to ChartEx binning properties. */
|
|
29
|
+
export declare function computeHistogramBins(values: ReadonlyArray<number>, options: HistogramOptions, format?: HistogramValueFormatter): HistogramBin[];
|
|
30
|
+
/**
|
|
31
|
+
* Aggregate raw observations by their (repeated) category label instead of by
|
|
32
|
+
* numeric value range: `cx:layoutPr/cx:aggregation` (COM-verified against
|
|
33
|
+
* charts-com.pptx slide 31 / chartEx6.xml) means the source rows are
|
|
34
|
+
* CATEGORICAL, one row per occurrence, with the value column carrying a
|
|
35
|
+
* constant `1` so summing it counts occurrences per category. Summing
|
|
36
|
+
* (rather than literally counting rows) also does the right thing for a
|
|
37
|
+
* dataset that authors a real numeric measure per row instead of a bare `1`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function aggregateByCategory(values: ReadonlyArray<number>, rawCategories: ReadonlyArray<string>): HistogramBin[];
|
|
40
|
+
//# sourceMappingURL=histogram-binning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"histogram-binning.d.ts","sourceRoot":"","sources":["../../src/chart/histogram-binning.ts"],"names":[],"mappings":"AASA,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAChC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,cAAc,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,SAAS,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,sCAAsC;AACtC,MAAM,MAAM,uBAAuB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;AAEhE,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,EAAE,CAAC;CACxB;AAaD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAa7F;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,EAC7B,OAAO,EAAE,gBAAgB,EACzB,MAAM,GAAE,uBAAgC,GACtC,YAAY,EAAE,CA8EhB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,EAC7B,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,GAClC,YAAY,EAAE,CAUhB"}
|
package/dist/chart/index.cjs
CHANGED
|
@@ -219,11 +219,178 @@ function computeStackedSeriesPlots(seriesValues, catCount, percent) {
|
|
|
219
219
|
});
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// src/chart/axis-nice.ts
|
|
223
|
+
var NICE_STEPS = [1, 2, 2.5, 5, 10];
|
|
224
|
+
var HEADROOM = 0.05;
|
|
225
|
+
var POINTS_PER_GRIDLINE = 20;
|
|
226
|
+
var MIN_TARGET_INTERVALS = 1;
|
|
227
|
+
var MAX_TARGET_INTERVALS = 10;
|
|
228
|
+
var PX_PER_POINT = 4 / 3;
|
|
229
|
+
var DEFAULT_TARGET_INTERVALS = 4;
|
|
230
|
+
var ZERO_ANCHOR_RATIO = 5 / 6;
|
|
231
|
+
function axisTargetIntervals(plotHeightPx) {
|
|
232
|
+
if (!Number.isFinite(plotHeightPx) || plotHeightPx <= 0) {
|
|
233
|
+
return DEFAULT_TARGET_INTERVALS;
|
|
234
|
+
}
|
|
235
|
+
const plotHeightPt = plotHeightPx / PX_PER_POINT;
|
|
236
|
+
const raw = Math.round(plotHeightPt / POINTS_PER_GRIDLINE);
|
|
237
|
+
return Math.min(MAX_TARGET_INTERVALS, Math.max(MIN_TARGET_INTERVALS, raw));
|
|
238
|
+
}
|
|
239
|
+
function niceAxisStep(value) {
|
|
240
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
const exponent = Math.floor(Math.log10(value));
|
|
244
|
+
const power = 10 ** exponent;
|
|
245
|
+
const mantissa = value / power;
|
|
246
|
+
const step = NICE_STEPS.find((candidate) => mantissa <= candidate + 1e-12) ?? 10;
|
|
247
|
+
return step * power;
|
|
248
|
+
}
|
|
249
|
+
function snap(value, unit, direction) {
|
|
250
|
+
const scaled = value / unit;
|
|
251
|
+
const rounded = direction === "up" ? Math.ceil(scaled - 1e-9) : Math.floor(scaled + 1e-9);
|
|
252
|
+
return rounded * unit;
|
|
253
|
+
}
|
|
254
|
+
function niceValueAxisBounds(dataMin, dataMax, targetIntervals = DEFAULT_TARGET_INTERVALS, headroom = HEADROOM) {
|
|
255
|
+
if (!Number.isFinite(dataMin) || !Number.isFinite(dataMax)) {
|
|
256
|
+
return { min: 0, max: 1, majorUnit: 0.5 };
|
|
257
|
+
}
|
|
258
|
+
const low = Math.min(dataMin, dataMax);
|
|
259
|
+
const high = Math.max(dataMin, dataMax);
|
|
260
|
+
const intervals = Math.max(1, Math.round(targetIntervals));
|
|
261
|
+
if (low === high) {
|
|
262
|
+
if (low === 0) {
|
|
263
|
+
return { min: 0, max: 1, majorUnit: 1 / intervals };
|
|
264
|
+
}
|
|
265
|
+
const unit2 = niceAxisStep(Math.abs(low) / intervals);
|
|
266
|
+
return low > 0 ? { min: 0, max: snap(low, unit2, "up") || unit2, majorUnit: unit2 } : { min: snap(low, unit2, "down"), max: 0, majorUnit: unit2 };
|
|
267
|
+
}
|
|
268
|
+
const dataSpan = high - low;
|
|
269
|
+
const padding = dataSpan * headroom;
|
|
270
|
+
let paddedMin;
|
|
271
|
+
let paddedMax;
|
|
272
|
+
if (low >= 0) {
|
|
273
|
+
paddedMin = low < high * ZERO_ANCHOR_RATIO ? 0 : low - padding;
|
|
274
|
+
paddedMax = high + padding;
|
|
275
|
+
} else if (high <= 0) {
|
|
276
|
+
paddedMax = high > low * ZERO_ANCHOR_RATIO ? 0 : high + padding;
|
|
277
|
+
paddedMin = low - padding;
|
|
278
|
+
} else {
|
|
279
|
+
paddedMin = low - padding;
|
|
280
|
+
paddedMax = high + padding;
|
|
281
|
+
}
|
|
282
|
+
const unit = niceAxisStep((paddedMax - paddedMin) / intervals);
|
|
283
|
+
const min = paddedMin === 0 ? 0 : snap(paddedMin, unit, "down");
|
|
284
|
+
let max = paddedMax === 0 ? 0 : snap(paddedMax, unit, "up");
|
|
285
|
+
if (max <= min) {
|
|
286
|
+
max = min + unit;
|
|
287
|
+
}
|
|
288
|
+
return { min, max, majorUnit: unit };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/chart/histogram-binning.ts
|
|
292
|
+
function binLabel(lower, upper, closed, format) {
|
|
293
|
+
return closed === "r" ? `(${format(lower)}, ${format(upper)}]` : `[${format(lower)}, ${format(upper)})`;
|
|
294
|
+
}
|
|
295
|
+
function scottBinCount(values, min, max) {
|
|
296
|
+
if (values.length < 2 || max <= min) {
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
300
|
+
const variance = values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (values.length - 1);
|
|
301
|
+
const sd = Math.sqrt(variance);
|
|
302
|
+
if (!(sd > 0)) {
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
const width = 3.49 * sd * values.length ** (-1 / 3);
|
|
306
|
+
return Math.max(1, Math.ceil((max - min) / width));
|
|
307
|
+
}
|
|
308
|
+
function computeHistogramBins(values, options, format = String) {
|
|
309
|
+
const closed = options.intervalClosed ?? "l";
|
|
310
|
+
const finite = values.map((value, sourceIndex) => ({ value, sourceIndex })).filter((item) => Number.isFinite(item.value));
|
|
311
|
+
if (finite.length === 0) {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
const underflow = typeof options.underflow === "number" ? options.underflow : void 0;
|
|
315
|
+
const overflow = typeof options.overflow === "number" ? options.overflow : void 0;
|
|
316
|
+
const under = underflow === void 0 ? [] : finite.filter(
|
|
317
|
+
(item) => closed === "r" ? item.value <= underflow : item.value < underflow
|
|
318
|
+
);
|
|
319
|
+
const over = overflow === void 0 ? [] : finite.filter((item) => closed === "l" ? item.value >= overflow : item.value > overflow);
|
|
320
|
+
const regular = finite.filter((item) => !under.includes(item) && !over.includes(item));
|
|
321
|
+
const result = [];
|
|
322
|
+
if (underflow !== void 0) {
|
|
323
|
+
result.push({
|
|
324
|
+
value: under.length,
|
|
325
|
+
label: `${closed === "r" ? "\u2264" : "<"} ${format(underflow)}`,
|
|
326
|
+
sourceIndices: under.map((item) => item.sourceIndex)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (regular.length > 0) {
|
|
330
|
+
const min = Math.min(...regular.map((item) => item.value));
|
|
331
|
+
const max = Math.max(...regular.map((item) => item.value));
|
|
332
|
+
const requestedSize = options.binSize && options.binSize > 0 ? options.binSize : void 0;
|
|
333
|
+
const requestedCount = options.binCount && options.binCount > 0 ? options.binCount : void 0;
|
|
334
|
+
const start = requestedSize ? Math.floor(min / requestedSize) * requestedSize : min;
|
|
335
|
+
const count = requestedSize ? Math.max(
|
|
336
|
+
closed === "l" ? Math.floor((max - start) / requestedSize) + 1 : Math.ceil((max - start) / requestedSize),
|
|
337
|
+
1
|
|
338
|
+
) : Math.max(
|
|
339
|
+
requestedCount ?? scottBinCount(
|
|
340
|
+
regular.map((item) => item.value),
|
|
341
|
+
min,
|
|
342
|
+
max
|
|
343
|
+
),
|
|
344
|
+
1
|
|
345
|
+
);
|
|
346
|
+
const width = requestedSize ?? Math.max((max - start) / count, 1);
|
|
347
|
+
const bins = Array.from({ length: count }, (_, index) => ({
|
|
348
|
+
value: 0,
|
|
349
|
+
label: binLabel(start + index * width, start + (index + 1) * width, closed, format),
|
|
350
|
+
sourceIndices: []
|
|
351
|
+
}));
|
|
352
|
+
for (const item of regular) {
|
|
353
|
+
const rawIndex = closed === "r" ? Math.ceil((item.value - start) / width) - 1 : Math.floor((item.value - start) / width);
|
|
354
|
+
const index = Math.max(0, Math.min(rawIndex, bins.length - 1));
|
|
355
|
+
const bin = bins[index];
|
|
356
|
+
if (!bin) continue;
|
|
357
|
+
bin.value += 1;
|
|
358
|
+
bin.sourceIndices.push(item.sourceIndex);
|
|
359
|
+
}
|
|
360
|
+
result.push(...bins);
|
|
361
|
+
}
|
|
362
|
+
if (overflow !== void 0) {
|
|
363
|
+
result.push({
|
|
364
|
+
value: over.length,
|
|
365
|
+
label: `${closed === "l" ? "\u2265" : ">"} ${format(overflow)}`,
|
|
366
|
+
sourceIndices: over.map((item) => item.sourceIndex)
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
function aggregateByCategory(values, rawCategories) {
|
|
372
|
+
const { uniqueCategories, rowIndexesByCategory } = groupRowsByCategory(rawCategories);
|
|
373
|
+
return uniqueCategories.map((label) => {
|
|
374
|
+
const rows = rowIndexesByCategory.get(label) ?? [];
|
|
375
|
+
return {
|
|
376
|
+
value: rows.reduce((sum, row) => sum + (values[row] ?? 0), 0),
|
|
377
|
+
label,
|
|
378
|
+
sourceIndices: rows
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
exports.aggregateByCategory = aggregateByCategory;
|
|
384
|
+
exports.axisTargetIntervals = axisTargetIntervals;
|
|
222
385
|
exports.computeBoxStats = computeBoxStats;
|
|
386
|
+
exports.computeHistogramBins = computeHistogramBins;
|
|
223
387
|
exports.computeLinearRegression = computeLinearRegression;
|
|
224
388
|
exports.computeRSquared = computeRSquared;
|
|
225
389
|
exports.computeStackedSeriesPlots = computeStackedSeriesPlots;
|
|
226
390
|
exports.fitPolynomial = fitPolynomial;
|
|
227
391
|
exports.groupRowsByCategory = groupRowsByCategory;
|
|
392
|
+
exports.niceAxisStep = niceAxisStep;
|
|
393
|
+
exports.niceValueAxisBounds = niceValueAxisBounds;
|
|
228
394
|
exports.resolveBlankDisplay = resolveBlankDisplay;
|
|
395
|
+
exports.scottBinCount = scottBinCount;
|
|
229
396
|
exports.visibleRuns = visibleRuns;
|
package/dist/chart/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/chart/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/chart/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC"}
|
package/dist/chart/index.mjs
CHANGED
|
@@ -217,4 +217,165 @@ function computeStackedSeriesPlots(seriesValues, catCount, percent) {
|
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
// src/chart/axis-nice.ts
|
|
221
|
+
var NICE_STEPS = [1, 2, 2.5, 5, 10];
|
|
222
|
+
var HEADROOM = 0.05;
|
|
223
|
+
var POINTS_PER_GRIDLINE = 20;
|
|
224
|
+
var MIN_TARGET_INTERVALS = 1;
|
|
225
|
+
var MAX_TARGET_INTERVALS = 10;
|
|
226
|
+
var PX_PER_POINT = 4 / 3;
|
|
227
|
+
var DEFAULT_TARGET_INTERVALS = 4;
|
|
228
|
+
var ZERO_ANCHOR_RATIO = 5 / 6;
|
|
229
|
+
function axisTargetIntervals(plotHeightPx) {
|
|
230
|
+
if (!Number.isFinite(plotHeightPx) || plotHeightPx <= 0) {
|
|
231
|
+
return DEFAULT_TARGET_INTERVALS;
|
|
232
|
+
}
|
|
233
|
+
const plotHeightPt = plotHeightPx / PX_PER_POINT;
|
|
234
|
+
const raw = Math.round(plotHeightPt / POINTS_PER_GRIDLINE);
|
|
235
|
+
return Math.min(MAX_TARGET_INTERVALS, Math.max(MIN_TARGET_INTERVALS, raw));
|
|
236
|
+
}
|
|
237
|
+
function niceAxisStep(value) {
|
|
238
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
const exponent = Math.floor(Math.log10(value));
|
|
242
|
+
const power = 10 ** exponent;
|
|
243
|
+
const mantissa = value / power;
|
|
244
|
+
const step = NICE_STEPS.find((candidate) => mantissa <= candidate + 1e-12) ?? 10;
|
|
245
|
+
return step * power;
|
|
246
|
+
}
|
|
247
|
+
function snap(value, unit, direction) {
|
|
248
|
+
const scaled = value / unit;
|
|
249
|
+
const rounded = direction === "up" ? Math.ceil(scaled - 1e-9) : Math.floor(scaled + 1e-9);
|
|
250
|
+
return rounded * unit;
|
|
251
|
+
}
|
|
252
|
+
function niceValueAxisBounds(dataMin, dataMax, targetIntervals = DEFAULT_TARGET_INTERVALS, headroom = HEADROOM) {
|
|
253
|
+
if (!Number.isFinite(dataMin) || !Number.isFinite(dataMax)) {
|
|
254
|
+
return { min: 0, max: 1, majorUnit: 0.5 };
|
|
255
|
+
}
|
|
256
|
+
const low = Math.min(dataMin, dataMax);
|
|
257
|
+
const high = Math.max(dataMin, dataMax);
|
|
258
|
+
const intervals = Math.max(1, Math.round(targetIntervals));
|
|
259
|
+
if (low === high) {
|
|
260
|
+
if (low === 0) {
|
|
261
|
+
return { min: 0, max: 1, majorUnit: 1 / intervals };
|
|
262
|
+
}
|
|
263
|
+
const unit2 = niceAxisStep(Math.abs(low) / intervals);
|
|
264
|
+
return low > 0 ? { min: 0, max: snap(low, unit2, "up") || unit2, majorUnit: unit2 } : { min: snap(low, unit2, "down"), max: 0, majorUnit: unit2 };
|
|
265
|
+
}
|
|
266
|
+
const dataSpan = high - low;
|
|
267
|
+
const padding = dataSpan * headroom;
|
|
268
|
+
let paddedMin;
|
|
269
|
+
let paddedMax;
|
|
270
|
+
if (low >= 0) {
|
|
271
|
+
paddedMin = low < high * ZERO_ANCHOR_RATIO ? 0 : low - padding;
|
|
272
|
+
paddedMax = high + padding;
|
|
273
|
+
} else if (high <= 0) {
|
|
274
|
+
paddedMax = high > low * ZERO_ANCHOR_RATIO ? 0 : high + padding;
|
|
275
|
+
paddedMin = low - padding;
|
|
276
|
+
} else {
|
|
277
|
+
paddedMin = low - padding;
|
|
278
|
+
paddedMax = high + padding;
|
|
279
|
+
}
|
|
280
|
+
const unit = niceAxisStep((paddedMax - paddedMin) / intervals);
|
|
281
|
+
const min = paddedMin === 0 ? 0 : snap(paddedMin, unit, "down");
|
|
282
|
+
let max = paddedMax === 0 ? 0 : snap(paddedMax, unit, "up");
|
|
283
|
+
if (max <= min) {
|
|
284
|
+
max = min + unit;
|
|
285
|
+
}
|
|
286
|
+
return { min, max, majorUnit: unit };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/chart/histogram-binning.ts
|
|
290
|
+
function binLabel(lower, upper, closed, format) {
|
|
291
|
+
return closed === "r" ? `(${format(lower)}, ${format(upper)}]` : `[${format(lower)}, ${format(upper)})`;
|
|
292
|
+
}
|
|
293
|
+
function scottBinCount(values, min, max) {
|
|
294
|
+
if (values.length < 2 || max <= min) {
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
298
|
+
const variance = values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (values.length - 1);
|
|
299
|
+
const sd = Math.sqrt(variance);
|
|
300
|
+
if (!(sd > 0)) {
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
const width = 3.49 * sd * values.length ** (-1 / 3);
|
|
304
|
+
return Math.max(1, Math.ceil((max - min) / width));
|
|
305
|
+
}
|
|
306
|
+
function computeHistogramBins(values, options, format = String) {
|
|
307
|
+
const closed = options.intervalClosed ?? "l";
|
|
308
|
+
const finite = values.map((value, sourceIndex) => ({ value, sourceIndex })).filter((item) => Number.isFinite(item.value));
|
|
309
|
+
if (finite.length === 0) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
const underflow = typeof options.underflow === "number" ? options.underflow : void 0;
|
|
313
|
+
const overflow = typeof options.overflow === "number" ? options.overflow : void 0;
|
|
314
|
+
const under = underflow === void 0 ? [] : finite.filter(
|
|
315
|
+
(item) => closed === "r" ? item.value <= underflow : item.value < underflow
|
|
316
|
+
);
|
|
317
|
+
const over = overflow === void 0 ? [] : finite.filter((item) => closed === "l" ? item.value >= overflow : item.value > overflow);
|
|
318
|
+
const regular = finite.filter((item) => !under.includes(item) && !over.includes(item));
|
|
319
|
+
const result = [];
|
|
320
|
+
if (underflow !== void 0) {
|
|
321
|
+
result.push({
|
|
322
|
+
value: under.length,
|
|
323
|
+
label: `${closed === "r" ? "\u2264" : "<"} ${format(underflow)}`,
|
|
324
|
+
sourceIndices: under.map((item) => item.sourceIndex)
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (regular.length > 0) {
|
|
328
|
+
const min = Math.min(...regular.map((item) => item.value));
|
|
329
|
+
const max = Math.max(...regular.map((item) => item.value));
|
|
330
|
+
const requestedSize = options.binSize && options.binSize > 0 ? options.binSize : void 0;
|
|
331
|
+
const requestedCount = options.binCount && options.binCount > 0 ? options.binCount : void 0;
|
|
332
|
+
const start = requestedSize ? Math.floor(min / requestedSize) * requestedSize : min;
|
|
333
|
+
const count = requestedSize ? Math.max(
|
|
334
|
+
closed === "l" ? Math.floor((max - start) / requestedSize) + 1 : Math.ceil((max - start) / requestedSize),
|
|
335
|
+
1
|
|
336
|
+
) : Math.max(
|
|
337
|
+
requestedCount ?? scottBinCount(
|
|
338
|
+
regular.map((item) => item.value),
|
|
339
|
+
min,
|
|
340
|
+
max
|
|
341
|
+
),
|
|
342
|
+
1
|
|
343
|
+
);
|
|
344
|
+
const width = requestedSize ?? Math.max((max - start) / count, 1);
|
|
345
|
+
const bins = Array.from({ length: count }, (_, index) => ({
|
|
346
|
+
value: 0,
|
|
347
|
+
label: binLabel(start + index * width, start + (index + 1) * width, closed, format),
|
|
348
|
+
sourceIndices: []
|
|
349
|
+
}));
|
|
350
|
+
for (const item of regular) {
|
|
351
|
+
const rawIndex = closed === "r" ? Math.ceil((item.value - start) / width) - 1 : Math.floor((item.value - start) / width);
|
|
352
|
+
const index = Math.max(0, Math.min(rawIndex, bins.length - 1));
|
|
353
|
+
const bin = bins[index];
|
|
354
|
+
if (!bin) continue;
|
|
355
|
+
bin.value += 1;
|
|
356
|
+
bin.sourceIndices.push(item.sourceIndex);
|
|
357
|
+
}
|
|
358
|
+
result.push(...bins);
|
|
359
|
+
}
|
|
360
|
+
if (overflow !== void 0) {
|
|
361
|
+
result.push({
|
|
362
|
+
value: over.length,
|
|
363
|
+
label: `${closed === "l" ? "\u2265" : ">"} ${format(overflow)}`,
|
|
364
|
+
sourceIndices: over.map((item) => item.sourceIndex)
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
function aggregateByCategory(values, rawCategories) {
|
|
370
|
+
const { uniqueCategories, rowIndexesByCategory } = groupRowsByCategory(rawCategories);
|
|
371
|
+
return uniqueCategories.map((label) => {
|
|
372
|
+
const rows = rowIndexesByCategory.get(label) ?? [];
|
|
373
|
+
return {
|
|
374
|
+
value: rows.reduce((sum, row) => sum + (values[row] ?? 0), 0),
|
|
375
|
+
label,
|
|
376
|
+
sourceIndices: rows
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export { aggregateByCategory, axisTargetIntervals, computeBoxStats, computeHistogramBins, computeLinearRegression, computeRSquared, computeStackedSeriesPlots, fitPolynomial, groupRowsByCategory, niceAxisStep, niceValueAxisBounds, resolveBlankDisplay, scottBinCount, visibleRuns };
|