Merge remote-tracking branch 'gh/main' into worktree-agent-a97453f7fa65d51b9

This commit is contained in:
Edward Langley
2026-09-20 21:36:21 -07:00
63 changed files with 4250 additions and 2616 deletions
+38 -32
View File
File diff suppressed because one or more lines are too long
+32 -31
View File
@@ -238,26 +238,19 @@ optimising for: `http://localhost` is not a secure-context problem, so nothing
below is needed to develop, to demo locally, or to share a TUI-hosted document
with another tab on the same machine.
One caveat even there, for the reason spelled out below: a WebSocket is exempt
from the same-origin policy, and **nothing in the server checks `Origin`** — so
a page you did not open can connect to `ws://127.0.0.1:8787` and read or edit
the document while the server runs.
The server checks `Origin` on the handshake, which is what makes that safe. A
WebSocket is exempt from the same-origin policy — there is no CORS preflight —
so without the check any page you visited could connect to `ws://127.0.0.1:8787`
and read or edit the document while the server runs. **Pages served from
loopback are allowed by default**, which is exactly the recipe above; every
other page is refused unless you name it with `--allow-origin`. A client that
sends no `Origin` at all — curl, a script, a native client — is allowed, because
no browser omits it and refusing them would answer a question `--bind` already
answers.
How much the *browser* mitigates that is in flux and should not be leaned on.
Chromium has been rolling out **Local Network Access**, which puts requests
from a public-network origin to a loopback or private-network address behind a
permission prompt; per Chrome's own documentation WebSockets were **not yet**
covered when the prompt began shipping, with coverage planned, so whether a
given browser and version gates *this* socket is not something this project can
state — and nothing here can test it: the Playwright harness serves a loopback
origin, so Local Network Access never engages in it. Two gaps are structural
rather than version-dependent: it is a Chromium feature, so other browsers need
not behave the same; and it only engages for a **public** origin, so the
locally-served page this section is about is outside it entirely.
Treat it, then, as tolerable for a local session you start and stop, and as a
reason not to leave one running unattended. The durable fix is the server
checking `Origin` itself, which it does not yet do.
This is not authentication (see the next section) and it does not depend on the
browser: whatever Chromium's **Local Network Access** work ends up covering, the
handshake is refused here rather than at the browser's discretion.
### Sharing a TUI-hosted document with a remote browser
@@ -311,9 +304,13 @@ any hostile page an allowed user visits can open a socket to your endpoint
*from that user's own allowed address*, and read and write the document. An
address check cannot see that, because by then the request genuinely does come
from an allowed client. Only the **`Origin` header** distinguishes them, and
`improvise-ws-server` **never looks at it** — it accepts every handshake. So a
proxy needs an Origin allow-list **in addition to**, never instead of,
whichever control below you pick. The worked example carries both.
`improvise-ws-server` checks it: pages served from loopback are allowed, and
anything else must be named with `--allow-origin`. **Deploying behind a proxy
therefore needs the flag** — the deployed page's origin is
`https://fiddlerwoaroof.github.io`, which is not loopback. Keep the Origin
allow-list in the proxy as well: it is defence in depth now rather than the only
defence, and it is what covers a proxy configuration that strips or rewrites the
header before the server can see it.
So the honest controls are the ones that need **no credential from the page**:
@@ -352,11 +349,11 @@ improvise.example.com {
@denied not remote_ip 203.0.113.4 198.51.100.0/24
abort @denied
# ALSO not optional, and it is NOT redundant with the address check.
# A WebSocket is exempt from the same-origin policy, so any hostile page
# an allowed user visits can open this socket FROM THAT ALLOWED ADDRESS
# and read and edit the document. The address check cannot see that; only
# the Origin can. Name the exact origins the page is served from.
# Defence in depth, and NOT redundant with the address check. A WebSocket
# is exempt from the same-origin policy, so any hostile page an allowed
# user visits can open this socket FROM THAT ALLOWED ADDRESS. The address
# check cannot see that; only the Origin can. The server checks it too —
# this is what covers a proxy that strips or rewrites the header.
@badorigin not header Origin https://fiddlerwoaroof.github.io
abort @badorigin
@@ -365,8 +362,11 @@ improvise.example.com {
```
```sh
improvise-ws-server bank-info.improv --port 8787 # still plain ws, still loopback
caddy run # terminates TLS, proxies to it
# --allow-origin is required here: the deployed page is not a loopback origin,
# so without it the server refuses the handshake the proxy forwards.
improvise-ws-server bank-info.improv --port 8787 \
--allow-origin https://fiddlerwoaroof.github.io # still plain ws, still loopback
caddy run # terminates TLS, proxies to it
```
On the same terms as the browser caveat below: **this configuration has not
@@ -390,8 +390,9 @@ answered as an ordinary request and the socket never opens.
For a one-off share with no DNS name and no certificate of your own,
`cloudflared tunnel --url http://127.0.0.1:8787` or `ngrok http 8787` each
print an https hostname that works the same way — pass the hostname to `?ws=`
without a scheme. Both print a **public** URL and neither authenticates
anything by default, and an unguessable URL is not a control.
without a scheme, and pass the page's own origin to `--allow-origin`, since it
is not loopback. Both print a **public** URL and neither authenticates anything
by default, and an unguessable URL is not a control.
Two things carry over from above. `ngrok http --basic-auth` does **not** help
here — the page cannot answer a basic-auth challenge on a socket handshake —
+261 -1
View File
@@ -255,6 +255,58 @@ document it serves, so it binds a loopback address by default and `serve` says
so. `--bind` exists so the limit is a choice you can see rather than one buried
in the code. ADR 001 owns what multi-writer safety becomes.
### Origin
`src/origin.rs` is the handshake's one access control, and the reason it cannot
be delegated: **a websocket handshake is exempt from the same-origin policy**.
There is no CORS preflight, so any page a user visits can open a socket to the
endpoint *from that user's own address*, and read and edit every document the
server holds — cross-site websocket hijacking (improvise-kc7n). An address
check cannot see it, because the request genuinely comes from an allowed
address; an IP allow-list, a private network and loopback binding all answer
"may this **client** reach the endpoint", and the attacker's answer is honestly
yes. Only `Origin` says what is **driving** the client, which is
`doctrine(who_may_connect_is_two_questions)`. **Loopback is the least protected
case, not the safest**: there is no proxy in front of it to check anything, and
loopback is not a boundary a browser enforces, so while the server runs any page
open in that browser can reach `ws://127.0.0.1:8787`.
**The default allows loopback pages and nothing else**, because a default that
fails open is worth nothing and a default that breaks the shipped recipe is a
different bug. The two shipped paths were the constraint. The local pairing
recipe serves the page with `python3 -m http.server 8080 --directory web`, so
its origin is `http://localhost:8080` while the socket is on another loopback
port — loopback is therefore the *default*, not a flag, or the one case worth
optimising for would need configuring. The deployed Pages page sends
`https://fiddlerwoaroof.github.io` and can only reach the server through a TLS
proxy (`improvise-ws-server` speaks plain `ws://`), which is already a
multi-step setup with an Origin allow-list in it; one `--allow-origin` is
therefore an honest cost there and the project's own deployment origin is not
hard-coded as a default. `OriginPolicy::permits` is the whole decision, and the
transport's `admitted` is the only caller.
**An absent `Origin` is allowed, decided rather than defaulted.** Every browser
sends one on a handshake, unconditionally — unlike `fetch`, there is no
no-`Origin` mode to fall into — so an absent one is a non-browser client: curl,
a native client, this crate's own `tests/socket.rs`. Refusing those would break
the repo's own tests and every scripted client while buying nothing, since an
attacker who can run a non-browser client already has direct network access,
which is the question `--bind` and a proxy answer. The cost is stated rather
than hidden: **a proxy that strips `Origin` turns this check off**, so the
proxy's own Origin allow-list stays in the recipe as defence in depth.
Two shapes state no origin a policy can answer for, and both are refused rather
than read as absent: an `Origin` that is not valid text, and **two `Origin`
headers**. `HeaderMap::get` is first-wins, and the deployment this README
recommends puts a proxy in front of the socket — one that appends rather than
sets the header leaves two, and answering from the first lets the second be
anything at all. No browser sends two.
**What this does not buy is authentication.** It answers "what page is driving
this client", never "who is connecting"; anything that is not a browser page
sends no `Origin` and is unaffected by any of it. improvise-kpnr is still open
and is a different question.
---
## `improvise-protocol/src/resolve.rs` — one keypress → `Command`s
@@ -537,6 +589,82 @@ The classes that remain:
- **Drill.** Session-local residue, and the one case that does **not** go
upstream silently — see below.
#### The three improvise-e24v listed as free, and what each really costs
improvise-4bwa. Each of the three is short exactly one fact, and the three
answers are different in kind — which is the point, because "needs enrichment"
read as one bucket is what let them be listed as free.
**`view-back` is declined, permanently as things stand, and it is not an
enrichment at all.** `ViewNavigate { forward: false }` emits
`ApplyAndClearDrill` *before* `ViewBack`, and `ApplyAndClearDrill` is
deliberately off the wire: its operand is `drill_state.pending_edits`, session-
local residue belonging to whoever staged it, and a `Command::Model` is routed
with no `SessionId` (gotcha 38, improvise-3x3u). A client that mirrored
`view-back` by reconstructing the drill commit would commit **somebody else's
uncommitted typing** — which is the exact thing 3x3u removed, and the reason it
must not be "mirrored line for line" here despite that being the rule everywhere
else in this module. So it stays upstream until ADR 001 layer 2 gives the
addressed form. Note the sharper reading: `mirror_the_cmd_line_for_line` is a
rule about *resolvers*, and a `Cmd` whose first effect is unaddressable is not a
`Cmd` a thin client may mirror at all. The doctrine does not conflict with this;
it simply does not reach it.
**`toggle-prune-empty` needs one bool on a per-view projection.**
`TogglePruneEmpty::execute` reads `ctx.view.prune_empty` only to word its status
— "Showing all rows/columns" against "Hiding empty rows/columns". The
`ModelCommand` itself is composable today; it is the sentence beside it that a
client cannot write, so mirroring it would put the two ends out of step on the
status line in exactly half of all presses. `View::prune_empty` is on the `View`
inside the `Workbook`, in no widget cache and not in `ViewState`.
**`delete-formula-at-cursor` needs an element-type change, and the shape of the
change is the whole decision.** The effect is `RemoveFormula { target,
target_category }`; `FormulaCache` carries the formula's *raw text*
(`"Profit = Revenue - Cost"`), from which neither field is recoverable without
reimplementing `parse_formula` in the client. Adding a `targets` vector
**beside** `formulas` would be two answers to "what is in the formula panel",
which is improvise-edp's disease and the mistake improvise-anr9 had to pay a
type change to undo. So the element type grows the fields; it does not acquire a
parallel array.
Both of those are ordinary projection additions and both live in
`improvise-protocol` (`render.rs`, `resolve.rs`) and `improvise-command`
(`ui/project.rs`) — not in this crate, which is why this section records the
design and the work lands with whoever owns those files. Whatever lands, the
named list `the_command_names_that_still_need_the_host_are_these` shrinks by
exactly what closed.
#### What a client that could parse would cost: measured
improvise-cl7y asks a client to stop claiming "Formula added" for text only the
host can parse. Its own note says the two routes are a dependency decision or a
status addressed back to the composer, and leaves the cost of the first
unmeasured. It is measured now, by adding `improvise-formula` to this crate,
calling `parse_formula` from an exported method so nothing is stripped, and
building the bundle both ways:
| | `improvise_wasm_client_bg.wasm` | gzipped |
|---|---|---|
| today | 503,854 | 191,613 |
| with `improvise-formula` | 634,515 | 238,973 |
| cost | **+130,661 (+25.9%)** | **+47,360 (+24.7%)** |
The raw column is exact and byte-verified; the gzipped one is a `gzip -9`
**proxy** and not what a visitor downloads, since GitHub Pages serves brotli.
The conclusion is unaffected — the raw figure alone carries it — but do not
quote "+47,360 gzipped" as a transfer size.
A quarter of the download, for a status string. That is the evidence for the
other route — the host addressing a status back to the composer, the way
`ClientFrame::Save`'s rejection already does — and it costs no dependency.
Note what the measurement does **not** settle, because it is the more
interesting half: a client that could parse would be a **second derivation** of
whether a formula is well formed, agreeing with the host only for as long as
both call the same function. The status line is not the only thing that wants
it, so the number above is a floor rather than the price of a feature.
### `stranding`: a prompt this client can enter but cannot commit
`enter-mode` is trivially resolvable here, and that is the trap: the user
@@ -1112,7 +1240,7 @@ reaching it from https means terminating TLS in front of it). From an https page
an explicit `ws://` is **upgraded** rather than passed through, since passing it
through can only produce a failure the user cannot act on.
### `set_viewport`, not `Client.viewport`
### `set_viewport`, and the twin that is gone
Same frame, but it also **records** the window. A client learns its window only
from what it announced — the row frame's own length is the window clipped to the
@@ -1121,6 +1249,23 @@ arithmetic matters. Without it the client cannot clamp, so every cursor key goes
upstream into `Host::handle`'s empty vec and the arrow keys silently do nothing
(improvise-bpq2).
There used to be a frame-only twin, `Client::viewport`, exported beside it and
called by nobody. Its doc said it was kept because `web/app.js` already spelled
it — and `web/app.js` called `set_viewport` in the same wave, so the reason was
false before it was read (improvise-seik). That matters more than an ordinary
unused function, because **the two are one letter apart and the wrong one fails
silently**: it sends an identical frame and simply forgets the window, which
turns every cursor key into the black hole above. It is deleted, along with the
exported `init()` free function that duplicated `new Client()`.
A deletion is not the guard, though — the next export with no caller arrives the
same way. `web/tests/bundle/wasm_surface.test.js` now reads the generated
`.d.ts` and requires every declared method to be one `app.js` calls, or to be
named in an `UNCALLED` table with its reason. `free` and `initSync` are
wasm-bindgen's own; `revision` is there because `on_frame` returns it. Proved by
injecting a spare `static viewport(...)` declaration into the built `.d.ts` and
watching it fail by name.
### `frameCost`: what the grid's own frame costs, in whole cells
One header row and one header column was right until improvise-anr9: the column
@@ -1216,6 +1361,70 @@ Neither arm leaves one armed.
1200 reads `"1,200"` and committing that stores the *string* `"1,200"`. Enter
removes the focused `<input>`, which fires `blur`, which calls `close` again:
harmless, but only by accident.
- **The title bar is three fields and one joining.** `doc_name`, `doc_dirty` and
`doc_stored_as` come off the cache `src/draw.rs::draw_title` folds; the joining
— `name (stored_as) [+]`, in that order — is `shell.js::documentTitle`, which
is the TUI's own spelling minus the `improvise ·` chrome and the right-hand
key strip. The three were projected by improvise-z754 and painted by nothing
for a wave, and `render_surface.test.js` could not see it: that check is a
**substring search**, so a field named by a function nothing calls reads as
painted. What sees it is `contract.spec.js`, which loads the page and asserts
the text (improvise-5jna). The marker itself is a node test rather than a
harness one, because the standalone deployment persists on every accepted
batch and clears the flag in the same round trip, so `[+]` is transient there.
### The selection scrolls itself into view
`web/app.js` called `scrollIntoView` nowhere at all, so a selection driven past
the bottom of `#grid` stayed there, invisible, with no way back but scrolling by
hand (improvise-jq8l). Two things about it are easy to get wrong.
**It is not a projection bug and not a `row_offset` bug.** The row had been sent:
`#grid` has `overflow: auto` and the page announces an extent from `ROW_PX`/
`COL_PX`, which **under-estimate** — a real row is 28.6px against the assumed 26
— and inline group rows are deliberately not charged for at all. So the page
renders more rows than its own box can show, and the overflow is the DOM's, not
the window's. Measured on `harness/fixtures/tall.improv`: 105px of overflow, the
selected cell 96px below the visible bottom, `scrollTop` still 0.
**`block: "nearest"` is the whole option.** It scrolls only when the element is
actually out of view, and it scrolls the nearest scroll container rather than the
document, which is what keeps the page itself from moving sideways.
The reason this was untestable until now is the fixture: `web/demo.improv` is
four rows and `harness/fixtures/grouped.improv` is eight, and neither overflows a
484px box. `tall.improv` is thirty rows in three groups, and the spec asserts the
overflow exists **before** asserting anything about the scroll — without that
line the whole test passes on a page that never scrolls.
### The pointer takes cells on `mousedown`
A cell's handler is `mousedown` with `preventDefault()`, not `click`, and the
reason is an ordering trap worth the paragraph (improvise-0x9u). From an
**editing** state a `click` never arrived at all: the mousedown moved focus, the
`<input>`'s `blur` ran `close`, `close` repainted, `replaceChildren` destroyed
the `<td>` the mousedown had landed on, and no `click` was ever dispatched. The
pointer was dead until the user pressed Escape or clicked outside the grid.
Three things had to move together, and each is load-bearing:
- **`mousedown`, so the handler runs before any focus change.**
- **`preventDefault()`, so no focus change happens at all.** Otherwise the
default action focuses `#grid` *after* the handler has called `input.focus()`
on the new editor, and blurs it immediately. The keyboard listener is on
`window`, so nothing needs `#grid` focused.
- **`close` no longer repaints, and `blur` checks `closed` first.** Removing a
focused `<input>` fires `blur` **synchronously**, so a `close` that repainted
from inside itself detached the very `<td>` the handler was about to open an
editor on. This is the same repair one level down, and it is why the first
attempt at the fix still failed: the `blur` arm re-entered and rebuilt the
table. `close` restores the **drawn** text rather than the seed, since nothing
repaints behind it any more.
The state the old spec could not see is the whole point: it pressed Escape
before clicking the second cell, so it only ever clicked from a non-editing
page — this repo's characteristic defect, a test that passes while the thing it
names is broken in a neighbouring state.
### The keyboard
@@ -1284,6 +1493,57 @@ where one bad frame would poison every later one.
---
## `docs/` — the landing page and its casts
### The casts are recorded by a script, not by `asciinema rec`
`docs/record-casts.py` drives `improvise` on a pty it owns and writes asciicast
v2 itself. `asciinema rec` records a **shell**, and the shell it records is the
recorder's own: every previous cast opened and closed on
`(0) Mac:<hostname> ~gf/u/edwlan/improvise`, on what improvise-kh8 had since
made the site root, and `formulas.cast` and `import.cast` showed that prompt as
their visible **opening frame** (improvise-2oxt).
The repair is structural rather than careful: `improvise` is the pty's only
child, so there is no shell for a prompt to leak from — nothing to remember to
clean, and nothing that comes back when somebody re-records on a different
machine. Three other things stopped being accidents at the same time. The
**geometry** is an input (120×37, the numbers `docs/index.html` already spelled,
which is why they were not changed — a 30-row player over a 37-row cast clips
the tile bar and the status line, exactly where the output is). The **pacing**
is a per-step table rather than typing speed. And the header carries **no `env`
and no `timestamp`**: the first held `$SHELL`, the second the recording
machine's clock, and neither is anything a visitor is owed.
Two decisions inside the scripts that are not obvious and cost a re-record each:
- **No `:q` is typed at the end.** The player loops, so the last frame is also
the frame before the first, and a minibuffer reading `:q` is what it would
show. The process is signalled instead.
- **Each scenario has to be arranged so the thing it names is visible.** The
drill cast drilled into a cell with no records behind it — a true frame about
nothing — so it now moves `Customer` off the page axis first, which makes the
cell an aggregate over nine customers and gives the records grid more than one
row. The pivot cast pressed `c` on a category that was already on the column
axis, which is a no-op with a status line. Neither was visible from the
keystrokes; both were found by rendering the recording back and reading it.
That last point is the method, and it is reusable: a cast can be replayed
through a small ANSI reconstruction and read as text, so "what does frame N
actually show" is a question with an answer rather than a guess. It is how the
`+cat` and `+` columns in `drill.cast` — improvise-2oxt's substantive
complaint, since the old recording predated both — were confirmed present.
What the harness can check, and does: `landing.spec.js` asserts over the cast
**files** that no header carries `env` or `timestamp`, that the geometry is the
one the page configures, and that no chunk contains an OSC window-title
sequence, which is the leak's signature — `improvise` emits none and a shell
emits one per prompt, carrying the cwd. Asserted over the file rather than the
rendering because that is where the leak lives: a player draws a prompt as
ordinary text, and a screenshot assertion would have to know what the hostname
had been.
---
## `harness/` — the browser instrument
### Why it exists, and what belongs in it
+23
View File
@@ -39,6 +39,29 @@ differences are the whole content of the design (improvise-kdp):
for `SUM`/`COUNT`, which have an identity, and an error for
`AVG`/`MIN`/`MAX`, which do not.
### `evaluate_aggregated` takes `Aggregation`, not a list it cannot read
The parameter was `none_cats: &[String]`, and the body consulted it exactly
once, for emptiness — a boolean wearing a list's type, since the aggregation
itself runs over `key.0` through `matching_values`. It never filtered by that
list, in any revision: `git log -S` back to the commit that introduced
`Axis::None` shows the same `is_empty()` and nothing else. So the name promised
a filter the body has never performed, and the next caller to build a careful
list would have threaded it into a parameter that cannot use it.
`Aggregation::{Exact, OverHidden}` admits only the two states the body
distinguishes, and `Aggregation::of(none_cats)` performs the reduction once, at
the boundary that holds the list (improvise-5jm5,
`doctrine(an_invariant_is_made_unrepresentable_not_validated)` — the same answer
`prune_axis_items` and `gp-tree`'s `Row<T>` got). It is an enum rather than a
`bool` because the two states have names worth reading at the call site, and the
regression guard is the **signature**: the thing the ticket describes — two
different lists, same emptiness, same answer — is no longer constructible.
Note what does *not* change: `recompute_formulas` and `aggregate_raw` keep
`&[String]`, because they walk the elements (stripping each hidden coordinate
off a stem). Two parameters that looked alike were doing two different jobs.
### Non-numeric values reduce by unanimity, and the branch order is the rule
`aggregate_cell_values` checks the **all-numeric branch first**. If every value
+49 -1
View File
@@ -92,6 +92,28 @@ round-tripped from the day it exists.
What the *evaluator* does with it, and why a fitted tree is always wrapped in a
`SUM`, is `context/code/core.md` §13.
### `WHERE` carries the same boundary, and the mechanism has to be generatable
`where_clause` and `inline_where` are compound-atomic with `sp+` after the
keyword, exactly as `is_expr` is (improvise-r08u). Non-atomic, pest auto-skipped
`WHITESPACE` after `^"WHERE"`, so `SUM(Rev WHEREAS x = y)` read as a `WHERE` on
a category `AS` — a silently wrong filter where it should have been a syntax
error, the same class of defect `IS` had.
Two things decided the shape, and the second is the one worth carrying. A
negative lookahead `!bare_char` with `sp*` would have been *strictly* more
permissive — it keeps `WHERE|Region|=East`, which the old rule accepted — but
**the grammar-walking generator cannot honour a predicate**: it walks
`formula.pest` through `pest_meta` and emits `WHEREa=""`, text the grammar then
refuses, so `generated_formula_parses` goes red. A rule whose only guard is a
lookahead is a rule the generator can violate. `sp+` is generatable, matches
`IS`, and costs exactly one accepted spelling: a category may no longer abut the
keyword, so `WHERE |Region|` needs its space.
Note compound-atomicity *alone* closes the ticket's own reproducer, because
`AS x` cannot be one identifier once implicit whitespace is gone. `sp+` is what
closes the residue, `WHEREAS=y`, and the negative test names both.
## A formula target is unquoted, exactly as a reference is
`parse_formula` strips the pipes off the target the same way it does off an
@@ -158,6 +180,18 @@ format order-independent — a `## View:` may name a category defined further
down. `PCategory::items` is a `Vec` in file order because `IndexMap` insertion
order is load-bearing (gotcha 18), so pass 2 must replay the list as written.
Two `## Category:` sections naming one category are **one** category, and the
merge is a concatenation performed in pass 1 (`ParsedFile::push_category`,
improvise-349m). Merging in pass 2 — building each section in turn — was not
idempotent. A group is created either by the item that names it or by a `>`
line, and `build_category` creates its own section's item-groups *before* its
own `>` lines; so the first section's `>` groups landed ahead of the second
section's item-groups, while `format_md` writes one section with all the items
before all the `>` lines. The reparse then built the other order, and the two
`format_md` outputs differed. Concatenating first makes the built order the
written order by construction. Refusing a duplicate header was the alternative
and would have made a legal hand-written file an error for no gain.
### A section header takes its name as the whole line, totally
`header_name = { pipe_quoted ~ &NEWLINE | line_name }` (improvise-pczv). The
@@ -390,7 +424,21 @@ and the derived `_Month`/`_Quarter` categories inherit the error. What makes it
tolerable is that the wizard shows the detected format and lets the user change
it — the same argument two-digit years already rested on. It is pinned by a test
so that changing the answer is a decision somebody makes rather than a side
effect of reordering the table. Filed as **improvise-l8rh**.
effect of reordering the table.
**Ruled a known limit** (improvise-l8rh, the user, 2026-09-20). Two things the
ruling rests on. Changing the default to day-first only moves which half of the
world imports silently wrong; and the honest fix is larger than the heuristic —
an ambiguous column should *surface* its ambiguity rather than resolve it, which
changes `FieldProposal`'s shape and the wizard step that displays it, the way
`Dataset::warnings` returns a decision the user should hear about as data. Worth
doing deliberately, not as a patch.
What makes the limit reasonable to live with is that it is now *deterministic*:
`analyze_records` iterated a `HashSet` until improvise-7yx, so the ten samples
`detect_date_format` inspects were a different ten every run and a column whose
eleventh value ruled out `%m/%d/%Y` detected a different format run to run. The
ten-sample window itself is unchanged and is part of the limit.
### CSV parsing
+74 -13
View File
@@ -477,20 +477,30 @@ the top edge (improvise-jjx).
**`body_from(idx)`** drops the group header that owns the first drawn item: a
frame opening mid-group carries that header so `group_of` can answer for it,
but it is *above* the first drawn row and must not be painted as one. This is
the same cut `GridLayout::data_row_to_visual` has always made. Its argument
exists only because the two front ends scroll differently — a browser's frame
is already windowed and passes `0`, the TUI's covers the whole grid and passes
its row offset (gotcha 44) — **not** because the rule differs.
the same cut `GridLayout::data_row_to_visual` has always made.
**`group_of` answers "nearest preceding header", not membership, and
deliberately so: it is what the TUI has always painted.** `expand_category`
emits a header on *entering* a group and no boundary on leaving one, so an
ungrouped item following a grouped run is answered with the run's group — and
the TUI's column-group row spans it under that label today. Encoding
membership, or an end boundary, means changing `AxisEntry` in
`improvise-core` and changing what the TUI paints: a fix to the layout, not to
this projection of it, and it is filed rather than smuggled into the migration
that made the two ends agree (improvise-anr9, raised by Copilot on PR #23).
Two things about its argument, and the first of them **used to be the whole
story and was wrong**. It exists partly because the two front ends scroll
differently — a browser's frame is already windowed and passes `0`, the TUI's
covers the whole grid and passes its row offset (gotcha 44). But `idx` alone
cannot say whether the frame *leads the axis*, and at the top there is no
header "above" to drop: it is content, and a collapsed first group has no
other affordance. That is `AxisFrame::first` and improvise-0o6x — §3a.
**`group_of` answers membership**, and the paragraph that stood here said the
opposite: that it answers "nearest preceding header", deliberately, because
that is what the TUI had always painted, and that encoding an end boundary
would mean changing `AxisEntry` in `improvise-core` and was "filed rather than
smuggled into" improvise-anr9. All of that was true when written. **The filed
ticket is improvise-gd83 and it has landed** — `AxisEntry::GroupEnd` closes a
run, `groups()` clears on it, and the TUI's painted span changed with it. The
mechanism and its two emission arms are §3a.
That correction is here rather than only in §3a on purpose. This paragraph
forbade, in terms, the change §3a describes; a reader arriving at the stale
copy first would conclude the rule had been smuggled past its own prohibition.
When a fact is stated twice, changing it is a grep (gotcha 48) — and the two
copies being in *one file* is what made this worth saying out loud.
### The width rule has one home
@@ -751,6 +761,57 @@ Nothing is defined for the help overlay: its content is static and
---
## §3a. The frame's group boundaries and its position in the axis
Two facts `AxisFrame` gained after `improvise-anr9`, both about group runs.
**`FrameEntry::GroupEnd` closes a run.** `group_of` used to answer "nearest
preceding header", which is not membership: `expand_category` emitted a header
on *entering* a group and nothing on leaving one, so an ungrouped item after a
grouped run inherited it (`improvise-gd83`). The §3 paragraph that recorded the
old rule — and forbade this change — is corrected there, not merely superseded
here. `AxisEntry::GroupEnd` is
the boundary, carried onto the wire as `FrameEntry::GroupEnd`; `groups()` clears
`current` on it and `group_of` stops walking. It is emitted only on
`Some -> None`, never between two groups — the next header already ends the
previous run — and at the end of an expansion whose last item was grouped, which
is the across-prefix arm `cross_product` creates.
Nothing draws a `GroupEnd`. Both front ends skip it and it consumes no data
index, so it is invisible except through `group_of`/`groups`.
**`AxisFrame::first` is the absolute data index of the frame's first item.** The
windowing rule carries the nearest header *above* a scrolled window so
`group_of` can answer, and `body_from` drops it on the way to the screen. At the
top of an axis there is no "above", so that same leading header is content — a
collapsed first group has no other affordance (`improvise-0o6x`). `idx` alone
cannot tell the two apart, because the browser's frame is already windowed and
always passes `0`; `first + idx == 0` can.
The TUI builds over the whole grid so `first` is always `0` there, and the
browser's comes from the projected window. A frame built by hand in a test must
set it, which is why it is a plain field rather than derived.
It carries `#[serde(default)]`, and **that is a defect rather than a decision**
(improvise-1wl0): `first: 0` is not an absence but the claim "this frame leads
the axis", which is the claim `body_from` acts on — so a frame that loses the
field deserializes as leading. No skew ships today, both ends being one
workspace at one commit. Same argument as `Basis` having no `Default`.
**`group_runs` / `group_run_heads` are the coalescing rule's one home.** The TUI
and `render.rs::col_groups` each coalesced adjacent columns sharing a group,
kept in step by a comment (`improvise-8qlr`). Runs coalesce by group **name**,
so two adjacent runs under two different headers of the same name read as one —
that is what both ends did before and is preserved deliberately, because
changing the rule in the commit that moves it would leave the agreement test
unable to tell "both ends agree" from "both ends changed together".
`FrameGroup` carries `cat` and could fix it, and **a single home reads as
decided** — which is the new risk and why it is filed as improvise-h0iv rather
than left as a note. The TUI's one
divergence is that it labels its first *visible* column even mid-run, so a
scrolled grid does not draw an unlabelled group line.
## 4. `frame.rs` — one envelope pair, two deployments
### The framing adds as little as it can get away with
+28 -5
View File
@@ -121,11 +121,34 @@ enum, and only the last translation is local.**
`mode_style(&AppMode) -> Style` stays here for the same reason (`Style` and
`Color` are ratatui types and `improvise-command`, where `AppMode` lives,
carries no rendering-framework dependency) — **but it is not the same shape.**
It matches on `AppMode` directly with a `_ =>` wildcard for "the default chrome
for every non-highlighted mode", so a new `AppMode` variant silently inherits
that chrome instead of failing to compile, and there is no semantic enum in the
shared layer to decide it. That is `improvise-zx8t`, filed and not fixed.
carries no rendering-framework dependency), and since `improvise-zx8t` it is
**exhaustive**: the fourteen variants that take the default chrome are spelled
out rather than swept up by a `_ =>` wildcard, so adding an `AppMode` fails the
build here until someone chooses its colour.
It is still not quite the same shape as `axis_fg` and its two siblings, and the
difference is deliberate rather than pending. Those three map a *semantic enum*
the shared layer owns; `mode_style` matches `AppMode` itself, because there is
no `ModeColor`. `improvise-zx8t` offered making one, and it was declined here
for two reasons: the colour is the only thing at stake, and the ticket says to
decide it together with `improvise-k099`, which asks the same question of
`hint_text`/`label` and is not in this unit. Exhaustiveness is what the
repo map's "the compiler names every one you must fill in" promise needed; a
semantic enum is a separate, larger answer and is still open.
**The guard is the compiler, not a test.** Nothing asserts the colours, and a
test enumerating the variants would not fail when a fifteenth is added — only
the missing match arm does. Verified by deleting the `AppMode::Help` arm:
`error[E0004]: non-exhaustive patterns: `&AppMode::Help` not covered`.
### The minibuffer shares the bottom row with the status line
`draw_bottom_bar` used to render the minibuffer *instead of* `draw_status`, so a
status message written while any text-entry mode was open was never drawn
(`improvise-mai9`, and the stranding gotcha 21 describes). `minibuffer_line`
composes both: the prompt and buffer on the left, any pending `status_msg`
right-aligned. The alternatives and why they lost are in
`context/test-evidence/tui.md`.
### The caches are built here, per frame
+15 -9
View File
@@ -145,11 +145,18 @@ Escapes inside pipes: `\|` (literal pipe), `\\` (backslash), `\n` (newline).
This convention is shared between the `.improv` persistence format and the
formula parser's identifier syntax.
### Formula Tokenizer: Identifiers and Quoting
### Formula Identifiers and Quoting
**Bare identifiers** support multi-word names (e.g., `Total Revenue`) by
allowing spaces when followed by non-operator, non-keyword characters. Keywords
(`WHERE`, `SUM`, `AVG`, `MIN`, `MAX`, `COUNT`, `IF`) act as token boundaries.
**Bare identifiers** are `(ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")*`
— no spaces, the same shape `improv.pest`'s `bare_name` has. A multi-word name
such as `Total Revenue` must be pipe-quoted.
**Keywords need a word boundary after them, not before.** `IS`, `WHERE` and the
aggregate names are spelled `^"…"` in `formula.pest`, and `is_expr`/`where_clause`/
`inline_where` are compound-atomic with `sp+` so that `Region ISLAND` and
`SUM(Rev WHEREAS x = y)` are parse errors rather than silently-wrong filters. A
`SUM`/`IF` without parens falls through to `ref_expr`, which is why a bare
keyword still names an item; the writer quotes it anyway (`quote::quote_name`).
**Pipe-quoted identifiers** (`|...|`) allow any characters — including spaces,
keywords, and operators — inside the delimiters. Use pipes when a category or
@@ -162,11 +169,10 @@ item name collides with a keyword or contains special characters:
SUM(|Net Revenue| WHERE |Region Name| = |East Coast|)
```
Pipes produce `Token::Ident` (same as bare identifiers), so they work
everywhere an identifier is expected: expressions, aggregate arguments, WHERE
clause category names and filter values. Double-quoted strings (`"..."`)
remain `Token::Str` and are used only for WHERE filter values in the
`split_where` pre-parse step.
`identifier` is `pipe_quoted | bare_ident`, so a pipe-quoted name works
everywhere a bare one does: expressions, aggregate arguments, and a `WHERE`
clause's category name. A double-quoted string is a `filter_value` only — the
right-hand side of a `WHERE` or an `IS`, never an expression.
---
+2 -2
View File
File diff suppressed because one or more lines are too long
+241
View File
@@ -2062,3 +2062,244 @@ done
The other two losses are deliberate: `pointer.spec.js`'s sidebar test was
superseded by the open-at-launch ruling, and the widgets file was merged into
this one.
#### "a click moves the selection from an editing state too"
improvise-0x9u, and the fault was injected by running this assertion against the
bundle as it stood: `cells.nth(4).locator("input")` came back **0**, with the
selection still on cell 0, exactly as the bug report measured in Chromium.
Two things about it are worth more than the assertion. First, **what it does not
do**: it never presses Escape between the two clicks. The sibling test does, and
that single line is why the property it names ("a click moves the selection") was
asserted for a whole wave while the neighbouring state was broken — the click
path from an *editing* page was never exercised.
Second, **the fix's own first attempt failed this same test**, which is the part
to remember. Moving the handler to `mousedown` is necessary and not sufficient:
`close` still repainted, and removing a focused `<input>` fires `blur`
**synchronously**, so `close` rebuilt the table from inside itself and detached
the `<td>` the handler was about to open an editor on. The assertion could not
tell that apart from the original bug — same symptom, different cause — so it is
the debug run rather than the test that separated them. `close` therefore no
longer repaints, and the `blur` arm checks `closed` before dismissing.
Third — and this is the edit the repair nearly shipped unguarded — **the fix is
three edits, not two**, and the third is the one with the most careful comment:
`close` restores `td.textContent` (the *drawn* value) instead of `seed` (the
*stored* one), because `close` no longer repaints and `repaint()` early-returns
for as long as `editing` is set. Reverting that one line to
`td.replaceChildren(original)` left the whole suite green, and the defect was on
the front page: `web/demo.improv` carries `format: ,.0` over `18400`, and this
test's own two clicks — no Escape between them — leave cell 0 reading `18400`
where the page had drawn `18,400`, for as long as the second editor stays open.
The assertion added for it is `toHaveText("18,400")` after the second click, and
the injection is exactly that revert: it fails with `unexpected value "18400"`,
naming this test and no other.
The pattern is worth carrying past this entry. A repair of N edits whose N−1
guarded ones pass is not a guarded repair, and the unguarded edit is reliably
the one the author understood best — which is why it carries the clearest
comment and why it felt covered. For each distinct edit, name the test that
fails when only that edit is reverted; a line with a comment and no answer to
that question is the line to inject.
#### "the widgets the demo document fills are drawn, in the state that fills them"
The `#doc-name` row is improvise-5jna, and the reason it belongs in a **browser**
test rather than in `render_surface.test.js` is that the latter is a substring
search over the page's source: `doc_name` mentioned by a function nothing calls
reads as painted. That is how `model.minibuffer` once shipped dead, and how
z754's three title-bar fields sat projected and unpainted for a wave while the
static check was green. Proved by adding the assertion first: `element(s) not
found` for `#doc-name`.
The `[+]` marker is deliberately **not** asserted here. The standalone deployment
persists on every accepted batch and `effect::Save` clears the flag in the same
round trip (gotcha 45), so the marker is transient in a browser and any timing
assertion over it would be flaky rather than discriminating. Its spelling — the
order `name (stored_as) [+]`, the `null` `stored_as` writing no empty parens — is
`web/tests/shell.test.js`, over `documentTitle` directly.
#### "a selection driven below the fold scrolls itself into view"
improvise-jq8l, and the expensive half was the **fixture**, not the assertion.
Neither shipped document can reach the state: `web/demo.improv` is four rows and
`harness/fixtures/grouped.improv` is eight, and a probe measured both at
`scrollHeight === clientHeight` in a 484px box — nothing to scroll, so the
assertion would have passed against a page that never scrolls. That is the
vacuity this entry exists to record, and it is why the spec asserts
`scrollHeight - clientHeight > 0` **before** it presses a key.
`harness/fixtures/tall.improv` is thirty rows in three groups, which overflows for
a reason worth stating: `ROW_PX` is 26 and a real row measures 28.6px, and inline
group rows are not charged for at all, so the page renders more rows than its own
box can show. Measured against the unfixed page: 105px of overflow, the selected
cell 96px below the visible bottom, `scrollTop` still 0 after twenty-five
`ArrowDown`s.
Both directions are asserted, and neither alone is enough: the cell being inside
`#grid`'s rect would also hold if the selection had never moved, and `scrollTop >
0` would also hold if the page scrolled to the wrong place.
#### "the casts are legible on a phone, and the page still does not scroll sideways"
improvise-o31u, and **the instrument is the finding**. `getBoundingClientRect`
reports a height for a `content-visibility`-hidden child, and heading text
overflows its box without widening it, so neither "the element is present" nor a
box measurement can see this defect at all. What a reader sees is the **computed
font size**, and that is what is asserted.
Measured against the unfixed page, and it reproduces the ticket's figure to five
digits: `pivot.cast` renders its terminal at **4.83258px** on a 390px viewport.
The arithmetic is not subtle — the player is told `cols: 120`, the container is
~356px inside the page's 16px gutters, and the player DERIVES its font size from
the width it is given. After the fix: **11.9457px**.
Two assertions guard the trade this fix must not make. The page's own
`scrollWidth - clientWidth` must stay **0** — a document that scrolls sideways
on a phone is a worse bug than one unreadable section — and `#cast-pivot` must
be the thing that scrolls, which is what separates "contained the overflow" from
"removed the overflow by shrinking something else".
#### "a desktop viewport is unchanged by the phone fix"
The negative arm, and **it is forward-looking: no fault available today makes it
fail.** The entry previously named two mechanisms and both were measured, at a
1280px viewport, against the shipped page:
- `@media (max-width: 900px)` → `@media all`: this test **passes** (13.1131px,
`scrollWidth === clientWidth`).
- `min-width: 880px` moved from `.player > *` onto `.player`: this test
**passes**; the *phone* arm fails instead, on `scrolls.page === 0`.
The arithmetic is why, and it is worth stating rather than re-deriving: at
1280px `#cast-pivot` measures `clientWidth === scrollWidth === 966`, so an 880px
floor is inert at desktop wherever it is placed. The floor would have to exceed
the ~966px the player actually gets — or the content column would have to
narrow — before either assertion here could move. That is a real future fault
and the reason to keep the test: it pins 13.11px and "nothing to scroll" against
a floor raised past the player's own width. It is not a reason to claim the arm
currently discriminates between the two placements, because it does not.
Note also that the **phone** arm is stronger than this entry used to credit it
with: the misplaced-`min-width` fault, attributed here to the desktop arm, is
caught there. `scrolls.page === 0` is the assertion that separates "contained
the overflow" from "moved it onto the document".
#### "no cast carries the recording machine's shell, clock or geometry"
improvise-2oxt, and **both clauses were injected separately**, which matters
because the first would otherwise mask the second. Serving the pre-2oxt
`pivot.cast` fails on `env` — `Received: {"SHELL": "/bin/zsh", "TERM":
"screen-256color"}` — and stops there. Stripping `env` and `timestamp` from that
same header and serving the body unchanged then fails on `pivot.cast sets a
window title`, which is the clause that actually watches for the leak: an OSC
window-title sequence, emitted by a shell once per prompt and carrying the cwd,
and emitted by `improvise` never.
Asserted over the cast **file** rather than over the rendering, and that is the
finding. A player draws a shell prompt as ordinary terminal text, so a
screenshot or a `toContainText` assertion would have to know what the hostname
and home directory had been — a check that can only be written by someone who
has already seen the leak. The file-level signature can be written by anyone.
What no assertion here reaches, and how it was checked instead: whether each
cast SHOWS what its section claims. That is not a property of the bytes. It was
verified by replaying each recording through a small ANSI reconstruction and
reading the frames as text, which found two scenarios that recorded cleanly and
demonstrated nothing — a drill into a cell with no records behind it, and a `c`
pressed on a category already on the column axis. Both are true frames about the
wrong thing, which is the failure mode this repo keeps meeting, and neither was
visible from the keystroke list.
---
## `improvise-ws-server` — the `Origin` check (improvise-kc7n)
The bug: `grep -rni origin crates/improvise-ws-server/src/` returned nothing.
The server inspected no `Origin` header and accepted every handshake, and a
websocket handshake is exempt from the same-origin policy — no CORS preflight —
so any page a user visited could open a socket from that user's own allowed
address and read and edit every document served. An address check cannot see
it; the argument is `context/code/clients.md` §Origin.
Five edits, five guards. Each was reverted alone and the named test failed;
nothing here is covered only by a comment. Review added a sixth — see
`two_origins_state_none_this_policy_can_answer_for`.
### a_handshake_from_a_foreign_origin_is_refused
The reproduction, written first and watched fail on `2afbf37`: a handshake
carrying `Origin: https://evil.example` connected, and the test's
`connect_async(...).err()` was `None`. After the fix the upgrade is refused with
a 403 and the assertion is on the status, not merely on "an error happened" —
a refused upgrade and a dropped connection are different outcomes and only one
of them is the fix.
**Injection:** replacing the handshake callback's `admitted(&origins, req)` with
`Ok(())` — the transport collecting a policy it never asks — fails this and
`a_named_origin_reaches_the_document`, and nothing else in the workspace.
### a_handshake_with_no_origin_is_accepted
The case most likely to be wrong later, so it is named rather than left to a
match arm. Non-browser clients send no `Origin`: curl, a native client, this
file's own other tests. **Injection:** flipping `OriginPolicy::permits`'s `None`
arm to `false` fails this, `an_absent_origin_is_allowed`,
`the_handshake_refuses_what_the_policy_refuses`, and *both pre-existing socket
tests* — which is the point: the repo's own instrument is the non-browser client
this arm is about.
### a_handshake_from_a_loopback_page_is_accepted
The shipped local recipe — the page served by `python3 -m http.server 8080`,
the socket on another loopback port — must keep working with no flag, or the
default is a different bug from the one being fixed. **Injection:** making
`is_loopback` return `false` fails this and
`a_loopback_page_is_allowed_by_default` and leaves the refusal tests green,
which separates "allows loopback" from "allows everything".
### a_named_origin_reaches_the_document
`--allow-origin` end to end over a real socket, plus the negative arm in the
same test: widening for one page does not widen for its neighbours. Without
that second assertion the test passes against a policy that permits everything,
which is the fault E1a injects.
### allow_origin_widens_the_policy_and_nothing_else_does
The wiring, which no socket test can reach: the other tests build an
`OriginPolicy` directly, so a flag collected by clap and dropped on the floor is
invisible to every one of them. This drives `Args::parse_from` and asserts both
directions — bare refuses the deployed https origin and still allows loopback;
widened allows the named origin and not its neighbours. **Injection:** making
`Args::origin_policy` return `OriginPolicy::default()` fails this test alone.
### two_origins_state_none_this_policy_can_answer_for
Found by review rather than by the ticket. `HeaderMap::get` is **first-wins**,
and the deployment this README recommends puts a reverse proxy in front of the
socket — a proxy that *appends* `Origin` rather than setting it leaves two, and
reading the first lets the second be anything at all. No browser sends two, so
two is not a state any policy can answer for and the handshake is refused.
**Injection:** restoring the first-wins read (`(Some(value), Some(_))` answering
from `value`) fails this test alone, across the whole workspace. The test also
asserts the allowed origin *by itself* is still allowed, so the refusal is
demonstrably the second header rather than the first.
### a_refusal_is_a_forbidden_response
The refusal has to be a refused *upgrade*, and the socket test asserts the
`Error::Http` **kind** and the status rather than matching "403" in a
`Display` string: a dropped connection and a refused handshake are different
outcomes and only one of them is this fix. **Injection:** changing `forbidden()`
to `UNAUTHORIZED` fails this and `a_handshake_from_a_foreign_origin_is_refused`,
which is what shows the socket test reads the status rather than merely
observing that connecting failed.
What the signature covers instead of a test: `transport::serve` **takes** the
policy, so a host cannot forget to pass one — `doctrine(an_invariant_is_made_
unrepresentable_not_validated)`. What remains uncovered and is stated rather
than guarded: a proxy that strips `Origin` turns the check off, which is why the
proxy's own Origin allow-list stays in `README.md` as defence in depth.
+301
View File
@@ -403,6 +403,133 @@ than `effect.apply(&mut app)`. That is deliberate: the dirty flag is raised off
the `EffectKind` now (improvise-gv9r, gotcha 45), so driving the effect directly
would assert a flag nothing in production sets.
### every_file_writing_effect_aborts_the_batch_on_failure
improvise-z05r, the **third** instance of one rule: improvise-8tk fixed `Save` and
not `SaveAs`; PR #27 found `SaveAs`; `ExportCsv` was the arm still missing, so a
`Binding::Sequence` or a `:` line composing an export with anything after it
continued past a failed write.
The fix is structural rather than a third patch: all three now report through one
`report_file_write(app, outcome)`, which writes the status message either way and
sets `abort_effects` on `Err`. That is not sufficient by itself — a fourth writer
could still be written without calling it — so the test names the three, and a
fourth has to come here and be named. Naming is the mechanism; the shared function
is what makes the naming cheap to honour.
Each case drives `App::apply_effects` with `[writer, ChangeMode(Quit)]` against a
path under a directory that does not exist, and asserts both `last_batch_aborted()`
and that the trailing mode change never landed.
Injected fault: drop `app.abort_effects = true` from `report_file_write`. Fails by
name, along with the four older abort tests — which is the point: they and this one
now share one mechanism.
Injected fault, narrower: route `ExportCsv` around the reporter and write its status
directly. Only this test fails, naming `ExportCsv`, with 460 passing — the exact
state the ticket describes.
### a_batch_that_writes_nothing_leaves_the_document_clean
improvise-748f. `ViewNavigate { forward: false }` emits `ApplyAndClearDrill`
unconditionally, and that effect returns immediately when `drill_state` is `None`.
It declares `Model(CellData)` regardless — `Effect::kind` takes no `App`, so the
classification cannot be state-dependent — so `apply_effects` raised
`model_state.dirty` on a document nothing had touched. Press view-back, press `:q`,
and the quit is refused; autosave arms thirty seconds later.
The older test here, `start_drill_and_apply_clear_drill_with_no_edits`, pinned the
wrong behaviour in an assertion message arguing it was the same over-approximation a
failed `LoadModel` gets. It is not: a succeeding `LoadModel` leaves a document that
really does differ from the file its path names, and that remains declared. This was
a wrong answer to no divergence at all.
### a_cell_data_effect_no_raw_cell_records_still_dirties_the_document
The guard against the cure improvise-748f **proposed**, which is unsound. The ticket
suggested raising `dirty` off `bump_changed_records`' per-record fingerprint of the
raw cells, on the reading that `CellData` means cells. Five of the ten `CellData`
effects change the document without writing a raw cell: `AddItem`, `AddItemInGroup`
and `SortData` (which reorders — and `.improv` carries the order), and `AddFormula`
and `RemoveFormula`, whose results live in `Model::formula_cache` and not in the
store. Observing raw cells would have left every one of them reading clean, which is
a *lost edit* — the opposite direction from the spurious prompt it was fixing.
So the observation is `document_content` — `document_config` plus the cells in
iteration order and the formulas — and **only the `CellData` arm reads it**. The
`LayoutGeometry` arm keeps the narrow `document_config`. A first version pointed both
arms at the wide fingerprint, and review benchmarked what that costs at 18,000 cells
in release over 200 batches after warm-up: one `SetCell` batch 8.07 ms → 10.21 ms
(+27%, a constant factor on a path that was already O(cells) through
`record_fingerprints`), but one `SetNumberFormat` batch **78 µs → 2.63 ms, 33×** —
an *asymptotic* change from O(config) to O(cells + config) on the held-key path that
`[`/`]`, axis cycle, hide/show, group toggle, transpose and prune toggle all take.
~380/sec where they had been ~12,800/sec. The split costs no correctness because all
five blind effects are `CellData`; that every `LayoutGeometry` effect really is
visible to the narrow fingerprint is asserted separately, and not left as a reading.
`WholeWorkbook` stays *declared*, and that is the remaining over-report: a `LoadModel`
whose incoming content happens to be identical still marks the document dirty, and a
failed one does too. Both cost one redundant write.
The `SortData` case needs two cells inserted out of sorted order, or it reorders
nothing and its case asserts the opposite of what it names. The `AddFormula` case
removes the fixture's formula first, or adding it is a no-op.
Injected fault: narrow `document_content` back to the configuration it hashed before.
Fails naming `SortData`, with five other dirty-flag tests.
### every_layout_geometry_effect_is_visible_to_the_narrow_fingerprint
The guard that makes the two-fingerprint split safe rather than a trap. Splitting
`CellData` (wide) from `LayoutGeometry` (narrow) is only sound while every effect
wearing `LayoutGeometry` writes something `document_config` hashes; an effect it
cannot see would leave the document reading clean **forever**, which is the same
lost-edit failure the wide fingerprint exists to prevent, arriving by the other door.
The thirteen are enumerated by brace-matching the `impl Effect` blocks, not by
grepping — the naive grep reports fourteen, picking up `HelpPageSet`, which is
`ViewOnly` and is simply the last `pub struct` before `#[cfg(test)]` (gotcha 38 names
this exact artefact). Each case asserts its effect really declares
`Model(LayoutGeometry)` before asserting anything else, so a reclassified effect fails
here rather than silently leaving the list unrepresentative.
**Four** cases carry a `prepare` step run **directly against the workbook** and not
through an effect, because the interesting state is the precondition. What makes such
a case honest is the **no-op-fails** property: without its precondition the effect
writes nothing, an unwritten document leaves the fingerprint unmoved, and the case
therefore *fails* rather than passing vacuously.
Measured, by deleting each `prepare` in turn, **two** of the four are load-bearing in
exactly that way and two are not:
- `DeleteView` needs a second view; without it, `dirty` reads false and the case fails.
- `ShowItem` needs a hidden item; without it, `dirty` reads false and the case fails.
- `ToggleGroup` is **not** load-bearing here. `ToggleGroup::apply` calls
`toggle_group_collapse` unconditionally — its membership check gates only
`reset_cursor` — so `collapsed_groups` moves whether or not the group is real, and
the case passes with the `prepare` deleted. The step documents the realistic input;
it is the *cursor* arm, tested elsewhere, that needs a real group.
- `TransposeAxes` is **not** load-bearing either, and its step is literally a no-op:
`View::on_category_added` already assigns the first regular category `Row` and the
second `Column`, so the fixture arrives with `Type`/`Month` differing. The step
states the precondition rather than establishing it, and the case passes without it.
`dirty` is cleared **after** `prepare` and before the effect. That clear is defensive
hygiene for a *future* `prepare` that goes through `apply_effects`, and it is not what
makes any case honest today: `dirty` is raised **only** inside `App::apply_effects`,
and no `prepare` in this test runs an effect, so no preparation can set it. Demonstrated
by removing the clear **and** blinding `document_config` to `0` together: all thirteen
cases still read `dirty == false`. An earlier version of this entry claimed the
opposite — that without the clear "all four cases pass against any fingerprint" — and
measurement contradicts it.
Injected fault: `document_config` returns `0`. Fails, with five other dirty-flag tests
across three files. Injected fault, narrower and the one that proves the test is
pointed at the right thing: drop `view.number_format` from `document_config`. Fails
naming **`SetNumberFormat`**, with only one other test failing anywhere.
---
## `ui/app.rs`
@@ -862,6 +989,119 @@ The counters are shared document state on `App`, never on the portable session
slice (gotcha 30): a client inheriting another's idea of how far the document has
moved is exactly the stale basis this machinery exists to catch.
### a_page_flip_under_prune_empty_invalidates_the_cursor
improvise-7kvw, the case improvise-9mo9 decided the other nine and deliberately left
open. `SetPageSelection` changes which records are visible and not which categories
frame them, so a grid position normally keeps naming the same cell. Under
`View::prune_empty` that is false: which rows survive pruning is a function of the
page selection, so a page flip renumbers the grid while neither `reset_cursor` nor
`cursor_epoch` moved — gotcha 24's failure in both directions at once, the host's own
cursor stale and no client told.
9mo9 did not patch it because `[`/`]` paging is constant navigation and sending the
cursor home on every flip is worse than the hole when `prune_empty` is off, which is
the default. So the reset is conditional on `prune_empty`, and on the selection
actually moving.
The fixture has three regions and two quarters whose non-empty rows differ, so
position 0 names `North` on Q1 and `South` on Q2. The test asserts the row labels
really differ before asserting the epoch moved; the negative arm asserts they really
*do not* differ with pruning off, so neither case can pass vacuously.
Injected fault: drop the reset. The prune arm fails. Injected fault: make the reset
unconditional. The no-prune arm fails.
### a_page_flip_without_prune_empty_leaves_the_cursor_alone
The negative arm of the above, and it is load-bearing for the same reason gotcha 24
gives for `SetNumberFormat`: without it, "reset on every page flip" passes the
positive test and costs the user their position on every `[`/`]`.
### selecting_the_page_already_selected_invalidates_no_cursor
The third arm, and the one the user actually hits. `SetPageSelection` resets the
cursor only when the selection **moved**, and that guard shipped untested: deleting
`moved &&` left the whole workspace green at 1416, which review caught.
It is not an edge case. `PageNext`/`PagePrev` are odometer-style and emit a
`SetPageSelection` for **every** page category on every keypress, so with more than
one page category the non-carrying categories are always no-ops, and a single
one-item category re-selects the same item every time. Unguarded, each such keystroke
bumps `cursor_epoch`, sends the host's cursor to the origin and broadcasts
`Command::ResetCursor` to every client on that view — improvise-75ld's "a no-op that
refused everybody else", one tier down and aimed at the cursor instead of the basis.
The test asserts both preconditions **before** applying the effect — that the fixture
really is on `Q1`, and that the grid really does prune — and that the row labels did
not move before asserting the epoch stood still. So it cannot pass by re-selecting a
page that was never selected, or by testing a grid that does not prune.
Both `before` placements are load-bearing, and both were wrong once. The page check
used to run **after** the effect, which sets the page to `Q1`, so it could not see a
fixture that started elsewhere: with the fixture moved to `Q2` the test did fail, but
at the *row-labels* assertion, under a message about renumbering. The sentence naming
the property was not the assertion guarding it. The `prune_empty` check did not exist
at all, and with pruning off the reset never fires whatever `moved` says — so
`paged_model(false)` plus the deleted guard left the test **green**, proving nothing,
with nothing saying so.
Both preconditions read `App::view()`, the view the effect writes through
`app.view_mut()`, and not `Workbook::active_view()`, which is the document's
*default* and is deliberately allowed to differ from it (gotcha 29). They agree in
this single-client fixture, so the distinction is latent rather than a live bug —
but an assertion about what an effect did must read the view that effect wrote.
Injected fault: delete `moved &&`. Fails by name, alone.
Injected fault: `paged_model(false)` **and** delete `moved &&`. Fails on the
`prune_empty` assertion; before that assertion existed, this combination passed.
Injected fault: `paged_model`'s fixture starts on `Q2`. Fails on the page-selection
assertion, which now names it; before the move it failed on the row labels instead.
### a_foreign_layout_is_not_shown_the_hosts_staged_drill_text
improvise-pubt, filed with an argument for unreachability that the test disproves.
`project::cell_display` fills `edit_text` from `App::display_value_in(layout, key)`,
and since improvise-jytm that layout may be another client's.
`drill_state.pending_edits` are keyed by `(row index, RecordCol)` against the
**host's** drill layout (gotcha 1), and a row index in one layout names nothing in
another.
The ticket read the arm as unreachable because "a foreign layout built by
`GridLayout::new` carries no frozen records, so it should not produce a synthetic
key". That is wrong: records mode **auto-detects** from the axes (gotcha 1), so an
ordinary saved view with `_Index` on Row and `_Dim` on Column gives `GridLayout::new`
a records layout whose `cell_key` returns exactly the synthetic `(_Index, _Dim)` key
the drill arm matches on. The test fails on the original code, naming the host's
staged text read out of a layout it was never staged against.
The fix is `App::staged_drill_edit`, which consults `drill_state` only when
`std::ptr::eq(layout, &self.layout)`. A pointer comparison rather than a name
comparison because `grid_in` reuses `&app.layout` by reference where the view names
agree and derives a fresh one otherwise, so identity *is* the question being asked.
Whether the host's pending drill text should reach any other client at all is
improvise-tom1 and is untouched here.
The positive arm — the host's own layout still shows `STAGED` — is in the same test,
or "never consult the drill" passes, which would blank the TUI's own drill editor.
The drill fixture needs a real `RecordRow`, not an empty `Vec`: the host's layout
iterates the frozen snapshot, so an empty one gives a zero-row grid and the positive
arm cannot be built.
### a_default_that_names_nothing_says_which_invariant_broke
improvise-h074. `App::resolve_view` falls back to `workbook.active_view` for a name
the document has not got and then indexes `workbook.views[default]`. Several of these
call sites previously went through `Workbook::active_view()`, which carries
`.expect("active_view always names an existing view")`; `IndexMap`'s `Index` impl
panics with no message at all, so a workbook whose default names nothing — reachable
only by constructing the struct directly, since `create_view`/`delete_view` maintain
it — failed with a bare index panic where it used to name the broken invariant.
Injected fault: restore `&workbook.views[default]`. The test fails on the expected
substring.
---
## `ui/project.rs`
@@ -2180,6 +2420,21 @@ An empty prompt must not reach `SaveAs`: `SaveAs(PathBuf::from(""))` asks the
backend to write to `""`, and the user then reads whatever I/O error that produces
instead of "you typed nothing".
### commit_export_bails_on_an_empty_path
improvise-z05r's first arm. `CommitExport` did not bail on an empty path where
`CommitBuffer`'s three kinds all do (gotcha 20: buffer to trim to bail-on-empty to
dirty to status is the shared spine), so Enter on an empty export prompt asked the
backend to write to `""` and the user read whatever I/O error that produced.
The same change reorders the batch: the mode change now comes **first**, because
`ExportCsv` aborts on a failed write as of z05r's other arm, and an abort at the tail
would strand the user in `ExportPrompt` — where `draw_bottom_bar` renders the
minibuffer *instead of* the status line, so the export error is never drawn at all
and the operation appears to do nothing (gotcha 21). That ordering is asserted in
`commit_export_produces_export_and_normal_mode`, by index and not by presence.
---
## `command/cmd/fit.rs`
@@ -2362,6 +2617,52 @@ second entry under "view-back" and removes "view-forward" from the registry
entirely; `parse` takes the first match, so every surviving entry still agrees with
its own name. Uniqueness is what catches it.
### the_context_reports_each_panels_own_open_flag
improvise-y4xg and improvise-qsmp, two shapes of one defect. `registry.rs` mapped
`Panel` to a `CmdContext` field in two places, and `App::cmd_context` copies the
three flags out of `ViewState` in a third. **Both were demonstrated unguarded**:
swapping Formula and Category at the two registry sites left the workspace at 1400
passing, and hardcoding the `category_panel_open` propagator to `true` left it at
1394.
Nothing exercised either, because `cmd/mod.rs::test_helpers` sets the panel flags
explicitly on the context it builds — the command tests construct the value they then
assert on, so no test ever drove the real propagator.
The registry mapping is gone: both sites call `CmdContext::panel_open(panel)`, the
one mapping on this side of the wall. That leaves two permutable sites, and the test
is aimed at both from a path neither controls — it writes `view_state`'s three fields
**by name** and reads back through the enum accessor on a context built by the real
`App::cmd_context`. PR #70's review caught the weaker shape of this: a panel test that
zeroed and read through the same four accessors passed a consistent permutation.
Injected fault: swap `Panel::Formula` and `Panel::Category` in `panel_open`. Fails.
Injected fault: hardcode the `category_panel_open` propagator to `true`. Fails.
### toggle_panel_visibility_reads_the_panel_it_names
The behavioural half, and the one that reaches the registry. Its interactive arm
passes `currently_open` straight through, so with exactly one panel open the command
built for each of the three names carries its own panel's flag. Both injections above
fail here too.
### a_focused_open_panel_closes_and_a_closed_one_opens
improvise-qsmp's own scenario: `toggle-panel-and-focus` decides close only when the
panel is *both* open and focused, so the case that separates a right propagator from
a stuck-true one needs the mode set to the panel's own. With the flag stuck true,
pressing a panel's key on a closed panel decides to *close* it and the panel becomes
unopenable by its own key — which is what this test's second arm names.
### Helper — `open_only`
Not a test. It spells `formula_panel_open`, `category_panel_open` and
`view_panel_open` as fields rather than looping over `Panel::ALL` through an
accessor, and that is the whole evidentiary value: a permutation in any mapping
between the `Panel` enum and those three booleans has to disagree with this helper.
---
## `command/cmd/text_buffer.rs`
+68
View File
@@ -12,6 +12,20 @@ none.
## `model/types.rs`
### a_hidden_dimension_list_reduces_to_one_of_two_states
improvise-5jm5. `evaluate_aggregated` took `none_cats: &[String]` and consulted
it once, for emptiness — so two lists with different contents and the same
emptiness gave the same answer, while the name promised a filter. Nothing
misbehaved; the signature described something the body never did.
This test is the *smaller* half of the guard and says so. The real guard is the
signature: the defect the ticket describes is no longer constructible, exactly
as `Checked`'s constructor is the guard for the basis check rather than a test
that can fail. What this test does add is that `Aggregation::of` is the only
place the list is read, and that a list of two hidden dimensions is not somehow
a third state — the reduction is emptiness, and the enum is not a count.
### apply_agg_over_nothing_is_absent_for_every_function
An empty cell must read as *absent*, not as zero — otherwise every blank
@@ -543,3 +557,57 @@ uses. Fault injected into that arm alone: it fails, `Number(0.0)` against
The general shape: an injected fault proves coverage only of the code path it is
injected into. Two arms of the same rule need two injections, and if one of them
turns out to be unreachable, that is a fact about the design worth writing down.
### a_user_category_whose_name_leads_with_an_underscore_is_seated_as_regular
improvise-bidq. `View::on_category_added`'s early return read
`cat_name.starts_with('_') || self.is_records_layout()` — the **same
expression** mixing two derivations one operator apart, the left a name prefix
and the right `CategoryKind`. Four lines up, `is_records_layout` asks the enum.
`Model::add_category` does not validate names, so a user category literally
named `_Foo` was `CategoryKind::Regular` to `records_columns`, `cell_id` and
`is_empty_model`, and "virtual" to these two lines — seated on `Axis::None`,
so it never appeared in a pivot while every other consumer treated it as
ordinary. The second site (`holds_a_regular`) had the same split, which is why
the test asserts a *second* category's seat too: fixing only the first leaves
`_Foo` on Row while `Region` also takes Row.
Decided explicitly, per the ticket: `add_category` is **not** made to refuse a
leading underscore. The three reserved names are the ones `CategoryKind::of_name`
knows, `_Foo` is a legal document, and a user-visible restriction is a larger
change than making one answer out of two. The fix is that both sites now ask
`CategoryKind::of_name(..).is_virtual()`.
Fault injected by restoring either `starts_with('_')`: the test fails, `None`
against `Row` on the first site and `Row` against `Column` on the second.
### an_ungrouped_item_after_a_grouped_run_belongs_to_no_group
### a_grouped_run_does_not_leak_into_the_next_prefix
improvise-gd83, the two arms of one defect. `expand_category` emitted a
`GroupHeader` on **entering** a group and no boundary on leaving one, so
`row_group_for`/`col_group_for`'s nearest-preceding walk answered with a group
that had already ended.
The arms are separate because the two escapes are different and a fix can close
one and miss the other. The first is *within* one expansion — `Jan[Q1]`,
`Feb[Q1]`, `Sep` — closed by emitting `AxisEntry::GroupEnd` on a
`Some -> None` transition. The second is *across* expansions: `cross_product`
expands the inner category once per prefix, each call starting with
`last_group = None`, so `Food/Bob[Family]` is immediately followed by
`Clothing/Carol`, which is ungrouped and inherited Family with no transition
anywhere to hook onto. That one needs a `GroupEnd` at the **end** of an
expansion whose last item was grouped.
`GroupEnd` is deliberately NOT emitted on `Some(A) -> Some(B)`: B's own header
already ends A's run, and emitting one there moved
`data_row_to_visual_skips_headers` and `data_col_to_visual_skips_headers` for
no gain. Those two are untouched, which is the evidence the boundary is as
narrow as it claims.
Fault injected by deleting either emission: the corresponding test fails with
`Some(("Month", "Q1"))` / `Some(("Recipient", "Family"))` against `None`, and
`the_two_front_ends_draw_the_same_column_header` fails with it.
+56 -7
View File
@@ -203,6 +203,33 @@ backend from quietly normalising or re-encoding what it is handed.
## `improvise-io/src/persistence/parse.rs`
### two_sections_naming_one_category_round_trip_to_a_stable_group_order
improvise-349m: `format_md(parse_md(text))` was not idempotent when a file
carried two `## Category:` sections with the same name — the two outputs
differed only in the order of the `>` group lines. It looked like a flake
because only `grammar_prop_tests::generated_file_roundtrips` saw it, and only
when the generator happened to emit a duplicate header; taking proptest's
minimal input and running it deterministically is what turned it into a bug.
The fixture is the smallest shape that shows it: a first section defining a
group by a `>` line only, and a second whose *item* names a different group that
also gets a `>` line. Both groups must have parents, or `format_md` writes only
one `>` line and the differing internal order is invisible in the text — which
is the trap when shrinking this fixture further. Fault injected: reverting
`ParsedFile::push_category` to a plain `Vec::push` fails it, and the ticket's own
minimal input was checked against the fix as well.
**Stability is only half of what the merge owes, and the first version of this
test could see only that half.** Adversarial review injected the other fault —
`Some(_existing) => {}`, which *drops* the duplicate section outright — and the
whole `improvise-io` suite stayed green at 2000 proptest cases while the
fixture's `- X[G2]` and `> G2[P2]` vanished from the document without a word.
Main's behaviour was unstable-but-lossless; a test that compares two printed
forms cannot tell that from stable-and-lossy, because both sides of the equality
lose the same thing. The three `contains` assertions run *before* the equality
for exactly that reason, and they fail under the drop.
### parse_md_formula_category_suffix_without_space
Bug improvise-zgb (b). The formulas walker located the `[Category]` suffix with
@@ -444,9 +471,10 @@ can override it. The same argument as the ambiguity below.
nothing in the data can tell them apart. The heuristic is first-match over
`DATE_FORMATS`, so the American reading wins — a European CSV imports with its
days and months swapped, silently, and the derived `_Month`/`_Quarter`
categories inherit the error. Filed as **improvise-l8rh**; pinned here so that
changing the answer is a decision somebody makes rather than a side effect of
reordering the table.
categories inherit the error. **improvise-l8rh ruled this a known limit** rather
than a defect, so this test is what makes changing the answer a decision
somebody makes rather than a side effect of reordering the table. The argument
is in `context/code/formula-and-io.md`.
The loop at the end is the precondition that makes this a statement about
*ambiguity* rather than about one format merely fitting: the other reading has
@@ -646,7 +674,28 @@ reopens the hole; that substitution is what this test was finally proved
against. The negative arm is paired with a positive one, or "reject everything"
would pass.
`^"WHERE"` has the identical hole and was deliberately **not** fixed here:
pre-existing, far less exposed (it sits in one fixed position after an
expression, where `IS` follows any identifier in expression position), and a
grammar change with its own round-trip risk. Filed as improvise-r08u.
`^"WHERE"` had the identical hole. It is closed now — see below.
### where_does_not_swallow_the_start_of_a_longer_word
improvise-r08u, the `WHERE` half of the same defect. `where_clause` and
`inline_where` were ordinary rules, so pest auto-inserted `WHITESPACE` after
`^"WHERE"` and `SUM(Rev WHEREAS x = y)` parsed as a `WHERE` on a category `AS`:
a silently wrong filter where it should have been a syntax error.
The test carries **two** negative shapes because the repair has two independent
halves, and this was found by injecting each separately. Making the rules
compound-atomic alone already refuses `WHEREAS x = y` — once implicit whitespace
is gone, `AS x` cannot be one identifier — so a test built only on the ticket's
reproducer passes over a rule still spelled `sp*`, and `WHEREAS=y` is what that
rule still reads as a filter. Faults injected: the original non-atomic rule
(fails on the first assertion) and `sp+` relaxed to `sp*` (fails on the second).
The positive arms are there so "refuse every WHERE" cannot pass.
### where_requires_a_space_after_the_keyword
The one spelling `sp+` costs, stated rather than discovered later:
`WHERE|Region Name|=|East Coast|` used to parse and now does not. `IS` has
required its space since improvise-4zl7, so this is the two keywords agreeing.
The paired positive arm — the same text with the space — is what stops it
degrading into "any WHERE with a quoted category fails".
+128
View File
@@ -414,3 +414,131 @@ a word pool instead of random letters, `pipe_inner` is never empty,
`rest_of_line` always produces at least one character, and repetitions (`*`)
produce 1–4 items rather than 0. Each invocation is seeded from the current time
plus the PID.
### a_status_message_reaches_the_bottom_row_while_a_minibuffer_is_open
### the_minibuffer_prompt_is_not_displaced_by_the_message
### an_empty_message_never_pushes_a_row_that_fits_past_its_width
improvise-mai9. `draw_bottom_bar` rendered the minibuffer **instead of** the
status line, so a message written while a text-entry mode was open was never
drawn — not truncated, not overwritten. A test asserting `app.status_msg` was
set passed in every one of those cases, which is how this survived
improvise-6t3's careful effect ordering.
Of the ticket's two candidate fixes, the row is **shared** rather than the
callers being made structurally unable to report: giving the status a second
row changes frame geometry (improvise-q0vb's cost), and "always leave the mode
before reporting" is what `CommitSave` does by hand and would have to be
repeated at every text-entry commit — the thing the ticket calls a workaround.
**The first version of these tests could not fail for the bug, and that is the
entry worth reading.** They called the composing helper `minibuffer_line`
directly. The defect was in the *wiring* — `draw_bottom_bar` building its own
`format!` and never consulting `status_msg` — so reverting that one line
restored the bug in full with all 1411 tests green. The helper did not even go
dead: the test module's `use` kept it alive while it asserted nothing about the
product. Found by review (PR #77), reproduced here before rewriting.
So the guard is now `Buffer`-backed: a real `Terminal<TestBackend>` renders
`draw_bottom_bar` and the assertion reads the row the terminal received. That
is the only assertion that can fail for mai9's actual defect. Fault injected by
restoring `format!("{}{}", mb.prompt, buf)` plus the cursor glyph — both wiring
tests fail, the first naming the missing message and printing the row it did
compose.
The second test exists because a "fix" that dropped to `draw_status` would
satisfy the first on the message alone; it pins that the prompt still leads.
**The third test is the negative arm, and its first version was unfalsifiable
for arithmetic reasons.** `fill_line` computes `pad = width - left.len() -
right.len()`, so with an empty status an unconditional `format!(" {msg} ")`
produces a byte-identical row at any width where the line fits: `left` plus
`width - left.len()` spaces, either way. The `match status_msg.is_empty()` is
observable **only** in the band where the prompt fits but `left.len() + 4`
does not — widths 16..=19 for this fixture, where the unguarded arm returns a
row wider than the terminal.
That test then failed a second time in a more interesting way: it derived its
lower bound by *calling `minibuffer_line` itself*, so the injected fault moved
the bound with it and the loop skipped the failing band entirely. The bound is
now spelled locally. **A bound read off the function under test is not a
bound** — it is the fault grading its own exam.
Known and not fixed here: `fill_line` pads on `left.len()`, which is bytes, so
the right-hand message sits two cells further right than intended whenever the
left contains the multi-byte cursor glyph. `draw_status` has always done this
with its own multi-byte content; it is pre-existing and out of scope. It is
also what makes the band above four columns wide rather than two.
### a_group_header_first_on_its_axis_reaches_both_front_ends
improvise-0o6x. `AxisFrame::body_from(idx)` started at the first **item**, so at
the top of the axis a leading `FrameEntry::Group` was skipped on the way to the
screen — in both front ends. A collapsed group first on its axis therefore had
its rows hidden and its header nowhere, so there was no affordance to get them
back. The frame carried the header correctly; `body_from` dropped it.
It is a boundary condition, as the ticket suspected, but the boundary is not
`idx == 0`. The windowing rule carries the nearest header ABOVE a scrolled
window for `group_of` and `body_from` discards it — correctly. At the top of
the axis there is no "above", and a frame cannot tell the two apart from `idx`
alone, because the browser's frame is windowed and always passes `0`. So
`AxisFrame` gained `first`, the absolute data index of its first item, and the
rule is `first + idx == 0`. Without it the browser would have drawn an extra
header row at every scrolled position that the TUI does not.
This test is separate from `the_two_front_ends_draw_the_same_row_header`
because that fixture orders `Carol` before `Family` to dodge this case **on
purpose**, and says so. Both collapse states are driven: the uncollapsed arm
alone passes against a frame that leads with a header only when it has items
under it, which is the shape the windowing bug below produced.
The windowing half was found by this test and not predicted by the ticket. A
collapsed leading group has NO item to carry its header into the window, so it
reaches the frame only through `axis_frame`'s `pending` queue — and a first
version of the fix had `GroupEnd` **clear** `pending` above the window, which
deleted exactly that header. `GroupEnd` is now queued like a header and clears
nothing.
Fault injected by forcing `body_from`'s match to `false`: this test fails and
`the_whole_frame_is_unchanged` fails with it.
### the_whole_frame_is_unchanged
Re-pinned for improvise-0o6x. `GROUPED_FRAME` moved by exactly one line: the
leading `▼ Co…` header now draws above `East`, and everything below shifts
down one. Nothing else in the buffer changed. That the group line, the column
headers, the separator, the data rows and the totals strip are byte-identical
is the evidence the change is the boundary condition it claims to be and not a
reshuffle of the draw loop.
### the_two_front_ends_draw_the_same_column_header
Two changes, from two tickets.
improvise-gd83: `THE_MEMBERSHIP_CORRECT_SPANS` and its `assert_ne!` tripwire are
gone, and the value they predicted — `[(Q1,2), (None,1), (Q1,2), (None,1)]` —
is now what the test asserts, element for element. `Sep` is ungrouped, so the
run ends before it and resumes under the second measure's own `Q1` header. A
precondition asserts the fixture still contains an ungrouped column, because
without one the test cannot see a leaked group at all.
improvise-8qlr: the TUI arm was `group_line.contains("▼ Q1")`, which is
one-directional and passes while the TUI labels **every** column. The run rule
now lives once, as `AxisFrame::group_runs`/`group_run_heads`, and both ends
call it; the TUI's group line is compared cell for cell against spans derived
from the browser's own runs.
Fault injected exactly as the ticket specifies — forcing the TUI to emit the
group label on every column (`head = col_groups.get(ci)`, dropping the run-head
lookup). Before: `cargo test -p improvise` stayed green. After: this test fails
with `["▼ Q1","▼ Q1","","▼ Q1","▼ Q1",""]` against
`["▼ Q1","","","▼ Q1","",""]`.
One thing the shared rule does **not** flatten: the TUI labels its first
*visible* column even mid-run, because a scrolled grid would otherwise draw an
unlabelled group line. That is a scrolling concession, kept explicitly, and it
is why the comparison is run at `col_offset == 0`.
@@ -1,3 +1,4 @@
use crate::model::Aggregation;
use crate::ui::effect::{self, Effect};
use super::core::{Cmd, CmdContext};
@@ -140,7 +141,9 @@ impl Cmd for YankCell {
"yank"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let value = ctx.model.evaluate_aggregated(&self.key, ctx.none_cats());
let value = ctx
.model
.evaluate_aggregated(&self.key, Aggregation::of(ctx.none_cats()));
vec![
Box::new(effect::SetYanked(value)),
effect::set_status("Yanked"),
@@ -195,6 +195,32 @@ mod tests {
let dbg = effects_debug(&effects);
assert!(dbg.contains("ExportCsv"), "Expected ExportCsv, got: {dbg}");
assert!(dbg.contains("Normal"), "Expected Normal mode, got: {dbg}");
assert!(
dbg.find("Normal").unwrap() < dbg.find("ExportCsv").unwrap(),
"the mode change must precede the write an abort can reach: {dbg}"
);
}
/// BUG (improvise-z05r): the fourth commit kind did not bail on an empty
/// path, so Enter on an empty prompt exported to "".
#[test]
fn commit_export_bails_on_an_empty_path() {
let m = two_cat_model();
let layout = make_layout(&m);
let reg = make_registry();
for buffered in ["", " "] {
let mut bufs = HashMap::new();
bufs.insert("export".to_string(), buffered.to_string());
let mut ctx = make_ctx(&m, &layout, &reg);
ctx.buffers = &bufs;
let effects = CommitExport.execute(&ctx);
let dbg = effects_debug(&effects);
assert!(
!dbg.contains("ExportCsv"),
"an empty export path must write nothing, got: {dbg}"
);
assert!(dbg.contains("Normal"), "Expected Normal mode, got: {dbg}");
}
}
/// Naming a new measure in the trailing `+` column at the bottom-right sent the
@@ -696,10 +722,18 @@ impl Cmd for CommitExport {
"commit-export"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let buf = ctx.buffers.get("export").cloned().unwrap_or_default();
let path = read_buffer(ctx, "export").trim().to_string();
if path.is_empty() {
return vec![
effect::set_status("No path given — not exported"),
effect::change_mode(AppMode::Normal),
];
}
// The mode change comes **first**, or a failed export aborts with the
// minibuffer still drawn over its error (gotcha 21).
vec![
Box::new(effect::ExportCsv(std::path::PathBuf::from(buf))),
effect::change_mode(AppMode::Normal),
Box::new(effect::ExportCsv(std::path::PathBuf::from(path))),
]
}
}
@@ -81,6 +81,16 @@ impl<'a> CmdContext<'a> {
.and_then(crate::view::synthetic_record_info)
}
/// Whether `panel` is open. The one `Panel` → flag mapping on this side of
/// the wall, mirroring `ViewState::panel_open` (improvise-y4xg).
pub fn panel_open(&self, panel: Panel) -> bool {
match panel {
Panel::Formula => self.formula_panel_open,
Panel::Category => self.category_panel_open,
Panel::View => self.view_panel_open,
}
}
pub fn row_count(&self) -> usize {
self.layout.row_count()
}
@@ -355,11 +355,7 @@ pub fn default_registry() -> CmdRegistry {
require_args("toggle-panel-and-focus", args, 1)?;
let panel = parse_panel(&args[0])?;
// Default interactive: if already open+focused -> close, else open+focus
let currently_open = match panel {
Panel::Formula => ctx.formula_panel_open,
Panel::Category => ctx.category_panel_open,
Panel::View => ctx.view_panel_open,
};
let currently_open = ctx.panel_open(panel);
let currently_focused = ctx.mode.panel() == Some(panel);
let (open, focused) = if currently_open && currently_focused {
(false, false) // close
@@ -389,11 +385,7 @@ pub fn default_registry() -> CmdRegistry {
|args, ctx| {
require_args("toggle-panel-visibility", args, 1)?;
let panel = parse_panel(&args[0])?;
let currently_open = match panel {
Panel::Formula => ctx.formula_panel_open,
Panel::Category => ctx.category_panel_open,
Panel::View => ctx.view_panel_open,
};
let currently_open = ctx.panel_open(panel);
Ok(Box::new(TogglePanelVisibility {
panel,
currently_open,
@@ -725,3 +717,106 @@ mod tests {
}
}
}
#[cfg(test)]
mod panel_open_tests {
use super::*;
use crate::command::cmd::test_helpers::two_cat_model;
use crate::command::key::{KeyCode, KeyModifiers};
use crate::ui::app::{App, AppMode};
/// Write the raw field each `Panel` names. Spelling the fields here is what
/// gives the read side a second path — context/test-evidence/command.md.
fn open_only(app: &mut App, panel: Panel) {
app.view_state.formula_panel_open = panel == Panel::Formula;
app.view_state.category_panel_open = panel == Panel::Category;
app.view_state.view_panel_open = panel == Panel::View;
}
fn panel_mode(panel: Panel) -> AppMode {
match panel {
Panel::Formula => AppMode::FormulaPanel,
Panel::Category => AppMode::CategoryPanel,
Panel::View => AppMode::ViewPanel,
}
}
#[test]
fn the_context_reports_each_panels_own_open_flag() {
for opened in Panel::ALL {
let mut app = App::new(two_cat_model(), None);
open_only(&mut app, opened);
let ctx = app.cmd_context(KeyCode::Null, KeyModifiers::NONE);
for asked in Panel::ALL {
assert_eq!(
ctx.panel_open(asked),
asked == opened,
"with only {opened:?} open, the context answers wrongly for {asked:?}"
);
}
}
}
#[test]
fn toggle_panel_visibility_reads_the_panel_it_names() {
for opened in Panel::ALL {
let mut app = App::new(two_cat_model(), None);
open_only(&mut app, opened);
let ctx = app.cmd_context(KeyCode::Null, KeyModifiers::NONE);
for asked in Panel::ALL {
let args = [asked.name().to_string()];
let cmd = ctx
.registry
.interactive("toggle-panel-visibility", &args, &ctx)
.expect("toggle-panel-visibility resolves");
let dbg = format!("{cmd:?}");
let expected = format!("currently_open: {}", asked == opened);
assert!(
dbg.contains(&expected),
"with only {opened:?} open, toggling {asked:?} read another \
panel's flag: {dbg}"
);
}
}
}
#[test]
fn a_focused_open_panel_closes_and_a_closed_one_opens() {
for panel in Panel::ALL {
let mut app = App::new(two_cat_model(), None);
open_only(&mut app, panel);
app.view_state.mode = panel_mode(panel);
let args = [panel.name().to_string()];
let ctx = app.cmd_context(KeyCode::Null, KeyModifiers::NONE);
let dbg = format!(
"{:?}",
ctx.registry
.interactive("toggle-panel-and-focus", &args, &ctx)
.expect("toggle-panel-and-focus resolves")
);
assert!(
dbg.contains("open: false"),
"{panel:?} is open and focused, so its key must close it: {dbg}"
);
let mut app = App::new(two_cat_model(), None);
app.view_state.formula_panel_open = false;
app.view_state.category_panel_open = false;
app.view_state.view_panel_open = false;
app.view_state.mode = panel_mode(panel);
let ctx = app.cmd_context(KeyCode::Null, KeyModifiers::NONE);
let dbg = format!(
"{:?}",
ctx.registry
.interactive("toggle-panel-and-focus", &args, &ctx)
.expect("toggle-panel-and-focus resolves")
);
assert!(
dbg.contains("open: true"),
"{panel:?} is closed, so its own key must open it rather than \
leave it unopenable: {dbg}"
);
}
}
}
@@ -1,3 +1,4 @@
use crate::model::Aggregation;
use crate::model::cell::CellValue;
use crate::ui::app::AppMode;
use crate::ui::effect::{self, Effect, Panel};
@@ -112,7 +113,10 @@ impl Cmd for SearchNavigate {
Some(k) => k,
None => return false,
};
let s = match ctx.model.evaluate_aggregated(&key, ctx.none_cats()) {
let s = match ctx
.model
.evaluate_aggregated(&key, Aggregation::of(ctx.none_cats()))
{
Some(CellValue::Number(n)) => format!("{n}"),
Some(CellValue::Text(t)) => t,
Some(CellValue::Error(e)) => format!("ERR:{e}"),
+330 -34
View File
@@ -80,8 +80,26 @@ fn view_shapes(workbook: &Workbook) -> std::collections::BTreeMap<String, u64> {
.collect()
}
/// Everything a `LayoutGeometry` effect can write that the file would carry. Not the
/// cell data — `record_fingerprints` answers for that — so it is cheap per batch.
/// `document_config` plus the cells and the formulas. `CellData` is wider than its
/// name (improvise-748f), and only that arm pays the walk — context/code/command.md.
fn document_content(workbook: &Workbook) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
for (key, value) in workbook.model.data.iter_cells() {
key.hash(&mut h);
hash_value(value, &mut h);
}
for formula in workbook.model.formulas() {
formula.raw.hash(&mut h);
formula.target.hash(&mut h);
formula.target_category.hash(&mut h);
}
document_config(workbook).hash(&mut h);
h.finish()
}
/// Everything a `LayoutGeometry` effect can write that the file would carry. Not
/// the cells, so it stays O(config) on the held-key path (improvise-748f).
fn document_config(workbook: &Workbook) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
@@ -131,6 +149,26 @@ fn sorted_pairs(
out
}
/// Exhaustive, and by bits rather than by `Debug`: two values that print alike
/// must not fingerprint alike.
fn hash_value(value: &CellValue, hasher: &mut impl std::hash::Hasher) {
use std::hash::Hash;
match value {
CellValue::Number(n) => {
0u8.hash(hasher);
n.to_bits().hash(hasher);
}
CellValue::Text(t) => {
1u8.hash(hasher);
t.hash(hasher);
}
CellValue::Error(e) => {
2u8.hash(hasher);
e.hash(hasher);
}
}
}
/// A per-record XOR fingerprint of the **raw** cells it holds. Process-local and
/// never on a wire, unlike [`RecordHash`] — see `context/code/command.md`.
fn record_fingerprints(data: &DataStore) -> std::collections::HashMap<RecordHash, u64> {
@@ -139,22 +177,7 @@ fn record_fingerprints(data: &DataStore) -> std::collections::HashMap<RecordHash
for (key, value) in data.iter_cells() {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut hasher);
// Exhaustive, and by bits rather than by `Debug`: two values that print
// alike must not fingerprint alike.
match value {
CellValue::Number(n) => {
0u8.hash(&mut hasher);
n.to_bits().hash(&mut hasher);
}
CellValue::Text(t) => {
1u8.hash(&mut hasher);
t.hash(&mut hasher);
}
CellValue::Error(e) => {
2u8.hash(&mut hasher);
e.hash(&mut hasher);
}
}
hash_value(value, &mut hasher);
*out.entry(RecordHash::of(&key)).or_insert(0) ^= hasher.finish();
}
out
@@ -413,7 +436,13 @@ impl App {
Some((key, view)) => (key.as_str(), view),
None => {
let default = workbook.active_view.as_str();
(default, &workbook.views[default])
// The message `Workbook::active_view()` carried: `IndexMap`'s
// `Index` panics bare, naming no invariant (improvise-h074).
let view = workbook
.views
.get(default)
.expect("active_view always names an existing view");
(default, view)
}
}
}
@@ -488,13 +517,8 @@ impl App {
/// The same text against a layout that may be another client's (improvise-jytm).
pub(crate) fn display_value_in(&self, layout: &GridLayout, key: &CellKey) -> String {
match crate::view::synthetic_record_info(key) {
Some((idx, col)) => layout
.record_col(col)
.and_then(|rc| {
self.drill_state
.as_ref()
.and_then(|s| s.pending_edits.get(&(idx, rc.clone())).cloned())
})
Some((idx, col)) => self
.staged_drill_edit(layout, idx, col)
.or_else(|| layout.resolve_display(key))
.unwrap_or_default(),
None => self
@@ -507,6 +531,18 @@ impl App {
}
}
/// The host's own staged drill text at a records position, and only for the
/// host's own layout — a row index names nothing in another (improvise-pubt).
fn staged_drill_edit(&self, layout: &GridLayout, idx: usize, col: usize) -> Option<String> {
let own = std::ptr::eq(layout, &self.layout);
let col = layout.record_col(col).filter(|_| own)?;
self.drill_state
.as_ref()?
.pending_edits
.get(&(idx, col.clone()))
.cloned()
}
pub fn cmd_context(&self, key: KeyCode, _mods: KeyModifiers) -> CmdContext<'_> {
let view = self.view();
let layout = &self.layout;
@@ -570,12 +606,19 @@ impl App {
let touches_model = effects
.iter()
.any(|e| matches!(e.kind(), EffectKind::Model(_)));
let records_before = effects
let writes_cells = effects
.iter()
.any(|e| matches!(e.kind(), EffectKind::Model(ModelChange::CellData)))
.then(|| record_fingerprints(&self.model_state.workbook.model.data));
.any(|e| matches!(e.kind(), EffectKind::Model(ModelChange::CellData)));
let moves_geometry = effects
.iter()
.any(|e| matches!(e.kind(), EffectKind::Model(ModelChange::LayoutGeometry)));
let records_before =
writes_cells.then(|| record_fingerprints(&self.model_state.workbook.model.data));
let shapes_before = touches_model.then(|| view_shapes(&self.model_state.workbook));
let mut config_basis = touches_model.then(|| document_config(&self.model_state.workbook));
// Two bases, because the cheap one is the held-key path: `[`/`]`, axis
// cycle and hide/show must not walk 18,000 cells (improvise-748f).
let mut content_basis = writes_cells.then(|| document_content(&self.model_state.workbook));
let mut config_basis = moves_geometry.then(|| document_config(&self.model_state.workbook));
let mut dirty = Dirty::NOTHING;
for effect in effects {
let kind = effect.kind();
@@ -585,11 +628,11 @@ impl App {
// rather than defaulting into the observed half by omission.
let declared_dirty = match kind {
EffectKind::ViewOnly => false,
// Things no structural fingerprint sees: a formula, a sort order,
// a replaced document.
EffectKind::Model(ModelChange::CellData) => true,
// A replaced document differs from the file its path names even
// where the content matches, so it is declared (improvise-748f).
EffectKind::Model(ModelChange::WholeWorkbook) => true,
// Observed instead, in `observe_reshape` (improvise-75ld).
// Observed below, each against the fingerprint its tier needs.
EffectKind::Model(ModelChange::CellData) => false,
EffectKind::Model(ModelChange::LayoutGeometry) => false,
};
if declared_dirty {
@@ -597,6 +640,13 @@ impl App {
}
// The observed half, read **inside the loop** so `Save` still clears it
// in loop order (gotcha 45) — the position is the property.
if matches!(kind, EffectKind::Model(ModelChange::CellData))
&& let Some(basis) = content_basis
{
let now = document_content(&self.model_state.workbook);
self.model_state.dirty |= now != basis;
content_basis = Some(now);
}
if matches!(kind, EffectKind::Model(ModelChange::LayoutGeometry))
&& let Some(basis) = config_basis
{
@@ -939,6 +989,252 @@ mod tests {
);
}
/// BUG (improvise-pubt): `pending_edits` are keyed by a row index into the
/// host's drill, and a position in one layout names nothing in another.
#[test]
fn a_foreign_layout_is_not_shown_the_hosts_staged_drill_text() {
use crate::model::cell::{CellKey, CellValue};
use crate::view::{Axis, GridLayout, RecordCol};
use std::rc::Rc;
let mut wb = Workbook::new("Drill");
wb.add_category("Region").unwrap();
wb.add_item("Region", "East").unwrap();
wb.add_item("_Measure", "Revenue").unwrap();
wb.model.set_cell(
CellKey::new(vec![
("Region".into(), "East".into()),
("_Measure".into(), "Revenue".into()),
]),
CellValue::Number(7.0),
);
wb.create_view("Records");
{
let v = &mut wb.views["Records"];
v.set_axis("_Index", Axis::Row);
v.set_axis("_Dim", Axis::Column);
v.set_axis("Region", Axis::None);
v.set_axis("_Measure", Axis::None);
}
let mut app = App::new(wb, None);
let foreign = GridLayout::new(
&app.model_state.workbook.model,
&app.model_state.workbook.views["Records"],
);
assert!(
foreign.is_records_mode(),
"the other client's view is not records mode, so no synthetic key exists"
);
let col = (0..foreign.col_count())
.find(
|c| matches!(foreign.record_col(*c), Some(RecordCol::Measure(m)) if m == "Revenue"),
)
.expect("the records grid has a Revenue column");
let key = foreign
.cell_key(0, col)
.expect("the records grid names a cell at (0, Revenue)");
assert!(
crate::view::synthetic_record_info(&key).is_some(),
"the foreign layout produced no synthetic key, so the drill arm is unreachable"
);
let mut pending = std::collections::HashMap::new();
pending.insert(
(0, RecordCol::Measure("Revenue".into())),
"STAGED".to_string(),
);
let record = crate::view::RecordRow {
coords: CellKey::new(vec![("Region".into(), "East".into())]),
values: vec![("Revenue".to_string(), CellValue::Number(7.0))],
};
app.drill_state = Some(DrillState {
records: Rc::new(vec![record]),
pending_edits: pending,
});
assert_ne!(
app.display_value_in(&foreign, &key),
"STAGED",
"another client was shown the host's uncommitted drill text against \
coordinates it was never staged for"
);
// The positive arm: on the host's own layout the staged text is exactly
// what the editor must open with, or "never consult the drill" passes.
app.switch_to_view("Records");
app.rebuild_layout();
let own_key = app
.layout
.cell_key(0, col)
.expect("the host's own records grid names a cell at (0, Revenue)");
assert_eq!(
app.display_value_of(&own_key),
"STAGED",
"the host's own staged drill text was dropped"
);
}
/// Two pages whose non-empty rows differ, so pruning renumbers the grid when
/// the page flips — improvise-7kvw. `prune_empty` is the caller's choice.
fn paged_model(prune: bool) -> App {
use crate::model::cell::{CellKey, CellValue};
use crate::view::Axis;
let mut wb = Workbook::new("Paged");
wb.add_category("Region").unwrap();
wb.add_category("Quarter").unwrap();
for item in ["North", "South", "East"] {
wb.add_item("Region", item).unwrap();
}
for item in ["Q1", "Q2"] {
wb.add_item("Quarter", item).unwrap();
}
wb.add_item("_Measure", "Revenue").unwrap();
for (quarter, region) in [
("Q1", "North"),
("Q1", "South"),
("Q2", "South"),
("Q2", "East"),
] {
wb.model.set_cell(
CellKey::new(vec![
("Quarter".into(), quarter.into()),
("Region".into(), region.into()),
("_Measure".into(), "Revenue".into()),
]),
CellValue::Number(1.0),
);
}
{
let v = wb.active_view_mut();
v.set_axis("Region", Axis::Row);
v.set_axis("_Measure", Axis::Column);
v.set_axis("Quarter", Axis::Page);
v.set_page_selection("Quarter", "Q1");
v.prune_empty = prune;
}
let mut app = App::new(wb, None);
app.rebuild_layout();
app
}
fn row_labels(app: &App) -> Vec<String> {
(0..app.layout.row_count())
.map(|r| app.layout.row_label(r))
.collect()
}
fn flip_to_q2(app: &mut App) {
app.apply_effects(vec![Box::new(super::super::effect::SetPageSelection {
category: "Quarter".to_string(),
item: "Q2".to_string(),
})]);
}
/// BUG (improvise-7kvw): pruning depends on the page, so a page flip renumbers
/// the grid and left both the host's cursor and every client's stale.
#[test]
fn a_page_flip_under_prune_empty_invalidates_the_cursor() {
let mut app = paged_model(true);
let before_labels = row_labels(&app);
let epoch = app.cursor_epoch();
app.cursor_mut().selected = (1, 0);
flip_to_q2(&mut app);
assert_ne!(
before_labels,
row_labels(&app),
"the two pages prune the same rows, so this case proves nothing"
);
assert_ne!(
app.cursor_epoch(),
epoch,
"the grid renumbered and no cursor was invalidated"
);
assert_eq!(
app.cursor().selected,
(0, 0),
"the host's own cursor is stale"
);
}
/// The third arm, and the one `[`/`]` hits constantly: odometer paging emits a
/// `SetPageSelection` per page category per keypress, most of them no-ops.
#[test]
fn selecting_the_page_already_selected_invalidates_no_cursor() {
let mut app = paged_model(true);
// Both preconditions, asserted BEFORE the effect: the effect itself sets
// the page to Q1, so a check after it cannot see a fixture elsewhere.
assert!(
app.view().prune_empty,
"with pruning off the reset never fires whatever `moved` says, so \
this case would pass against the guard it exists to hold"
);
assert_eq!(
app.view().page_selection("Quarter"),
Some("Q1"),
"the fixture was not on Q1, so re-selecting it is not the no-op case"
);
let before_labels = row_labels(&app);
app.cursor_mut().selected = (1, 0);
let epoch = app.cursor_epoch();
app.apply_effects(vec![Box::new(super::super::effect::SetPageSelection {
category: "Quarter".to_string(),
item: "Q1".to_string(),
})]);
assert_eq!(
before_labels,
row_labels(&app),
"re-selecting the same page renumbered the grid, so this proves nothing"
);
assert_eq!(
app.cursor_epoch(),
epoch,
"a page that did not move threw every client's cursor away, and \
odometer paging emits one of these per page category per keypress"
);
assert_eq!(app.cursor().selected, (1, 0));
}
/// The other arm: with pruning off the page names the same rows, so paging
/// must not throw the user's position away on an ordinary `[`/`]`.
#[test]
fn a_page_flip_without_prune_empty_leaves_the_cursor_alone() {
let mut app = paged_model(false);
let before_labels = row_labels(&app);
let epoch = app.cursor_epoch();
app.cursor_mut().selected = (1, 0);
flip_to_q2(&mut app);
assert_eq!(
before_labels,
row_labels(&app),
"the page flip renumbered the grid, so this is not the no-reshape arm"
);
assert_eq!(
app.cursor_epoch(),
epoch,
"paging with nothing pruned threw a cursor away"
);
assert_eq!(app.cursor().selected, (1, 0));
}
/// improvise-h074: the fallback indexes `views` by the document's default, and
/// a bare `IndexMap` panic names no invariant where `active_view()` did.
#[test]
#[should_panic(expected = "active_view always names an existing view")]
fn a_default_that_names_nothing_says_which_invariant_broke() {
let mut app = two_col_model();
// Only a hand-built or foreign-deserialized workbook can be here:
// `create_view`/`delete_view` both maintain the invariant.
app.model_state.workbook.active_view = "NoSuchView".to_string();
let _ = app.resolve_view("AlsoNoSuchView");
}
/// improvise-8uz: moving the cursor must leave the shared document untouched,
/// so one client scrolling cannot move another's viewport.
#[test]
+251 -42
View File
@@ -440,8 +440,14 @@ pub struct SetPageSelection {
}
impl Effect for SetPageSelection {
fn apply(&self, app: &mut App) {
let moved = app.view().page_selection(&self.category) != Some(self.item.as_str());
app.view_mut()
.set_page_selection(&self.category, &self.item);
// Which rows survive pruning is a function of the page, so under
// `prune_empty` a page flip renumbers the grid (improvise-7kvw).
if moved && app.view().prune_empty {
app.reset_cursor();
}
}
fn kind(&self) -> EffectKind {
EffectKind::Model(ModelChange::LayoutGeometry)
@@ -744,24 +750,29 @@ impl Effect for SetDrillPendingEdit {
pub struct Save;
impl Effect for Save {
fn apply(&self, app: &mut App) {
match save_to_current_path(&*app.storage, &app.model_state) {
Ok(msg) => {
app.model_state.dirty = false;
app.view_state.status_msg = msg;
}
// A failed save aborts the rest of the batch, so the `Quit` that
// `wq`/`ZZ` chains never lands (improvise-8tk, gotcha 21).
Err(msg) => {
app.view_state.status_msg = msg;
app.abort_effects = true;
}
let outcome = save_to_current_path(&*app.storage, &app.model_state);
if outcome.is_ok() {
app.model_state.dirty = false;
}
report_file_write(app, outcome);
}
fn kind(&self) -> EffectKind {
EffectKind::ViewOnly
}
}
/// Every effect that writes a file reports through here: the status message either
/// way, and a failure aborts the rest of the batch (improvise-z05r, gotcha 21).
fn report_file_write(app: &mut App, outcome: Result<String, String>) {
match outcome {
Ok(msg) => app.view_state.status_msg = msg,
Err(msg) => {
app.view_state.status_msg = msg;
app.abort_effects = true;
}
}
}
/// Write the workbook to its current file path, returning the status
/// message either way. Missing path and I/O failure are both errors.
fn save_to_current_path(
@@ -782,19 +793,15 @@ fn save_to_current_path(
pub struct SaveAs(pub PathBuf);
impl Effect for SaveAs {
fn apply(&self, app: &mut App) {
match crate::persistence::save_to(&*app.storage, &app.model_state.workbook, &self.0) {
Ok(()) => {
app.model_state.file_path = Some(self.0.clone());
app.model_state.dirty = false;
app.view_state.status_msg = format!("Saved to {}", self.0.display());
}
// The same abort `Save` owes: `SaveAs` is the arm improvise-8tk
// left out, so `:w /bad/path . q` quit on a failed write.
Err(e) => {
app.view_state.status_msg = format!("Save error: {e}");
app.abort_effects = true;
}
let outcome =
crate::persistence::save_to(&*app.storage, &app.model_state.workbook, &self.0)
.map(|()| format!("Saved to {}", self.0.display()))
.map_err(|e| format!("Save error: {e}"));
if outcome.is_ok() {
app.model_state.file_path = Some(self.0.clone());
app.model_state.dirty = false;
}
report_file_write(app, outcome);
}
fn kind(&self) -> EffectKind {
EffectKind::ViewOnly
@@ -930,19 +937,15 @@ pub struct ExportCsv(pub PathBuf);
impl Effect for ExportCsv {
fn apply(&self, app: &mut App) {
let view_name = app.view_name().to_string();
match crate::persistence::export_csv_to(
let outcome = crate::persistence::export_csv_to(
&*app.storage,
&app.model_state.workbook,
&view_name,
&self.0,
) {
Ok(()) => {
app.view_state.status_msg = format!("Exported to {}", self.0.display());
}
Err(e) => {
app.view_state.status_msg = format!("Export error: {e}");
}
}
)
.map(|()| format!("Exported to {}", self.0.display()))
.map_err(|e| format!("Export error: {e}"));
report_file_write(app, outcome);
}
fn kind(&self) -> EffectKind {
EffectKind::ViewOnly
@@ -2004,6 +2007,33 @@ mod tests {
);
}
/// BUG (improvise-z05r): the third arm of a rule with three. Named, so a
/// fourth file-writing effect has to come here — see the evidence.
#[test]
fn every_file_writing_effect_aborts_the_batch_on_failure() {
let bad = PathBuf::from("/nonexistent-dir-improvise-z05r/out");
type Writer = fn(&PathBuf) -> Box<dyn Effect>;
let writers: [(&str, Writer); 3] = [
("Save", |_| Box::new(Save)),
("SaveAs", |p| Box::new(SaveAs(p.clone()))),
("ExportCsv", |p| Box::new(ExportCsv(p.clone()))),
];
for (label, make) in writers {
let mut app = test_app();
app.model_state.file_path = Some(bad.clone());
app.apply_effects(vec![make(&bad), Box::new(ChangeMode(AppMode::Quit))]);
assert!(
app.last_batch_aborted(),
"{label} failed to write and did not stop the batch"
);
assert_ne!(
app.view_state.mode,
AppMode::Quit,
"{label} failed to write and the trailing effect applied anyway"
);
}
}
/// The positive arm: a `SaveAs` that succeeds must not abort. Without it,
/// "always abort" passes the test above.
#[test]
@@ -2327,19 +2357,198 @@ mod tests {
app.apply_effects(vec![Box::new(ApplyAndClearDrill)]);
assert!(app.drill_state.is_none());
assert!(
app.model_state.dirty,
"a drill exited with no edits still marks the document dirty, \
because `apply_effects` raises the flag off the declared \
`EffectKind` and this effect declares `Model(CellData)` whether \
or not it wrote anything (improvise-gv9r). That is the same \
over-approximation a failed `LoadModel` gets, in the same \
direction: one redundant write rather than a lost edit. It has \
been true since the flag moved; this test could not see it \
because it drove `apply` directly and so measured the effect in \
isolation rather than the path production takes."
!app.model_state.dirty,
"a drill exited with no edits wrote nothing, so the document is \
unchanged and `:q` must not be refused (improvise-748f)"
);
}
/// improvise-748f: `ApplyAndClearDrill` on a document with no drill is what
/// view-back emits unconditionally, and it writes nothing at all.
#[test]
fn a_batch_that_writes_nothing_leaves_the_document_clean() {
let mut app = test_app();
assert!(app.drill_state.is_none());
app.apply_effects(vec![Box::new(ApplyAndClearDrill)]);
assert!(
!app.model_state.dirty,
"view-back on a document with no drill open refused the next `:q`"
);
}
/// `CellData` is wider than its name, and a raw-cell fingerprint sees none of
/// these — the cheap cure improvise-748f proposed would have lost them.
#[test]
fn a_cell_data_effect_no_raw_cell_records_still_dirties_the_document() {
type Writer = fn() -> Box<dyn Effect>;
let writers: [(&str, Writer); 5] = [
("AddItem", || {
Box::new(AddItem {
category: "Type".into(),
item: "Toys".into(),
})
}),
("AddItemInGroup", || {
Box::new(AddItemInGroup {
category: "Type".into(),
item: "Toys".into(),
group: "Leisure".into(),
})
}),
("SortData", || Box::new(SortData)),
("AddFormula", || {
Box::new(AddFormula {
raw: "Profit = Revenue - Cost".into(),
target_category: "_Measure".into(),
})
}),
("RemoveFormula", || {
Box::new(RemoveFormula {
target: "Profit".into(),
target_category: "_Measure".into(),
})
}),
];
for (label, make) in writers {
let mut app = test_app();
app.apply_effects(vec![Box::new(AddFormula {
raw: "Profit = Revenue - Cost".into(),
target_category: "_Measure".into(),
})]);
// Inserted out of sorted order, or `SortData` reorders nothing and
// its case proves the opposite of what it names.
for (n, (ty, month)) in [("Food", "Jan"), ("Clothing", "Feb")].iter().enumerate() {
app.model_state.workbook.model.set_cell(
CellKey::new(vec![
("Type".into(), (*ty).into()),
("Month".into(), (*month).into()),
]),
CellValue::Number(n as f64),
);
}
if label == "AddFormula" {
app.apply_effects(vec![Box::new(RemoveFormula {
target: "Profit".into(),
target_category: "_Measure".into(),
})]);
}
app.model_state.dirty = false;
app.apply_effects(vec![make()]);
assert!(
app.model_state.dirty,
"{label} changed the document and left it reading clean, so the \
edit is lost on quit and the worker never writes it back"
);
}
}
/// improvise-748f: the `LayoutGeometry` arm is observed against the **narrow**
/// `document_config`, so an effect it cannot see would read clean forever.
#[test]
fn every_layout_geometry_effect_is_visible_to_the_narrow_fingerprint() {
type Prepare = fn(&mut App);
type Make = fn() -> Box<dyn Effect>;
let nothing: Prepare = |_| {};
let cases: [(&str, Prepare, Make); 13] = [
("AddCategory", nothing, || {
Box::new(AddCategory("Region".into()))
}),
("RemoveCategory", nothing, || {
Box::new(RemoveCategory("Month".into()))
}),
("CreateView", nothing, || {
Box::new(CreateView("Second".into()))
}),
(
"DeleteView",
|app| {
app.model_state.workbook.create_view("Second");
},
|| Box::new(DeleteView("Second".into())),
),
("SetAxis", nothing, || {
Box::new(SetAxis {
category: "Type".into(),
axis: Axis::Column,
})
}),
("SetPageSelection", nothing, || {
Box::new(SetPageSelection {
category: "Type".into(),
item: "Food".into(),
})
}),
(
"ToggleGroup",
|app| {
app.model_state
.workbook
.add_item_in_group("Type", "Cake", "Baked")
.unwrap();
},
|| {
Box::new(ToggleGroup {
category: "Type".into(),
group: "Baked".into(),
})
},
),
("HideItem", nothing, || {
Box::new(HideItem {
category: "Type".into(),
item: "Food".into(),
})
}),
(
"ShowItem",
|app| {
app.view_mut().hide_item("Type", "Food");
},
|| {
Box::new(ShowItem {
category: "Type".into(),
item: "Food".into(),
})
},
),
(
"TransposeAxes",
|app| {
app.view_mut().set_axis("Type", Axis::Row);
app.view_mut().set_axis("Month", Axis::Column);
},
|| Box::new(TransposeAxes),
),
("CycleAxis", nothing, || Box::new(CycleAxis("Type".into()))),
("SetNumberFormat", nothing, || {
Box::new(SetNumberFormat(",.4f".into()))
}),
("TogglePruneEmpty", nothing, || Box::new(TogglePruneEmpty)),
];
for (label, prepare, make) in cases {
let mut app = test_app();
prepare(&mut app);
app.model_state.dirty = false;
let effect = make();
assert!(
matches!(
effect.kind(),
EffectKind::Model(ModelChange::LayoutGeometry)
),
"{label} is not a LayoutGeometry effect, so this case belongs elsewhere"
);
app.apply_effects(vec![effect]);
assert!(
app.model_state.dirty,
"{label} changed the document and `document_config` could not see it, \
so the document reads clean forever"
);
}
}
/// A staged edit to a measure column writes that one cell (improvise-c5v).
#[test]
fn apply_and_clear_drill_with_measure_edit() {
+12 -3
View File
@@ -4,10 +4,10 @@
use std::collections::BTreeMap;
use improvise_core::format::{format_f64, format_value, parse_number_format};
use improvise_core::model::Model;
use improvise_core::model::category::MEASURE_CATEGORY;
use improvise_core::model::cell::CellValue;
use improvise_core::model::record::{AggregateHash, RecordHash};
use improvise_core::model::{Aggregation, Model};
use improvise_core::view::layout::{AxisEntry, GridLayout, RecordCol};
use improvise_core::view::{Axis, View};
use improvise_core::workbook::Workbook;
@@ -197,6 +197,9 @@ fn axis_frame(
collapsed: view.is_group_collapsed(cat_name, group_name),
}));
}
// Queued like a header, never clearing one: a COLLAPSED group has
// no item to carry it and reaches the frame only through `pending`.
AxisEntry::GroupEnd => pending.push(FrameEntry::GroupEnd),
AxisEntry::DataItem(tuple) => {
let idx = data_idx;
data_idx += 1;
@@ -212,6 +215,7 @@ fn axis_frame(
}
AxisFrame {
cats: cats.to_vec(),
first: window.start,
entries,
}
}
@@ -233,7 +237,9 @@ fn column_totals(
.map(|c| {
let total: f64 = (0..layout.row_count())
.filter_map(|r| layout.cell_key(r, c))
.map(|key| model.evaluate_aggregated_f64(&key, &layout.none_cats))
.map(|key| {
model.evaluate_aggregated_f64(&key, Aggregation::of(&layout.none_cats))
})
.sum();
CellDisplay::number(format_f64(total, comma, decimals))
})
@@ -268,7 +274,7 @@ fn cell_display(
app.model_state
.workbook
.model
.evaluate_aggregated(&key, &layout.none_cats)
.evaluate_aggregated(&key, Aggregation::of(&layout.none_cats))
});
CellDisplay {
text: format_value(value.as_ref(), comma, decimals),
@@ -1234,6 +1240,7 @@ mod tests {
.iter()
.map(|e| match e {
FrameEntry::Group(g) => format!("[{}]", g.name),
FrameEntry::GroupEnd => "[/]".to_string(),
FrameEntry::Item(v) => v.join("+"),
})
.collect()
@@ -1755,6 +1762,7 @@ mod tests {
let cell = |n: &str| CellDisplay::number(n);
let mirror = GridCache {
row_frame: AxisFrame {
first: 0,
cats: vec!["Region".into()],
entries: vec![FrameEntry::Item(vec!["North".into()])],
},
@@ -1766,6 +1774,7 @@ mod tests {
};
let desired = GridCache {
row_frame: AxisFrame {
first: 0,
cats: vec!["Department".into()],
entries: mirror.row_frame.entries.clone(),
},
+3 -3
View File
@@ -6,7 +6,7 @@ use unicode_width::UnicodeWidthStr;
use improvise_protocol::header_level_widths;
use crate::format::format_f64;
use crate::model::Model;
use crate::model::{Aggregation, Model};
use crate::view::{AxisEntry, GridLayout};
/// Minimum column width — enough for short numbers/labels + 1 char gap.
@@ -62,7 +62,7 @@ pub fn compute_col_widths(
for (ci, wref) in widths.iter_mut().enumerate().take(n) {
let total: f64 = (0..layout.row_count())
.filter_map(|ri| layout.cell_key(ri, ci))
.map(|key| model.evaluate_aggregated_f64(&key, &layout.none_cats))
.map(|key| model.evaluate_aggregated_f64(&key, Aggregation::of(&layout.none_cats)))
.sum();
let s = format_f64(total, fmt_comma, fmt_decimals);
let w = s.width() as u16;
@@ -82,7 +82,7 @@ pub fn compute_col_widths(
pub fn compute_row_header_width(layout: &GridLayout) -> u16 {
let items = layout.row_items.iter().filter_map(|e| match e {
AxisEntry::DataItem(v) => Some(v),
AxisEntry::GroupHeader { .. } => None,
AxisEntry::GroupHeader { .. } | AxisEntry::GroupEnd => None,
});
header_level_widths(items, layout.row_cats.len().max(1))
.iter()
+1 -1
View File
@@ -4,4 +4,4 @@ pub mod record;
pub mod symbol;
pub mod types;
pub use types::Model;
pub use types::{Aggregation, Model};
+63 -16
View File
@@ -200,6 +200,25 @@ fn eval_is(context: &CellKey, filter: &Filter) -> f64 {
.is_some_and(|v| v == filter.item.as_str()) as i32 as f64
}
/// Whether a grid hides dimensions a cell key does not name. The emptiness of
/// the view's `Axis::None` list is the whole question — `context/code/core.md`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Aggregation {
/// The key names every dimension, so the cell is read as stored.
Exact,
/// Fold every cell the key does not name.
OverHidden,
}
impl Aggregation {
pub fn of(none_cats: &[String]) -> Self {
match none_cats.is_empty() {
true => Aggregation::Exact,
false => Aggregation::OverHidden,
}
}
}
/// Pure-data document model: categories, cells, formulas. It knows nothing
/// about views — see [`crate::workbook::Workbook`] and core.md §10.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -476,8 +495,8 @@ impl Model {
})
}
pub fn evaluate_aggregated(&self, key: &CellKey, none_cats: &[String]) -> Option<CellValue> {
if none_cats.is_empty() {
pub fn evaluate_aggregated(&self, key: &CellKey, agg: Aggregation) -> Option<CellValue> {
if agg == Aggregation::Exact {
return self.evaluate(key);
}
@@ -491,8 +510,8 @@ impl Model {
}
/// [`Self::evaluate_aggregated`] as `f64`, with `0.0` for an empty cell.
pub fn evaluate_aggregated_f64(&self, key: &CellKey, none_cats: &[String]) -> f64 {
self.evaluate_aggregated(key, none_cats)
pub fn evaluate_aggregated_f64(&self, key: &CellKey, agg: Aggregation) -> f64 {
self.evaluate_aggregated(key, agg)
.and_then(|v| v.as_f64())
.unwrap_or(0.0)
}
@@ -815,7 +834,7 @@ mod agg_helper_tests {
#[cfg(test)]
mod model_tests {
use super::Model;
use super::{Aggregation, Model};
use crate::model::cell::{CellKey, CellValue};
fn coord(pairs: &[(&str, &str)]) -> CellKey {
@@ -994,7 +1013,7 @@ mod model_tests {
// With Date hidden, it aggregates to SUM.
let none_cats = vec!["Date".to_string()];
let result = m.evaluate_aggregated(&partial_key, &none_cats);
let result = m.evaluate_aggregated(&partial_key, Aggregation::of(&none_cats));
assert_eq!(result, Some(CellValue::Number(150.0)));
}
@@ -1031,6 +1050,19 @@ mod model_tests {
);
}
#[test]
fn a_hidden_dimension_list_reduces_to_one_of_two_states() {
assert_eq!(Aggregation::of(&[]), Aggregation::Exact);
assert_eq!(
Aggregation::of(&["Date".to_string()]),
Aggregation::OverHidden
);
assert_eq!(
Aggregation::of(&["Date".to_string(), "Payee".to_string()]),
Aggregation::OverHidden
);
}
#[test]
fn evaluate_aggregated_no_hidden_delegates_to_evaluate() {
let mut m = Model::new("Test");
@@ -1039,7 +1071,7 @@ mod model_tests {
m.set_cell(coord(&[("Region", "East")]), CellValue::Number(42.0));
let key = coord(&[("Region", "East")]);
assert_eq!(
m.evaluate_aggregated(&key, &[]),
m.evaluate_aggregated(&key, Aggregation::Exact),
Some(CellValue::Number(42.0))
);
}
@@ -1065,7 +1097,7 @@ mod model_tests {
fn status_of(m: &Model) -> Option<CellValue> {
m.evaluate_aggregated(
&coord(&[("Payee", "Acme"), ("_Measure", "Status")]),
&["Date".to_string()],
Aggregation::of(&["Date".to_string()]),
)
}
@@ -1156,14 +1188,14 @@ mod model_tests {
let key = coord(&[("Payee", "Acme"), ("_Measure", "Price")]);
let none_cats = vec!["Date".to_string()];
let result = m.evaluate_aggregated(&key, &none_cats);
let result = m.evaluate_aggregated(&key, Aggregation::of(&none_cats));
assert_eq!(result, Some(CellValue::Number(20.0))); // avg(10, 30) = 20
}
}
#[cfg(test)]
mod formula_tests {
use super::Model;
use super::{Aggregation, Model};
use crate::formula::parse_formula;
use crate::model::cell::{CellKey, CellValue};
@@ -1715,12 +1747,18 @@ mod formula_tests {
m.recompute_formulas(&none_cats);
// Equality says X is zero…
assert_eq!(
m.evaluate_aggregated(&coord(&[("_Measure", "IsZero")]), &none_cats),
m.evaluate_aggregated(
&coord(&[("_Measure", "IsZero")]),
Aggregation::of(&none_cats)
),
Some(CellValue::Number(1.0))
);
// …so dividing by X must be div/0 in the cached path too.
assert_eq!(
m.evaluate_aggregated(&coord(&[("_Measure", "Ratio")]), &none_cats),
m.evaluate_aggregated(
&coord(&[("_Measure", "Ratio")]),
Aggregation::of(&none_cats)
),
Some(CellValue::Error("div/0".into()))
);
}
@@ -2382,7 +2420,7 @@ mod route_then_aggregate_tests {
let uncached = m.evaluate(&north);
m.recompute_formulas(&none_cats);
let cached = m.evaluate_aggregated(&north, &none_cats);
let cached = m.evaluate_aggregated(&north, Aggregation::of(&none_cats));
assert_eq!(uncached, Some(CellValue::Number(11.0)));
assert_eq!(cached, uncached, "the two evaluators must not diverge");
@@ -2397,7 +2435,10 @@ mod route_then_aggregate_tests {
m.recompute_formulas(&none_cats);
for region in ["North", "South"] {
assert_eq!(
m.evaluate_aggregated(&key(&[("Region", region), (MEASURE, "Pred")]), &none_cats),
m.evaluate_aggregated(
&key(&[("Region", region), (MEASURE, "Pred")]),
Aggregation::of(&none_cats)
),
Some(CellValue::Number(11.0)),
"{region}"
);
@@ -2428,7 +2469,10 @@ mod route_then_aggregate_tests {
m.recompute_formulas(&none_cats);
for (region, expected) in [("North", 1.0), ("South", 0.0)] {
assert_eq!(
m.evaluate_aggregated(&key(&[("Region", region), (MEASURE, "Pred")]), &none_cats),
m.evaluate_aggregated(
&key(&[("Region", region), (MEASURE, "Pred")]),
Aggregation::of(&none_cats)
),
Some(CellValue::Number(expected)),
"{region}"
);
@@ -2444,7 +2488,10 @@ mod route_then_aggregate_tests {
m.recompute_formulas(&none_cats);
for (region, expected) in [("North", 14.0), ("South", 200.0)] {
assert_eq!(
m.evaluate_aggregated(&key(&[("Region", region), (MEASURE, "Pred")]), &none_cats),
m.evaluate_aggregated(
&key(&[("Region", region), (MEASURE, "Pred")]),
Aggregation::of(&none_cats)
),
Some(CellValue::Number(expected)),
"{region}"
);
+105 -28
View File
@@ -1,8 +1,8 @@
use std::rc::Rc;
use crate::model::Model;
use crate::model::category::{DIM_CATEGORY, INDEX_CATEGORY, MEASURE_CATEGORY};
use crate::model::cell::{CellKey, CellValue};
use crate::model::{Aggregation, Model};
use crate::view::{Axis, View};
/// The trailing column that creates a new measure: typing a name into it
@@ -93,6 +93,9 @@ pub enum AxisEntry {
cat_name: String,
group_name: String,
},
/// The run opened by the last `GroupHeader` ends here (improvise-gd83).
/// Nothing is drawn for it; it exists so membership is not inherited.
GroupEnd,
DataItem(Vec<String>),
}
@@ -101,7 +104,7 @@ pub enum AxisEntry {
fn data_items(items: &[AxisEntry]) -> impl Iterator<Item = &Vec<String>> {
items.iter().filter_map(|e| match e {
AxisEntry::DataItem(v) => Some(v),
AxisEntry::GroupHeader { .. } => None,
AxisEntry::GroupHeader { .. } | AxisEntry::GroupEnd => None,
})
}
@@ -134,6 +137,12 @@ fn prune_axis_items(items: &mut Vec<AxisEntry>, keep: impl Fn(usize) -> bool) {
AxisEntry::GroupHeader { .. } => {
pending_header = Some(entry);
}
// The run it closes may have been pruned away entirely, in which
// case the header is still pending and both are dropped together.
AxisEntry::GroupEnd => match pending_header.take() {
Some(_) => {}
None => kept.push(entry),
},
AxisEntry::DataItem(_) => {
if keep(data_idx) {
if let Some(h) = pending_header.take() {
@@ -427,7 +436,7 @@ impl GridLayout {
for (ci, cell) in row.iter_mut().enumerate() {
*cell = self
.cell_key(ri, ci)
.and_then(|k| model.evaluate_aggregated(&k, &self.none_cats))
.and_then(|k| model.evaluate_aggregated(&k, Aggregation::of(&self.none_cats)))
.is_some();
}
}
@@ -485,7 +494,7 @@ impl GridLayout {
self.records_display(row, col).unwrap_or_default()
} else {
self.cell_key(row, col)
.and_then(|key| model.evaluate_aggregated(&key, &self.none_cats))
.and_then(|key| model.evaluate_aggregated(&key, Aggregation::of(&self.none_cats)))
.map(|v| crate::format::format_value(Some(&v), fmt_comma, fmt_decimals))
.unwrap_or_default()
}
@@ -529,34 +538,36 @@ impl GridLayout {
/// the Nth data row.
pub fn row_group_for(&self, data_row: usize) -> Option<(String, String)> {
let vi = self.data_row_to_visual(data_row)?;
self.row_items[..vi].iter().rev().find_map(|e| {
if let AxisEntry::GroupHeader {
cat_name,
group_name,
} = e
{
Some((cat_name.clone(), group_name.clone()))
} else {
None
}
})
self.row_items[..vi]
.iter()
.rev()
.find_map(|e| match e {
AxisEntry::GroupHeader {
cat_name,
group_name,
} => Some(Some((cat_name.clone(), group_name.clone()))),
AxisEntry::GroupEnd => Some(None),
AxisEntry::DataItem(_) => None,
})
.flatten()
}
/// The `(cat_name, group_name)` of the nearest `GroupHeader` preceding
/// the Nth data column.
pub fn col_group_for(&self, data_col: usize) -> Option<(String, String)> {
let vi = self.data_col_to_visual(data_col)?;
self.col_items[..vi].iter().rev().find_map(|e| {
if let AxisEntry::GroupHeader {
cat_name,
group_name,
} = e
{
Some((cat_name.clone(), group_name.clone()))
} else {
None
}
})
self.col_items[..vi]
.iter()
.rev()
.find_map(|e| match e {
AxisEntry::GroupHeader {
cat_name,
group_name,
} => Some(Some((cat_name.clone(), group_name.clone()))),
AxisEntry::GroupEnd => Some(None),
AxisEntry::DataItem(_) => None,
})
.flatten()
}
}
@@ -585,8 +596,12 @@ fn expand_category(
.get(item_name.as_str())
.and_then(|i| i.group.as_deref());
// Emit a group header at each group boundary.
// A header on entering a group; a `GroupEnd` only on leaving one for
// NO group — another group's header already ends the run.
if item_group != last_group {
if last_group.is_some() && item_group.is_none() {
result.push(AxisEntry::GroupEnd);
}
if let Some(g) = item_group {
result.push(AxisEntry::GroupHeader {
cat_name: cat_name.to_string(),
@@ -605,6 +620,11 @@ fn expand_category(
row.push(item_name.to_string());
result.push(AxisEntry::DataItem(row));
}
// The expansion is the end of the run too: the next prefix's items follow
// immediately and must not inherit it (improvise-gd83).
if last_group.is_some() {
result.push(AxisEntry::GroupEnd);
}
result
}
@@ -620,10 +640,14 @@ fn cross_product(model: &Model, view: &View, cats: &[String]) -> Vec<AxisEntry>
.into_iter()
.flat_map(|entry| match entry {
AxisEntry::DataItem(prefix) => expand_category(model, view, cat_name, prefix),
header @ AxisEntry::GroupHeader { .. } => vec![header],
marker @ (AxisEntry::GroupHeader { .. } | AxisEntry::GroupEnd) => vec![marker],
})
.collect();
}
// The axis itself ends the last run; a trailing marker names nothing.
while matches!(result.last(), Some(AxisEntry::GroupEnd)) {
result.pop();
}
result
}
@@ -1114,6 +1138,59 @@ mod tests {
assert!(jan.is_none());
}
/// improvise-gd83: two arms — an ungrouped item AFTER a grouped run, and
/// the same item under a later prefix. context/test-evidence/core.md.
#[test]
fn an_ungrouped_item_after_a_grouped_run_belongs_to_no_group() {
let mut wb = Workbook::new("T");
wb.add_category("Month").unwrap();
wb.add_category("Type").unwrap();
wb.add_item_in_group("Month", "Jan", "Q1").unwrap();
wb.add_item_in_group("Month", "Feb", "Q1").unwrap();
wb.add_item("Month", "Sep").unwrap();
wb.add_item("Type", "Food").unwrap();
let layout = GridLayout::new(&wb.model, wb.active_view());
assert_eq!(
layout.row_group_for(0),
Some(("Month".into(), "Q1".into())),
"precondition: Jan really is in Q1"
);
assert_eq!(
layout.row_group_for(2),
None,
"Sep is in no group — the run ended before it"
);
}
/// The second arm: the run ends at the end of ONE expansion, so the next
/// prefix's first item does not inherit it. context/test-evidence/core.md.
#[test]
fn a_grouped_run_does_not_leak_into_the_next_prefix() {
let mut wb = Workbook::new("T");
wb.add_category("Type").unwrap();
wb.add_category("Recipient").unwrap();
wb.add_item("Type", "Food").unwrap();
wb.add_item("Type", "Clothing").unwrap();
wb.add_item("Recipient", "Carol").unwrap();
wb.add_item_in_group("Recipient", "Alice", "Family")
.unwrap();
wb.add_item_in_group("Recipient", "Bob", "Family").unwrap();
wb.active_view_mut().set_axis("Recipient", Axis::Row);
let layout = GridLayout::new(&wb.model, wb.active_view());
assert_eq!(
layout.row_group_for(2),
Some(("Recipient".into(), "Family".into())),
"precondition: Food/Bob is in Family"
);
assert_eq!(
layout.row_group_for(3),
None,
"Clothing/Carol opens a new prefix and is in no group"
);
}
#[test]
fn ungrouped_items_produce_no_headers() {
let wb = two_cat_workbook();
+23 -2
View File
@@ -70,13 +70,15 @@ impl View {
/// first slot no regular holds. A virtual is **never** bumped (§7).
pub fn on_category_added(&mut self, cat_name: &str) {
if !self.category_axes.contains_key(cat_name) {
let axis = if cat_name.starts_with('_') || self.is_records_layout() {
let virtual_or_records =
CategoryKind::of_name(cat_name).is_virtual() || self.is_records_layout();
let axis = if virtual_or_records {
Axis::None
} else {
let holds_a_regular = |axis: Axis| {
self.categories_on(axis)
.into_iter()
.any(|c| !c.starts_with('_'))
.any(|c| !CategoryKind::of_name(c).is_virtual())
};
match (holds_a_regular(Axis::Row), holds_a_regular(Axis::Column)) {
(false, _) => Axis::Row,
@@ -292,6 +294,25 @@ mod tests {
);
}
/// improvise-bidq: `_Foo` is `CategoryKind::Regular` to every other
/// consumer — see context/test-evidence/core.md.
#[test]
fn a_user_category_whose_name_leads_with_an_underscore_is_seated_as_regular() {
let v = view_with_cats(&["_Foo"]);
assert_eq!(
v.axis_of("_Foo"),
Axis::Row,
"of_name says Regular, so the first slot is Row — not None"
);
let v = view_with_cats(&["_Foo", "Region"]);
assert_eq!(
v.axis_of("Region"),
Axis::Column,
"_Foo holds Row as a regular category, so Region takes Column"
);
}
#[test]
fn first_category_assigned_to_row() {
let v = view_with_cats(&["Region"]);
+4 -2
View File
@@ -20,7 +20,9 @@ formula = { SOI ~ target ~ "=" ~ expr ~ where_clause? ~ EOI }
// to `|Total Revenue|` name the same item.
target = { identifier }
where_clause = { ^"WHERE" ~ identifier ~ "=" ~ filter_value }
// Compound-atomic like `is_expr`, so `sp+` is the real separator: relax it and
// `WHEREAS x = y` reads as a WHERE on a category `AS`.
where_clause = ${ ^"WHERE" ~ sp+ ~ identifier ~ sp* ~ "=" ~ sp* ~ filter_value }
// ---- expressions --------------------------------------------------------
@@ -64,7 +66,7 @@ paren_expr = { "(" ~ expr ~ ")" }
// Aggregates with optional inline WHERE filter inside the parens.
agg_call = { agg_func ~ "(" ~ expr ~ inline_where? ~ ")" }
agg_func = { ^"SUM" | ^"AVG" | ^"MIN" | ^"MAX" | ^"COUNT" }
inline_where = { ^"WHERE" ~ identifier ~ "=" ~ filter_value }
inline_where = ${ ^"WHERE" ~ sp+ ~ identifier ~ sp* ~ "=" ~ sp* ~ filter_value }
// IF(cond, then, else). Comparison is a standalone rule because comparison
// operators are not valid in general expressions — only inside an IF condition.
+29
View File
@@ -463,6 +463,35 @@ mod tests {
assert_eq!(filter.item, "East");
}
#[test]
fn where_does_not_swallow_the_start_of_a_longer_word() {
assert!(parse_formula("X = SUM(Rev WHEREAS x = y)", "Cat").is_err());
assert!(parse_formula("X = Revenue WHEREAS x = y", "Cat").is_err());
assert!(parse_formula("X = Revenue WHEREAS=y", "Cat").is_err());
assert!(parse_formula("X = SUM(Rev WHEREAS=y)", "Cat").is_err());
let inline = parse_formula("X = SUM(Rev WHERE Region = East)", "Cat").unwrap();
assert!(
matches!(&inline.expr, Expr::Agg(_, _, Some(_))),
"{:?}",
inline.expr
);
let trailing = parse_formula("X = Revenue WHERE Region = East", "Cat").unwrap();
assert_eq!(trailing.filter.as_ref().unwrap().category, "Region");
}
/// `sp+` is the boundary, so the keyword needs a space after it exactly as
/// `IS` does — a quoted category no longer abuts it.
#[test]
fn where_requires_a_space_after_the_keyword() {
assert!(parse_formula("X = Revenue WHERE|Region Name|=|East Coast|", "Cat").is_err());
let f = parse_formula("X = Revenue WHERE |Region Name|=|East Coast|", "Cat").unwrap();
let filter = f.filter.as_ref().unwrap();
assert_eq!(
(filter.category.as_str(), filter.item.as_str()),
("Region Name", "East Coast")
);
}
// ── IS, the coordinate predicate (improvise-4zl7) ───────────────────
#[test]
+39 -1
View File
@@ -50,6 +50,29 @@ struct ParsedFile {
views: Vec<PView>,
}
impl ParsedFile {
/// Two `## Category:` sections naming one category are one category. The
/// merge concatenates, so the built order is the order a writer emits.
fn push_category(&mut self, pc: PCategory) {
let PCategory {
name,
items,
group_parents,
} = pc;
match self.categories.iter_mut().find(|e| e.name == name) {
Some(existing) => {
existing.items.extend(items);
existing.group_parents.extend(group_parents);
}
None => self.categories.push(PCategory {
name,
items,
group_parents,
}),
}
}
}
// ── Helpers for walking the pest parse tree ──────────────────────────────────
/// Advance an iterator, returning an error if empty.
@@ -285,7 +308,7 @@ fn walk_file(file: Pair<'_, Rule>) -> Result<ParsedFile> {
Rule::initial_view => {
parsed.initial_view = Some(section_title(&mut pair.into_inner(), "initial_view")?);
}
Rule::category_section => parsed.categories.push(walk_category_section(pair)?),
Rule::category_section => parsed.push_category(walk_category_section(pair)?),
Rule::formulas_section => parsed.formulas.extend(walk_formulas_section(pair)?),
Rule::data_section => parsed.data.extend(walk_data_section(pair)?),
Rule::view_section => parsed.views.push(walk_view_section(pair)?),
@@ -1078,6 +1101,21 @@ mod parser_edge_cases {
assert!(!item_names.is_empty());
}
#[test]
fn two_sections_naming_one_category_round_trip_to_a_stable_group_order() {
let text = "v2025-04-09\n# Test\n## Category: C\n> G1[P1]\n\
## Category: C\n- X[G2]\n> G2[P2]\n";
let fmt = crate::persistence::format_md;
let printed = fmt(&parse_md(text).unwrap());
for owed in ["> G1[P1]", "> G2[P2]", "- X[G2]"] {
assert!(
printed.contains(owed),
"the merge dropped {owed}:\n{printed}"
);
}
assert_eq!(printed, fmt(&parse_md(&printed).unwrap()));
}
#[test]
fn parse_category_with_no_items() {
let text = "v2025-04-09\n# Test\n## Category: Empty\n## Category: Full\n- A\n";
+62 -11
View File
@@ -163,6 +163,9 @@ pub struct FrameGroup {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FrameEntry {
Group(FrameGroup),
/// The run opened by the last `Group` ends here — `AxisEntry::GroupEnd`
/// carried onto the wire (improvise-gd83). Nothing is drawn for it.
GroupEnd,
/// One label per header level, outermost first. Data indices — the
/// `row`/`col` of every other field on [`GridCache`] — count only these.
Item(Vec<String>),
@@ -175,6 +178,10 @@ pub struct AxisFrame {
/// The categories on this axis, outermost first. Empty is a real layout
/// (one nameless row or column), not a gap — hence [`AxisFrame::levels`].
pub cats: Vec<String>,
/// The **absolute** data index of this frame's first item. Zero means the
/// frame leads the axis, which is what [`AxisFrame::body_from`] asks.
#[serde(default)]
pub first: usize,
pub entries: Vec<FrameEntry>,
}
@@ -184,6 +191,7 @@ impl AxisFrame {
pub fn flat<S: Into<String>>(labels: impl IntoIterator<Item = S>) -> Self {
Self {
cats: Vec::new(),
first: 0,
entries: labels
.into_iter()
.map(|l| FrameEntry::Item(vec![l.into()]))
@@ -196,7 +204,7 @@ impl AxisFrame {
pub fn items(&self) -> impl Iterator<Item = &Vec<String>> {
self.entries.iter().filter_map(|e| match e {
FrameEntry::Item(v) => Some(v),
FrameEntry::Group(_) => None,
FrameEntry::Group(_) | FrameEntry::GroupEnd => None,
})
}
@@ -293,20 +301,51 @@ impl AxisFrame {
for entry in &self.entries {
match entry {
FrameEntry::Group(g) => current = Some(g),
FrameEntry::GroupEnd => current = None,
FrameEntry::Item(_) => out.push(current),
}
}
out
}
/// The **nearest preceding** group header — not the item's membership,
/// and deliberately so. **Not for a draw loop**; both points in §3.
/// The item's group **membership** — the nearest preceding header, unless
/// a `GroupEnd` closed it first (improvise-gd83). Not for a draw loop; §3.
pub fn group_of(&self, idx: usize) -> Option<&FrameGroup> {
let visual = self.visual_of(idx)?;
self.entries[..visual].iter().rev().find_map(|e| match e {
FrameEntry::Group(g) => Some(g),
FrameEntry::Item(_) => None,
})
self.entries[..visual]
.iter()
.rev()
.find_map(|e| match e {
FrameEntry::Group(g) => Some(Some(g)),
FrameEntry::GroupEnd => Some(None),
FrameEntry::Item(_) => None,
})
.flatten()
}
/// The runs of adjacent positions sharing a group, coalesced by NAME —
/// the coalescing rule's **one home**, both front ends (improvise-8qlr).
pub fn group_runs(&self) -> Vec<(Option<&FrameGroup>, usize)> {
let mut runs: Vec<(Option<&FrameGroup>, usize)> = Vec::new();
for group in self.groups() {
let name = group.map(|g| g.name.as_str());
match runs.last_mut() {
Some((prev, span)) if prev.map(|p| p.name.as_str()) == name => *span += 1,
_ => runs.push((group, 1)),
}
}
runs
}
/// The group label to paint above each position, `None` where the run
/// continues. Derived from [`AxisFrame::group_runs`], never beside it.
pub fn group_run_heads(&self) -> Vec<Option<&FrameGroup>> {
self.group_runs()
.into_iter()
.flat_map(|(group, span)| {
std::iter::once(group).chain(std::iter::repeat_n(None, span - 1))
})
.collect()
}
/// Position of the data item `idx` within `entries`, group headers
@@ -320,11 +359,16 @@ impl AxisFrame {
.nth(idx)
}
/// The entries a renderer walks from data item `idx`, dropping the group
/// header above it. The argument is scrolling, not the rule — see §3.
/// The entries a renderer walks from data item `idx`. The header above it
/// is dropped, EXCEPT at the top of the axis — §3a, improvise-0o6x.
pub fn body_from(&self, idx: usize) -> &[FrameEntry] {
let start = self.visual_of(idx).unwrap_or(self.entries.len());
&self.entries[start..]
match self.first + idx == 0 {
true => &self.entries[..],
false => {
let start = self.visual_of(idx).unwrap_or(self.entries.len());
&self.entries[start..]
}
}
}
/// The width of each header level, in display cells. Derived rather than
@@ -921,6 +965,7 @@ mod tests {
CellDisplay::number("3"),
]));
grid.apply(&GridUpdate::RowFrame(AxisFrame {
first: 0,
cats: vec!["Zone".into()],
entries: grid.row_frame.entries.clone(),
}));
@@ -953,6 +998,7 @@ mod tests {
/// A two-level axis: [A,x], [A,y], [B,z], with `B` in a collapsed group.
fn two_level_frame() -> AxisFrame {
AxisFrame {
first: 0,
cats: vec!["Type".into(), "Who".into()],
entries: vec![
FrameEntry::Item(vec!["A".into(), "x".into()]),
@@ -1034,6 +1080,7 @@ mod tests {
#[test]
fn header_widths_are_per_level_and_measured_in_display_cells() {
let frame = AxisFrame {
first: 0,
cats: vec!["A".into(), "B".into()],
entries: vec![FrameEntry::Item(vec!["Clothing".into(), "日本".into()])],
};
@@ -1071,6 +1118,7 @@ mod tests {
#[test]
fn an_axis_with_no_categories_still_has_one_level() {
let frame = AxisFrame {
first: 0,
cats: Vec::new(),
entries: vec![FrameEntry::Item(Vec::new())],
};
@@ -1200,6 +1248,7 @@ mod tests {
}))
};
let axis = |cat: &str, labels: [&str; 2]| AxisFrame {
first: 0,
cats: vec![cat.into()],
entries: labels
.iter()
@@ -1225,6 +1274,7 @@ mod tests {
fn renaming_a_row_category_invalidates_every_cell_under_it() {
let mut grid = identified_grid();
let renamed = AxisFrame {
first: 0,
cats: vec!["Department".into()],
entries: grid.row_frame.entries.clone(),
};
@@ -1264,6 +1314,7 @@ mod tests {
fn renaming_a_column_category_invalidates_every_cell_under_it() {
let mut grid = identified_grid();
let renamed = AxisFrame {
first: 0,
cats: vec!["Metric".into()],
entries: grid.col_frame.entries.clone(),
};
+2
View File
@@ -236,6 +236,8 @@ fn declined(name: &str, args: &[String]) -> Option<String> {
"toggle-records-mode" => "records mode",
"add-record-row" => "adding a record row",
"delete-formula-at-cursor" => "deleting a formula",
// NOT "not built": it commits a drill first, so composing it would
// commit another client's typing. Needs addressed commands — clients.md.
"view-back" => "stepping back to the previous view",
// A door this client will not open still has to say so: entering is
// resolvable and the commit behind it is not.
+15 -26
View File
@@ -19,9 +19,8 @@ pub struct Client {
state: ClientState,
}
/// Allocate a client: an empty view slice and one empty cache per widget. A
/// client that draws before its bootstrap draws an empty grid, correctly.
#[wasm_bindgen]
/// An empty view slice and one empty cache per widget. Not exported: the
/// page's door is `new Client()` (improvise-seik).
pub fn init() -> Client {
Client::default()
}
@@ -55,14 +54,8 @@ impl Client {
serde_json::to_string(&message).ok()
}
/// The whole client state as JSON: the view slice, every widget cache, and
/// the revision it corresponds to.
#[wasm_bindgen]
pub fn render_state(&self) -> Result<String, JsError> {
self.state_json().map_err(|e| JsError::new(&e.to_string()))
}
/// The cheap half of [`Client::render_state`]: has anything moved?
/// Has anything moved? The cheap question, and the one `on_frame` already
/// answers by returning it.
#[wasm_bindgen]
pub fn revision(&self) -> u64 {
self.state.revision
@@ -71,24 +64,17 @@ impl Client {
// The transport surface: a handful of string calls and the whole harness a
// page needs. See `context/code/clients.md`.
/// The handshake frame, associated rather than a method. **Prefer
/// [`Client::set_viewport`]** — this one forgets the window (clients.md).
/// The handshake frame, *and* the record of what this client can show.
/// The **only** door, and why that matters: `context/code/clients.md`.
#[wasm_bindgen]
pub fn viewport(rows: usize, cols: usize) -> String {
pub fn set_viewport(&mut self, rows: usize, cols: usize) -> String {
self.state.window = (rows, cols);
serde_json::to_string(&Self::viewport_frame(rows, cols))
// Two `usize` in a unit variant; unreachable, and an empty frame
// is skipped by the server rather than crashing the tab.
.unwrap_or_default()
}
/// The handshake frame, *and* the record of what this client can show —
/// which is not derivable from anything the host sends (clients.md).
#[wasm_bindgen]
pub fn set_viewport(&mut self, rows: usize, cols: usize) -> String {
self.state.window = (rows, cols);
Self::viewport(rows, cols)
}
/// Fold one frame, in arrival order, one at a time; returns the new
/// revision. Never reorder or coalesce — a split batch loses cells.
#[wasm_bindgen]
@@ -316,12 +302,15 @@ mod tests {
);
}
/// The handshake frame is the same either way; `set_viewport` also keeps
/// the window, which is what lets a cursor key resolve at all.
/// The handshake frame, and the window it records — one call, because a
/// second door that sent the frame and forgot the window was silent.
#[test]
fn set_viewport_sends_the_same_frame_and_keeps_the_window() {
fn set_viewport_sends_the_handshake_frame_and_keeps_the_window() {
let mut client = init();
assert_eq!(client.set_viewport(24, 8), Client::viewport(24, 8));
assert_eq!(
serde_json::from_str::<ClientFrame>(&client.set_viewport(24, 8)).unwrap(),
Client::viewport_frame(24, 8)
);
assert_eq!(client.state().window, (24, 8));
}
+18 -19
View File
@@ -216,7 +216,10 @@ pub fn render_model(state: &ClientState) -> RenderModel {
.body_from(0)
.iter()
.scan(0usize, |data_idx, entry| {
// `GroupEnd` is a boundary, not a row: it draws nothing and
// consumes no data index (improvise-gd83).
let row = match entry {
FrameEntry::GroupEnd => return Some(None),
FrameEntry::Group(group) => RenderRow::Group {
label: group.name.clone(),
collapsed: group.collapsed,
@@ -244,8 +247,9 @@ pub fn render_model(state: &ClientState) -> RenderModel {
}
}
};
Some(row)
Some(Some(row))
})
.flatten()
.collect(),
categories: entries
.iter()
@@ -443,26 +447,20 @@ fn col_levels(cols: &AxisFrame) -> Vec<Vec<String>> {
/// The column groups as runs, coalescing equal answers from
/// `AxisFrame::group_of`. Nothing at all when no column is in a group.
fn col_groups(cols: &AxisFrame) -> Vec<RenderColGroup> {
let mut runs: Vec<RenderColGroup> = Vec::new();
let mut any = false;
for group in cols.groups() {
any |= group.is_some();
let label = group.map(|g| g.name.clone());
let collapsed = group.is_some_and(|g| g.collapsed);
// Coalesced by NAME alone, as `src/ui/grid.rs` does, and the run's
// indicator is its FIRST group's. See `context/code/clients.md`.
match runs.last_mut() {
Some(run) if run.label == label => run.span += 1,
_ => runs.push(RenderColGroup {
label,
collapsed,
span: 1,
}),
}
}
// The coalescing rule is `AxisFrame::group_runs`, which the TUI draws
// from too — one home, not two agreeing copies (improvise-8qlr).
let runs = cols.group_runs();
let any = runs.iter().any(|(group, _)| group.is_some());
match any {
true => runs,
false => Vec::new(),
true => runs
.into_iter()
.map(|(group, span)| RenderColGroup {
label: group.map(|g| g.name.clone()),
collapsed: group.is_some_and(|g| g.collapsed),
span,
})
.collect(),
}
}
@@ -520,6 +518,7 @@ mod tests {
#[test]
fn column_groups_coalesce_by_name_as_the_tui_does() {
let frame = AxisFrame {
first: 0,
cats: vec!["Month".into()],
entries: vec![
group("Month", "Q1", false),
@@ -98,10 +98,10 @@ fn a_browser_client_seeds_types_a_number_and_sees_it_come_back() {
// The handshake. The extent comes first because the snapshot seeds the
// *visible* cells: one built at extent zero is an empty grid.
let handshake: ClientFrame = serde_json::from_str(&Client::viewport(10, 10))
let handshake: ClientFrame = serde_json::from_str(&client.set_viewport(10, 10))
.expect("the viewport frame must be one the server reads");
let ClientFrame::Viewport { rows, cols } = handshake else {
panic!("Client::viewport must produce a Viewport frame");
panic!("set_viewport must produce a Viewport frame");
};
let (id, snapshot) = host.connect(rows, cols);
deliver(&mut client, &[snapshot]);
@@ -305,7 +305,7 @@ fn a_resize_reprojects_the_grid_at_the_new_extent() {
"a one-row viewport is seeded with one row"
);
let frames = send(&mut host, id, &Client::viewport(10, 10));
let frames = send(&mut host, id, &client.set_viewport(10, 10));
deliver(&mut client, &frames);
assert_eq!(
client.model().rows.len(),
+4 -1
View File
@@ -297,7 +297,10 @@ mod tests {
fn value_at(wb: &Workbook, row: usize, col: usize) -> Option<CellValue> {
let layout = improvise_core::view::layout::GridLayout::new(&wb.model, wb.active_view());
let key = layout.cell_key(row, col)?;
wb.model.evaluate_aggregated(&key, &layout.none_cats)
wb.model.evaluate_aggregated(
&key,
improvise_core::model::Aggregation::of(&layout.none_cats),
)
}
/// `Storage::list` is how a browser offers a document picker, and it must
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
use improvise_command::ui::app::App;
use improvise_command::ui::reduce;
use improvise_core::model::Aggregation;
use improvise_core::model::cell::{CellKey, CellValue};
use improvise_core::view::Axis;
use improvise_core::view::layout::GridLayout;
@@ -190,7 +191,8 @@ fn the_worker_loads_projects_edits_and_persists_over_post_message() {
fn value_at(wb: &Workbook, row: usize, col: usize) -> Option<CellValue> {
let layout = GridLayout::new(&wb.model, wb.active_view());
let key = layout.cell_key(row, col)?;
wb.model.evaluate_aggregated(&key, &layout.none_cats)
wb.model
.evaluate_aggregated(&key, Aggregation::of(&layout.none_cats))
}
/// A second worker over the same backend sees what the first one saved: what a
+2
View File
@@ -2,9 +2,11 @@
//! Native only; no auth, no TLS, loopback only. `context/code/clients.md`.
pub mod host;
pub mod origin;
pub mod registry;
pub mod transport;
pub use host::{Addressed, Host};
pub use improvise_protocol::{ClientFrame, ServerFrame};
pub use origin::OriginPolicy;
pub use registry::{ClientId, DocumentId, Registry, Routed};
+44 -3
View File
@@ -10,6 +10,7 @@ use tokio::net::TcpListener;
use improvise_command::ui::app::App;
use improvise_core::workbook::Workbook;
use improvise_io::persistence;
use improvise_ws_server::origin::OriginPolicy;
use improvise_ws_server::registry::{DocumentId, Registry};
use improvise_ws_server::transport;
@@ -30,10 +31,22 @@ struct Args {
/// unauthenticated, unencrypted, fully writable documents.
#[arg(long, default_value = "127.0.0.1")]
bind: String,
/// An origin a browser page may open a socket from, e.g.
/// `https://example.github.io`. Repeatable; loopback is always allowed.
#[arg(long = "allow-origin", value_name = "ORIGIN")]
allow_origin: Vec<String>,
}
impl Args {
fn origin_policy(&self) -> OriginPolicy {
OriginPolicy::new(self.allow_origin.iter().cloned())
}
}
fn main() -> Result<()> {
let args = Args::parse();
let origins = args.origin_policy();
let registry = open_all(&args.files)?;
let names: Vec<String> = registry.ids().iter().map(DocumentId::to_string).collect();
@@ -47,15 +60,25 @@ fn main() -> Result<()> {
.await
.with_context(|| format!("failed to bind {}:{}", args.bind, args.port))?;
eprintln!(
"improvise-ws-server listening on ws://{}:{} (no auth, no TLS)\n documents: {}",
"improvise-ws-server listening on ws://{}:{} (no auth, no TLS)\n documents: {}\n \
origins: loopback pages{}",
args.bind,
args.port,
names.join(", ")
names.join(", "),
allowed_suffix(&args.allow_origin)
);
transport::serve(registry, listener).await
transport::serve(registry, listener, origins).await
}))
}
/// What the startup banner adds after "loopback pages".
fn allowed_suffix(named: &[String]) -> String {
match named.is_empty() {
true => String::new(),
false => format!(", {}", named.join(", ")),
}
}
/// Open every named document, or the scratch document if none was named. A
/// stem collision is fatal: a client could not tell the two documents apart.
fn open_all(files: &[PathBuf]) -> Result<Registry> {
@@ -140,6 +163,24 @@ mod tests {
);
}
/// The flag is the only way to widen the policy, and with none given the
/// deployed https page is refused while the local recipe still works.
#[test]
fn allow_origin_widens_the_policy_and_nothing_else_does() {
let bare = Args::parse_from(["improvise-ws-server"]).origin_policy();
assert!(bare.permits(Some("http://localhost:8080")));
assert!(!bare.permits(Some("https://fiddlerwoaroof.github.io")));
let widened = Args::parse_from([
"improvise-ws-server",
"--allow-origin",
"https://fiddlerwoaroof.github.io",
])
.origin_policy();
assert!(widened.permits(Some("https://fiddlerwoaroof.github.io")));
assert!(!widened.permits(Some("https://evil.example")));
}
#[test]
fn no_files_serves_one_scratch_document() {
let registry = open_all(&[]).unwrap();
+132
View File
@@ -0,0 +1,132 @@
//! Which origins may open a socket. A handshake is exempt from the same-origin
//! policy, so this is the only thing that can see a hijack: improvise-kc7n.
use std::net::Ipv4Addr;
/// The origins a browser page may open a socket from. Loopback pages are
/// always allowed; anywhere else has to be named.
#[derive(Debug, Clone, Default)]
pub struct OriginPolicy {
named: Vec<String>,
}
impl OriginPolicy {
/// `named` is what `--allow-origin` collected: a scheme and an authority,
/// with no path, exactly as a browser spells it.
pub fn new(named: impl IntoIterator<Item = String>) -> Self {
Self {
named: named
.into_iter()
.map(|origin| origin.trim_end_matches('/').to_ascii_lowercase())
.collect(),
}
}
/// Whether a handshake carrying `origin` may proceed. Absent is a decision
/// and not a fall-through: `context/code/clients.md` §Origin argues it.
pub fn permits(&self, origin: Option<&str>) -> bool {
match origin {
// A client that sends no `Origin` is not a browser page: curl, a
// native client, this crate's own tests.
None => true,
Some(origin) => self.permits_present(&origin.to_ascii_lowercase()),
}
}
fn permits_present(&self, origin: &str) -> bool {
is_loopback(origin) || self.named.iter().any(|allowed| allowed == origin)
}
}
/// Whether `origin` names a page served from this machine. `null` — a
/// `file://` page or a sandboxed frame — is not one, and that is the point.
fn is_loopback(origin: &str) -> bool {
match origin.split_once("://") {
Some(("http" | "https", authority)) => is_loopback_host(host_of(authority)),
_ => false,
}
}
/// The host an authority names, without its port. An IPv6 literal keeps its
/// brackets, which is how a browser spells it anyway.
fn host_of(authority: &str) -> &str {
match authority.rsplit_once(':') {
Some((host, port)) if !port.is_empty() && port.bytes().all(|b| b.is_ascii_digit()) => host,
_ => authority,
}
}
fn is_loopback_host(host: &str) -> bool {
host == "localhost"
|| host == "[::1]"
|| host.parse::<Ipv4Addr>().is_ok_and(|ip| ip.is_loopback())
}
#[cfg(test)]
mod tests {
use super::*;
fn allowing(origins: &[&str]) -> OriginPolicy {
OriginPolicy::new(origins.iter().map(|o| o.to_string()))
}
/// The hole itself: a page the user did not open, driving the socket from
/// the user's own allowed address.
#[test]
fn a_foreign_origin_is_refused() {
let policy = OriginPolicy::default();
assert!(!policy.permits(Some("https://evil.example")));
assert!(!policy.permits(Some("http://improvise.example.com")));
}
/// The shipped local recipe. Every spelling of loopback a browser can put
/// in the address bar, on any port, over either scheme.
#[test]
fn a_loopback_page_is_allowed_by_default() {
let policy = OriginPolicy::default();
for origin in [
"http://localhost:8080",
"http://127.0.0.1:8080",
"https://localhost",
"http://[::1]:8080",
"http://127.0.0.2:3000",
] {
assert!(policy.permits(Some(origin)), "{origin} must be allowed");
}
}
/// `null` is what a `file://` page and a sandboxed frame send, and both of
/// those are exactly what the check exists to keep out.
#[test]
fn the_null_origin_is_not_loopback() {
assert!(!OriginPolicy::default().permits(Some("null")));
}
/// A host that merely *contains* a loopback spelling is a different host.
#[test]
fn a_hostname_that_looks_like_loopback_is_not() {
let policy = OriginPolicy::default();
assert!(!policy.permits(Some("http://localhost.evil.example")));
assert!(!policy.permits(Some("http://127.0.0.1.evil.example")));
assert!(!policy.permits(Some("ws://localhost:8080")));
}
/// The deployed page reaches the server through a TLS proxy, so its origin
/// is named rather than guessed.
#[test]
fn a_named_origin_is_allowed_and_its_neighbours_are_not() {
let policy = allowing(&["https://fiddlerwoaroof.github.io"]);
assert!(policy.permits(Some("https://fiddlerwoaroof.github.io")));
assert!(policy.permits(Some("https://FiddlerWoaroof.GitHub.io")));
assert!(!policy.permits(Some("http://fiddlerwoaroof.github.io")));
assert!(!policy.permits(Some("https://fiddlerwoaroof.github.io.evil.example")));
}
/// Decided rather than defaulted: no browser omits `Origin` on a handshake,
/// so an absent one is a client an address check already answers.
#[test]
fn an_absent_origin_is_allowed() {
assert!(OriginPolicy::default().permits(None));
assert!(allowing(&["https://example.com"]).permits(None));
}
}
+97 -6
View File
@@ -10,12 +10,14 @@ use futures_util::{SinkExt, StreamExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{mpsc, oneshot};
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::tungstenite::handshake::server::{Request, Response};
use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response};
use tokio_tungstenite::tungstenite::http::{StatusCode, header};
use tokio_tungstenite::tungstenite::protocol::CloseFrame;
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
#[cfg(doc)]
use crate::host::Host;
use crate::origin::OriginPolicy;
use crate::registry::{ClientId, ConnectError, Registry, Routed};
use improvise_protocol::{ClientFrame, ServerFrame};
@@ -36,15 +38,17 @@ enum Inbound {
/// Accept connections on `listener` until it fails, serving every document in
/// `registry`, on the current thread in a `LocalSet` the caller supplies.
pub async fn serve(registry: Registry, listener: TcpListener) -> Result<()> {
pub async fn serve(registry: Registry, listener: TcpListener, origins: OriginPolicy) -> Result<()> {
let (tx, rx) = mpsc::unbounded_channel();
let documents = tokio::task::spawn_local(registry_loop(registry, rx));
let origins = Rc::new(origins);
while let Ok((stream, _peer)) = listener.accept().await {
let tx = tx.clone();
let origins = Rc::clone(&origins);
tokio::task::spawn_local(async move {
// A connection that goes wrong is that connection's problem.
let _ = connection(stream, tx).await;
let _ = connection(stream, tx, origins).await;
});
}
drop(tx);
@@ -114,14 +118,23 @@ fn attach(
/// One websocket connection, from handshake to close. The large error type is
/// tungstenite's and is never returned: refusal is the registry's answer.
#[allow(clippy::result_large_err)]
async fn connection(stream: TcpStream, tx: mpsc::UnboundedSender<Inbound>) -> Result<()> {
async fn connection(
stream: TcpStream,
tx: mpsc::UnboundedSender<Inbound>,
origins: Rc<OriginPolicy>,
) -> Result<()> {
// The document is named in the URL, which is readable only during the
// handshake, so it is captured out of the callback.
let requested: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
let seen = Rc::clone(&requested);
let ws = tokio_tungstenite::accept_hdr_async(stream, move |req: &Request, res: Response| {
*seen.borrow_mut() = document_of(req.uri().path());
Ok(res)
match admitted(&origins, req) {
true => {
*seen.borrow_mut() = document_of(req.uri().path());
Ok(res)
}
false => Err(forbidden()),
}
})
.await
.context("websocket handshake failed")?;
@@ -185,6 +198,29 @@ async fn connection(stream: TcpStream, tx: mpsc::UnboundedSender<Inbound>) -> Re
Ok(())
}
/// Whether `origins` permits this handshake. Two `Origin` headers, or one that
/// is not text, name no origin a policy could answer for and are refused.
fn admitted(origins: &OriginPolicy, req: &Request) -> bool {
let mut stated = req.headers().get_all(header::ORIGIN).iter();
match (stated.next(), stated.next()) {
(None, _) => origins.permits(None),
(Some(value), None) => value
.to_str()
.is_ok_and(|origin| origins.permits(Some(origin))),
(Some(_), Some(_)) => false,
}
}
/// What a refused handshake is answered with. A browser surfaces none of it to
/// the page, so the wording is for whoever is reading a proxy log.
fn forbidden() -> ErrorResponse {
let mut response = ErrorResponse::new(Some(
"origin not allowed; name it with --allow-origin".to_string(),
));
*response.status_mut() = StatusCode::FORBIDDEN;
response
}
/// The document a request path names; `/` and `` are "whichever one there is".
/// No unescaping: a `DocumentId` is a name a host chose, not a path.
fn document_of(path: &str) -> Option<String> {
@@ -230,4 +266,59 @@ mod tests {
// name, so a nested-looking path is simply a name nothing serves.
assert_eq!(document_of("/a/b"), Some("a/b".to_string()));
}
fn handshake(origin: Option<&[u8]>) -> Request {
let mut req = Request::builder().uri("/").body(()).unwrap();
if let Some(origin) = origin {
req.headers_mut().insert(
header::ORIGIN,
header::HeaderValue::from_bytes(origin).unwrap(),
);
}
req
}
/// A proxy that appends rather than sets `Origin` leaves two, and reading
/// the first would let the second be anything at all.
#[test]
fn two_origins_state_none_this_policy_can_answer_for() {
let mut req = handshake(Some(b"http://localhost:8080"));
req.headers_mut().append(
header::ORIGIN,
header::HeaderValue::from_static("https://evil.example"),
);
assert_eq!(req.headers().get_all(header::ORIGIN).iter().count(), 2);
assert!(!admitted(&OriginPolicy::default(), &req));
// And the allowed one alone is still allowed, so the refusal above is
// the second header and not the first.
assert!(admitted(
&OriginPolicy::default(),
&handshake(Some(b"http://localhost:8080"))
));
}
/// The seam between the policy and the handshake, including the one case
/// the policy cannot be asked about: an `Origin` that is not text.
#[test]
fn the_handshake_refuses_what_the_policy_refuses() {
let origins = OriginPolicy::default();
assert!(admitted(
&origins,
&handshake(Some(b"http://localhost:8080"))
));
assert!(admitted(&origins, &handshake(None)));
assert!(!admitted(
&origins,
&handshake(Some(b"https://evil.example"))
));
assert!(!admitted(&origins, &handshake(Some(b"http://\xffevil"))));
}
/// A refusal has to be a refused upgrade rather than an accepted socket
/// the server then ignores.
#[test]
fn a_refusal_is_a_forbidden_response() {
assert_eq!(forbidden().status(), StatusCode::FORBIDDEN);
}
}
+119 -2
View File
@@ -4,6 +4,7 @@
use futures_util::{SinkExt, StreamExt};
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::tungstenite::http::StatusCode;
use improvise_command::ui::app::App;
use improvise_core::model::cell::CellValue;
@@ -13,6 +14,7 @@ use improvise_protocol::{
BasisCache, CellId, Command, GridCache, GridUpdate, ModelCommand, Projection, WidgetCache,
};
use improvise_protocol::{ClientFrame, ServerFrame};
use improvise_ws_server::origin::OriginPolicy;
use improvise_ws_server::registry::{DocumentId, Registry};
/// One document, served as `quarterly`.
@@ -50,7 +52,12 @@ fn a_client_on_a_socket_is_seeded_and_gets_its_edit_back() {
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::task::spawn_local(async move {
let _ = improvise_ws_server::transport::serve(registry(), listener).await;
let _ = improvise_ws_server::transport::serve(
registry(),
listener,
OriginPolicy::default(),
)
.await;
});
// No path: with one document served, `/` reaches it, exactly as it
@@ -198,7 +205,9 @@ fn the_url_path_picks_the_document() {
registry
.open(DocumentId::new("beta"), named_app("Beta", "Bison"))
.unwrap();
let _ = improvise_ws_server::transport::serve(registry, listener).await;
let _ =
improvise_ws_server::transport::serve(registry, listener, OriginPolicy::default())
.await;
});
for (path, row) in [("alpha", "Aardvark"), ("beta", "Bison")] {
@@ -262,3 +271,111 @@ fn named_app(name: &str, row: &str) -> App {
app.rebuild_layout();
app
}
/// A hostile page an allowed user visits opens a socket from that user's own
/// allowed address. Only `Origin` separates it from the real page.
#[test]
fn a_handshake_from_a_foreign_origin_is_refused() {
with_server(OriginPolicy::default(), |addr| async move {
let Err(refused) =
tokio_tungstenite::connect_async(request(addr, Some("https://evil.example"))).await
else {
panic!("a foreign origin must not reach the document");
};
// The kind, not the wording: a refused upgrade and a dropped socket
// are different outcomes and only one of them is this fix.
let tokio_tungstenite::tungstenite::Error::Http(answer) = refused else {
panic!("the handshake failed for some other reason: {refused}");
};
assert_eq!(answer.status(), StatusCode::FORBIDDEN);
});
}
/// The shipped local recipe: the page is served by `python3 -m http.server`
/// on loopback and opens a socket to the server on another loopback port.
#[test]
fn a_handshake_from_a_loopback_page_is_accepted() {
with_server(OriginPolicy::default(), |addr| async move {
let (mut ws, _) =
tokio_tungstenite::connect_async(request(addr, Some("http://localhost:8080")))
.await
.expect("the local pairing recipe must keep working");
send(&mut ws, ClientFrame::Viewport { rows: 10, cols: 10 }).await;
assert!(matches!(recv(&mut ws).await, ServerFrame::Snapshot(_)));
});
}
/// Non-browser clients — curl, a native client, this file's other tests —
/// send no `Origin` at all, and absent is decided rather than defaulted.
#[test]
fn a_handshake_with_no_origin_is_accepted() {
with_server(OriginPolicy::default(), |addr| async move {
let (mut ws, _) = tokio_tungstenite::connect_async(request(addr, None))
.await
.expect("a client that sends no Origin is not a browser page");
send(&mut ws, ClientFrame::Viewport { rows: 10, cols: 10 }).await;
assert!(matches!(recv(&mut ws).await, ServerFrame::Snapshot(_)));
});
}
/// The deployed page reaches the server through a TLS proxy, and `Origin` is
/// the one thing that survives the proxy to say which page it was.
#[test]
fn a_named_origin_reaches_the_document() {
let named = OriginPolicy::new(["https://fiddlerwoaroof.github.io".to_string()]);
with_server(named, |addr| async move {
let (mut ws, _) = tokio_tungstenite::connect_async(request(
addr,
Some("https://fiddlerwoaroof.github.io"),
))
.await
.expect("--allow-origin must actually widen the policy");
send(&mut ws, ClientFrame::Viewport { rows: 10, cols: 10 }).await;
assert!(matches!(recv(&mut ws).await, ServerFrame::Snapshot(_)));
// Widening for one page does not widen for its neighbours.
assert!(
tokio_tungstenite::connect_async(request(addr, Some("https://evil.example")))
.await
.is_err()
);
});
}
/// A handshake request for `addr`, carrying `origin` when there is one.
fn request(
addr: std::net::SocketAddr,
origin: Option<&str>,
) -> tokio_tungstenite::tungstenite::http::Request<()> {
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
let mut request = format!("ws://{addr}/")
.into_client_request()
.expect("a loopback address is a valid websocket url");
if let Some(origin) = origin {
request
.headers_mut()
.insert("Origin", origin.parse().expect("a header value"));
}
request
}
/// Serve the one-document fixture on an ephemeral port and run `body`.
fn with_server<F, Fut>(origins: OriginPolicy, body: F)
where
F: FnOnce(std::net::SocketAddr) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let local = tokio::task::LocalSet::new();
runtime.block_on(local.run_until(async move {
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::task::spawn_local(async move {
let _ = improvise_ws_server::transport::serve(registry(), listener, origins).await;
});
body(addr).await;
}));
}
+175 -655
View File
@@ -1,655 +1,175 @@
{"version": 2, "width": 120, "height": 37, "timestamp": 1775772329, "idle_time_limit": 2.0, "env": {"SHELL": "/bin/zsh", "TERM": "screen-256color"}}
[0.255122, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"]
[0.26349, "o", "\u001b]0;~/git_repos/git.fiddlerwoaroof.com/u/edwlan/improvise\u0007"]
[0.389731, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J---\r\n(0) Mac:edwlan--s059 ~gf/u/edwlan/improvise \u001b[30m\u001b[35m\u001b[39mgit\u001b[35m\u001b[33m->\u001b[35m\u001b[32mmain\u001b[35m\u001b[39m\u001b[00m 2026-04-09 15:05:29\r\n16025:% \u001b[K"]
[0.389839, "o", "\u001b[?2004h"]
[3.260628, "o", "\u001b[3m./target/release/improvise examples/demo.improv\u001b[23m"]
[4.100025, "o", "\u001b[47D\u001b[23m.\u001b[23m/\u001b[23mt\u001b[23ma\u001b[23mr\u001b[23mg\u001b[23me\u001b[23mt\u001b[23m/\u001b[23mr\u001b[23me\u001b[23ml\u001b[23me\u001b[23ma\u001b[23ms\u001b[23me\u001b[23m/\u001b[23mi\u001b[23mm\u001b[23mp\u001b[23mr\u001b[23mo\u001b[23mv\u001b[23mi\u001b[23ms\u001b[23me\u001b[23m \u001b[23me\u001b[23mx\u001b[23ma\u001b[23mm\u001b[23mp\u001b[23ml\u001b[23me\u001b[23ms\u001b[23m/\u001b[23md\u001b[23me\u001b[23mm\u001b[23mo\u001b[23m.\u001b[23mi\u001b[23mm\u001b[23mp\u001b[23mr\u001b[23mo\u001b[23mv\u001b[?2004l"]
[4.10015, "o", "\r\r\n"]
[4.101098, "o", "\u001b]0;./target/release/improvise examples/demo.improv\u0007\u001b[2 q"]
[4.137227, "o", "\u001b[?1049h"]
[4.139474, "o", "\u001b[1;1H\u001b[1m\u001b[38;5;0;48;5;4m improvise · Acme Sales Demo (demo.improv) ?:help :q quit \u001b[2;1H\u001b[22m\u001b[39;49m┌\u001b[2;3HView:\u001b[2;9HDefault\u001b[2;17H───────────────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[3;1H│\u001b[38;5;5;49m [Customer = Stark Enterprises] \u001b[3;120H\u001b[39;49m│\u001b[4;1H│\u001b[4;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost \u001b[24m Revenue Profit \u001b[4;120H\u001b[22m\u001b[39;49m│\u001b[5;1H│\u001b[5;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-02 2025-03 2025-01 2025-02 2025-03 2025-01 2025-02 2025-03\u001b[5;120H\u001b[22m\u001b[39;49m│\u001b[6;1H│\u001b[38;5;8;49m────────────────────────────────────────────────"]
[4.139536, "o", "──────────────────────────────────────────────────────────────────────\u001b[39;49m│\u001b[7;1H│\u001b[1m\u001b[38;5;6;48;5;237mGadgets North \u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[23m\u001b[39;48;5;237m \u001b[39;49m│\u001b[8;1H│\u001b[8;12HEast\u001b[8;18H\u001b[3m 5,180\u001b[38;5;8;49m \u001b[39;49m 5,670 7,400\u001b[38;5;8;49m \u001b[39;49m 8,100 2,220\u001b[38;5;8;49m \u001b[39;49m 2,430\u001b[8;120H\u001b[23m│\u001b[9;1H│\u001b[9;12HSouth\u001b[9;18H\u001b[3m\u001b[38;5;8;49m \u001b[9;120H\u001b[23m\u001b[39;49m│\u001b[10;1H│\u001b[10;12HWest\u001b[10;18H\u001b[3m\u001b[38;5;8;49m \u001b[10;120H\u001b[23m\u001b[39;49m│\u001b[11;1H│Widgets\u001b[11;12HNorth\u001b[11;18H\u001b[3m\u001b[38;5;8;49m "]
[4.139643, "o", " \u001b[11;120H\u001b[23m\u001b[39;49m│\u001b[12;1H│\u001b[12;12HEast\u001b[12;18H\u001b[3m 7,080\u001b[38;5;8;49m \u001b[39;49m 7,500 11,800\u001b[38;5;8;49m \u001b[39;49m 12,500 4,720\u001b[38;5;8;49m \u001b[39;49m 5,000\u001b[12;120H\u001b[23m│\u001b[13;1H│\u001b[13;12HSouth\u001b[13;18H\u001b[3m\u001b[38;5;8;49m \u001b[13;120H\u001b[23m\u001b[39;49m│\u001b[14;1H│\u001b[14;12HWest\u001b[14;18H\u001b[3m\u001b[38;5;8;49m \u001b[14;120H\u001b[23m\u001b[39;49m│\u001b[15;1H│Sprockets\u001b[15;12HNorth\u001b[15;18H\u001b[3m\u001b[38;5;8;49m \u001b[15;120H\u001b[23m\u001b[39;49m│\u001b[16;1H│\u001b[16;12HEast\u001b[16;18H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 2,940\u001b[38;5;8;49m \u001b[39;49m 4,200\u001b[38;5;8;49m \u001b[39;49m 1,260\u001b[38;5;8;49m \u001b[16;120H\u001b[23m\u001b[39;49m│\u001b[17;1H│\u001b[17;12HSouth\u001b[17;18H\u001b[3m\u001b[38;5;8;49m \u001b[17;120H\u001b[23m\u001b[39;49m│\u001b[18;1H│\u001b[18;12HWest"]
[4.139729, "o", "\u001b[18;18H\u001b[3m\u001b[38;5;8;49m \u001b[18;120H\u001b[23m\u001b[39;49m│\u001b[19;1H│\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[39;49m│\u001b[20;1H│\u001b[1m\u001b[38;5;3;49mTotal 12,260 2,940 13,170 19,200 4,200 20,600 6,940 1,260 7,430\u001b[20;120H\u001b[22m\u001b[39;49m│\u001b[21;1H│\u001b[21;120H│\u001b[22;1H│\u001b[22;120H│\u001b[23;1H│\u001b[23;120H│\u001b[24;1H│\u001b[24;120H│\u001b[25;1H│\u001b[25;120H│\u001b[26;1H│\u001b[26;120H│\u001b[27;1H│\u001b[27;120H│\u001b[28;1H│\u001b[28;120H│\u001b[29;1H│\u001b[29;120H│\u001b[30;1H│\u001b[30;120H│\u001b[31;1H│\u001b[31;120H│\u001b[32;1H│\u001b[32;120H│\u001b[33;1H│\u001b[33;120H│\u001b[34;1H│\u001b[34;120H│\u001b[35;1H└───────────────────"]
[4.139761, "o", "───────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[36;1H\u001b[38;5;7;49m Tiles: \u001b[36;10H\u001b[38;5;8;49m [_Index ·] [_Dim ·] \u001b[38;5;4;49m [_Measure Col] \u001b[38;5;5;49m [Customer Pag] \u001b[38;5;8;49m [Date ·] \u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;4;49m [Date_Month Col] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL hjkl:nav i:edit R:records P:prune F/C/V:panels T:tiles [:]:page >:drill ::cmd Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.242195, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.344691, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.447079, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.549689, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.652179, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.754674, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.856161, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.958622, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.060824, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.163294, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.265591, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.367483, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.389269, "o", "\u001b[4;17H\u001b[1m\u001b[4m\u001b[38;5;3;49m \u001b[4;21HCost \u001b[4;41H\u001b[24m Revenue \u001b[4;67HProfit \u001b[4;89H\u001b[22m\u001b[39;49m \u001b[5;17H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-02 2025-03 2025-01 2025-02 2025-03 2025-01 2025-02 2025-03\u001b[22m\u001b[39;49m \u001b[7;12H\u001b[1m\u001b[38;5;6;48;5;237mEas\u001b[7;16H \u001b[3m\u001b[38;5;0;48;5;6m \u001b[7;20H5,180\u001b[22m\u001b[38;5;8;48;5;237m \u001b[7;33H\u001b[38;5;15;48;5;237m 5,670 7,400\u001b[7;57H 8,100 2,220\u001b[7;81H 2,430\u001b[23m\u001b[39;48;5;237m \u001b[8;2H\u001b[39;49mWidgets\u001b[8;17H\u001b[3m \u001b[8;20H7,080\u001b[38;5;8;49m \u001b[8;33H\u001b[39;49m \u001b[8;36H7,500 \u001b[8;43H11,80\u001b[8;49H\u001b[38;5;8;49m \u001b[8;57H\u001b[39;49m \u001b[8;59H12,50\u001b[8;65H \u001b[8;68H4,7\u001b[8;72H0\u001b[38;5;8;49m \u001b[8;81H\u001b[39;49m \u001b[8;84H5,000\u001b[23m \u001b[9;2HSprockets\u001b[9;12HEas\u001b[9;16H \u001b[3m\u001b[38;5;8;49m \u001b[9;25H\u001b[39;49m 2,940\u001b[9;49H 4,200\u001b[9;73H 1,260\u001b[9;89H\u001b[23m \u001b[10;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────"]
[5.389472, "o", "────────────────────────────────\u001b[11;2H\u001b[1m\u001b[38;5;3;49mTotal 12,260 2,940 13,170 19,200 4,200 20,600 6,940 1,260 7,430\u001b[22m\u001b[39;49m \u001b[12;12H \u001b[12;18H \u001b[13;12H \u001b[13;18H \u001b[14;12H \u001b[14;18H \u001b[15;2H \u001b[15;12H \u001b[15;18H \u001b[16;12H \u001b[16;18H \u001b[17;12H \u001b[17;18H \u001b[18;12H \u001b[18;18H \u001b[19;2H \u001b[20;2H "]
[5.389587, "o", " \u001b[37;11H\u001b[38;5;0;48;5;8mHiding empty rows/columns \u001b[37;40H \u001b[37;49H \u001b[37;63H \u001b[37;72H \u001b[37;82H \u001b[37;91H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.491141, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.592963, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.69456, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.797252, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.899269, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.001855, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.102804, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.204488, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.306416, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.408174, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.515144, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.614322, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.715087, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.816824, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.919456, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.023808, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.126353, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.227706, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.329812, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.431498, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.533584, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.635475, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.738315, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.840254, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.942468, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.044291, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.049255, "o", "\u001b[36;10H\u001b[1m\u001b[38;5;0;48;5;6m [_Index ·] \u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;5m TILES Hiding empty rows/columns Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.151473, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.252708, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.354391, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.456511, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.558371, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.66005, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.761856, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.839104, "o", "\u001b[36;10H\u001b[38;5;8;49m [_Index ·] \u001b[1m\u001b[38;5;0;48;5;6m [_Dim ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.941054, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.043964, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.146777, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.24872, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.29841, "o", "\u001b[36;22H\u001b[38;5;8;49m [_Dim ·] \u001b[1m\u001b[38;5;0;48;5;6m [_Measure Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.399634, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.500481, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.602021, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.704492, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.76467, "o", "\u001b[36;32H\u001b[38;5;4;49m [_Measure Col] \u001b[1m\u001b[38;5;0;48;5;6m [Customer Pag] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.866679, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.96862, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.070632, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.168973, "o", "\u001b[36;48H\u001b[38;5;5;49m [Customer Pag] \u001b[1m\u001b[38;5;0;48;5;6m [Date ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.271432, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.373277, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.475256, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.54583, "o", "\u001b[36;64H\u001b[38;5;8;49m [Date ·] \u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.647847, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.750326, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.851916, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.904656, "o", "\u001b[36;74H\u001b[38;5;2;49m [Product Row] \u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.007234, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.108949, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.210797, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.305394, "o", "\u001b[36;89H\u001b[38;5;2;49m [Region Row] \u001b[1m\u001b[38;5;0;48;5;6m [Date_Month Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.40791, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.509599, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.612098, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.714734, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.817078, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.837581, "o", "\u001b[1;47H\u001b[1m\u001b[38;5;0;48;5;4m[+]\u001b[4;20H\u001b[4m\u001b[38;5;3;49mCost\u001b[24m Revenue Profit\u001b[22m\u001b[39;49m \u001b[5;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets East \u001b[3m\u001b[38;5;0;48;5;6m 10,850\u001b[22m\u001b[38;5;15;48;5;237m 15,500 4,650\u001b[23m\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49mWidgets East \u001b[3m 14,580 24,300 9,720\u001b[23m \u001b[8;2HSprockets\u001b[8;19H\u001b[3m2,940 4,200 \u001b[8;34H1,260\u001b[23m \u001b[9;2H\u001b[38;5;8;49m────────────"]
[11.837774, "o", "──────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[10;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 44,000 15,630\u001b[22m\u001b[39;49m \u001b[11;2H \u001b[36;116H\u001b[1m\u001b[38;5;0;48;5;6m·] \u001b[22m\u001b[39;49m \u001b[37;10H\u001b[38;5;0;48;5;5mDate_Month →\u001b[37;23HN\u001b[37;25Hne \u001b[37;110HDefault \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.939188, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.040703, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.142444, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.243643, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.344543, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.44606, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.546854, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.648382, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.749506, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.85104, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.952602, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.037101, "o", "\u001b[36;103H\u001b[38;5;8;49m [Date_Month ·] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL Date_Month → None Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.1401, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.241851, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.343997, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.445753, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.547755, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.648763, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.750013, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.851891, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.953275, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.055126, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.15682, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.258532, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.359739, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.46102, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.562788, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.664633, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.766446, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.868114, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.969328, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.070964, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.172609, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.274437, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.376279, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.477908, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.579437, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.624853, "o", "\u001b[2;9H_Dril\u001b[2;15H ─\u001b[3;4H\u001b[38;5;5;49m_Measure\u001b[3;15HCost | Customer = Stark Enterprises | Product = Gadgets | Region = East] \u001b[4;6H\u001b[1m\u001b[4m\u001b[38;5;3;49m Customer\u001b[4;25H\u001b[24m Date Product Region Date_Month Value\u001b[6;2H\u001b[38;5;6;48;5;237m0 \u001b[38;5;0;48;5;6m Stark Enterprises\u001b[22m\u001b[38;5;15;48;5;237m 2025-03-25 Gadgets East 2025-03 5670\u001b[7;2H\u001b[39;49m1 Stark En\u001b[7;16Herprises 2025-01-31 Gadgets\u001b[7;46HEast\u001b[7;54H2025-01\u001b[7;63H5180\u001b[8;2H \u001b[8;12H \u001b[8;17H \u001b[9;2H \u001b[10;2H \u001b[36;10H\u001b[38;5;2;49m [_Index Row] \u001b[38;5;4;49m [_Dim Col] \u001b[38;5;5;49m [_Measure Pag] [Customer Pag] \u001b[38;5;8;49m [Date ·] \u001b[38;5;5;49m [Product Pag] [Region Pag] \u001b[38;5;8;49m▶\u001b[39;49m \u001b[37;12H\u001b[38;5;0;48;5;8mrilled into\u001b[37;24Hcell:\u001b[37;30H2\u001b[37;32Hrows\u001b[37;110H _Drill\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.726365, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.827956, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.929775, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.031367, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.132934, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.234738, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.335777, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.436366, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.53795, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.639488, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.74103, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.843208, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.944693, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.046398, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.148618, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.250351, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.351896, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.453688, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.555582, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.656954, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.758333, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.859977, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.961612, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.062956, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.164668, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.26625, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.367619, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.46896, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.570702, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.672674, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.774225, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.875264, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.976852, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.077943, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.179556, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.280996, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.382478, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.422995, "o", "\u001b[2;9HDefau\u001b[2;15Ht \u001b[3;4H\u001b[38;5;5;49mCustomer\u001b[3;15HStark Enterprises] \u001b[39;49m \u001b[4;6H \u001b[1m\u001b[4m\u001b[38;5;3;49m Cost\u001b[4;25H\u001b[24mRevenue Profit\u001b[22m\u001b[39;49m \u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets East \u001b[3m\u001b[38;5;0;48;5;6m 10,850\u001b[22m\u001b[38;5;15;48;5;237m 15,500 4,650\u001b[23m\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49mWidgets Eas\u001b[7;16H \u001b[3m 14,580 24,300 9,720\u001b[23m \u001b[7;46H \u001b[7;54H \u001b[7;63H \u001b[8;2HSprockets\u001b[8;12HEast\u001b[8;17H\u001b[3m 2,940 4,200 1,260\u001b[9;2H\u001b[23m\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[10;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 44,000 15,630\u001b[36;10H\u001b[22m\u001b[38;5;8"]
[19.42323, "o", ";49m [_Index ·] [_Dim ·] \u001b[38;5;4;49m [_Measure Col] \u001b[38;5;5;49m [Customer Pag] \u001b[38;5;8;49m [Date ·] \u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;8;49m [Date_Month ·] \u001b[37;112H\u001b[38;5;0;48;5;8mDefau\u001b[37;118Ht\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.52534, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.627683, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.729242, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.83102, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.932257, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.033895, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.135888, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.237369, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.288716, "o", "\u001b[4;17H\u001b[1m\u001b[38;5;3;49m Cost\u001b[4m Revenue\u001b[6;17H\u001b[22m\u001b[24m\u001b[3m\u001b[38;5;15;48;5;237m 10,850\u001b[1m\u001b[38;5;0;48;5;6m 15,500\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.390112, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.491486, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.593008, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.693621, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.795131, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.896129, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.96272, "o", "\u001b[2;9H_Dril\u001b[2;15H ─\u001b[3;4H\u001b[38;5;5;49m_Measure\u001b[3;15HRevenue | Customer\u001b[3;34H= Stark Enterprises | Product = Gadgets | Region = East] \u001b[4;6H\u001b[1m\u001b[4m\u001b[38;5;3;49m Customer\u001b[24m Date Product Region Date_Month Value\u001b[6;2H\u001b[38;5;6;48;5;237m0 \u001b[38;5;0;48;5;6m Stark Enterprises\u001b[22m\u001b[38;5;15;48;5;237m 2025-01-31 Gadgets East 2025-01 7400\u001b[7;2H\u001b[39;49m1 Stark En\u001b[7;16Herprises 2025-03-25 Gadgets\u001b[7;46HEast\u001b[7;54H2025-03\u001b[7;63H8100\u001b[8;2H \u001b[8;12H \u001b[8;17H \u001b[9;2H \u001b[10;2H \u001b[36;10H\u001b[38;5;2;49m [_Index Row] \u001b[38;5;4;49m [_Dim Col] \u001b[38;5;5;49m [_Measure Pag] [Customer Pag] \u001b[38;5;8;49m [Date ·] \u001b[38;5;5;49m [Product Pag] [Region Pag] \u001b[38;5;8;49m▶\u001b[39;49m \u001b[37;112H\u001b[38;5;0;48;5;8m _Dri\u001b[37;118Hl\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.064326, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.165838, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.26736, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.369255, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.470736, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.540131, "o", "\u001b[4;6H\u001b[1m\u001b[38;5;3;49m Customer\u001b[4m Date\u001b[6;6H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m Stark Enterprises\u001b[1m\u001b[38;5;0;48;5;6m 2025-01-31\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.641991, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.743614, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.844468, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.894452, "o", "\u001b[4;24H\u001b[1m\u001b[38;5;3;49m Date\u001b[4m Product\u001b[6;24H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m 2025-01-31\u001b[1m\u001b[38;5;0;48;5;6m Gadgets\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.996018, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.097619, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.188952, "o", "\u001b[4;35H\u001b[1m\u001b[38;5;3;49m Product\u001b[4m Region\u001b[6;35H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m Gadgets\u001b[1m\u001b[38;5;0;48;5;6m East\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.290866, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.392364, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.483448, "o", "\u001b[4;43H\u001b[1m\u001b[38;5;3;49m Region\u001b[4m Date_Month\u001b[6;43H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m East\u001b[1m\u001b[38;5;0;48;5;6m 2025-01\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.585324, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.686841, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.788996, "o", "\u001b[4;50H\u001b[1m\u001b[38;5;3;49m Date_Month\u001b[4m Value\u001b[6;50H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m 2025-01\u001b[1m\u001b[38;5;0;48;5;6m 7400\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.890257, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.99175, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.093257, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.193931, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.294717, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.36863, "o", "\u001b[6;61H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m7400 \u001b[37;1H\u001b[24m\u001b[38;5;2;48;5;235medit: 7400▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.470497, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.571891, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.673583, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.775236, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.877092, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.978622, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.080177, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.182019, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.283573, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.385055, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.48658, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.587937, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.617837, "o", "\u001b[6;64H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m \u001b[37;10H\u001b[24m\u001b[38;5;2;48;5;235m▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.719109, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.820616, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.922015, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.023349, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.12483, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.141478, "o", "\u001b[6;63H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m \u001b[37;9H\u001b[24m\u001b[38;5;2;48;5;235m▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.243544, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.344981, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.445341, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.499384, "o", "\u001b[6;62H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m \u001b[37;8H\u001b[24m\u001b[38;5;2;48;5;235m▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.601098, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.702607, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.804164, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.905108, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.953026, "o", "\u001b[6;62H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m5\u001b[37;8H\u001b[24m\u001b[38;5;2;48;5;235m5▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.053947, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.118277, "o", "\u001b[6;63H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m0\u001b[37;9H\u001b[24m\u001b[38;5;2;48;5;235m0▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.218892, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.293284, "o", "\u001b[6;64H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m0\u001b[37;10H\u001b[24m\u001b[38;5;2;48;5;235m0▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.394821, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.496143, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.597957, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.69913, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.800668, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.834513, "o", "\u001b[6;2H0 Stark Enterprises 2025-01-31 Gadgets East 2025-01 7500 \u001b[7;2H\u001b[1m\u001b[38;5;6;48;5;237m1 \u001b[22m\u001b[38;5;15;48;5;237m Stark Enterprises 2025-03-25 Gadgets East 2025-03\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m \u001b[22m\u001b[24m\u001b[39;48;5;237m \u001b[37;7H\u001b[1m\u001b[38;5;2;48;5;235m▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.935864, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.037078, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.140586, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.241716, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.342374, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.44323, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.544218, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.645819, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.747314, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.848602, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.950053, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.051416, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.153119, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.158637, "o", "\u001b[7;61H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m<\u001b[37;7H\u001b[24m\u001b[38;5;2;48;5;235m<▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.260014, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.360707, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.462516, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.563782, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.665631, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.767546, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.869222, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.970857, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.072476, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.173862, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.275017, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.37639, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.477963, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.579372, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.681267, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.782127, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.791533, "o", "\u001b[7;61H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m \u001b[37;7H\u001b[24m\u001b[38;5;2;48;5;235m▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.893075, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.994708, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.09607, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.197242, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.297699, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.383851, "o", "\u001b[7;61H\u001b[1m\u001b[38;5;0;48;5;6m 8100\u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;8m NORMAL Drilled into cell: 2 rows _Drill \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.485658, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.587581, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.688828, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.790416, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.892205, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.993716, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.095342, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.196814, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.235231, "o", "\u001b[2;9HDefau\u001b[2;15Ht \u001b[3;4H\u001b[38;5;5;49mCustomer\u001b[3;15HStark Enterprises]\u001b[3;34H\u001b[39;49m \u001b[4;6H \u001b[1m\u001b[38;5;3;49m Cost\u001b[4m Revenue\u001b[24m Profit\u001b[22m\u001b[39;49m \u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets East \u001b[22m\u001b[3m\u001b[38;5;15;48;5;237m 10,850\u001b[1m\u001b[38;5;0;48;5;6m 15,600\u001b[22m\u001b[38;5;15;48;5;237m 4,750\u001b[23m\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49mWidgets East \u001b[3m 14,580 24,300 9,720\u001b[23m \u001b[8;2HSprockets\u001b[8;12HEast\u001b[8;17H\u001b[3m 2,940 4,200 1,260\u001b[9;2H\u001b[23m\u001b[38;5;8;49m─────────────────────────────────────────────────────────────────────────────────────────────────────────"]
[31.23528, "o", "─────────────\u001b[10;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 44,100 15,730\u001b[36;10H\u001b[22m\u001b[38;5;8;49m [_Index ·] [_Dim ·] \u001b[38;5;4;49m [_Measure Col] \u001b[38;5;5;49m [Customer Pag] \u001b[38;5;8;49m [Date ·] \u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;8;49m [Date_Month ·] \u001b[37;112H\u001b[38;5;0;48;5;8mDefau\u001b[37;118Ht\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.33715, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.438167, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.539656, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.641338, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.742963, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.844469, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.945935, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.04757, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.148996, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.250673, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.352507, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.454665, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.556091, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.611975, "o", "\u001b[6;2HGadgets East \u001b[3m 10,850 15,600 4,750\u001b[23m \u001b[7;2H\u001b[1m\u001b[38;5;6;48;5;237mWidgets East \u001b[22m\u001b[3m\u001b[38;5;15;48;5;237m 14,580\u001b[1m\u001b[38;5;0;48;5;6m 24,300\u001b[22m\u001b[38;5;15;48;5;237m 9,720\u001b[23m\u001b[39;48;5;237m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.714152, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.816243, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.91789, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.02012, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.121571, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.223001, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.324736, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.426455, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.527682, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.629295, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.730933, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.832574, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.934164, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.009081, "o", "\u001b[7;2HWidgets East \u001b[3m 14,580 24,300 9,720\u001b[23m \u001b[8;2H\u001b[1m\u001b[38;5;6;48;5;237mSprockets East \u001b[22m\u001b[3m\u001b[38;5;15;48;5;237m 2,940\u001b[1m\u001b[38;5;0;48;5;6m 4,200\u001b[22m\u001b[38;5;15;48;5;237m 1,260\u001b[23m\u001b[39;48;5;237m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.111055, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.21937, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.321025, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.422024, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.523707, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.625411, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.726664, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.828292, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.929129, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.030341, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.13176, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.23305, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.334358, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.435652, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.518974, "o", "\u001b[2;9H_Dril\u001b[2;15H ─\u001b[3;4H\u001b[38;5;5;49m_Measure\u001b[3;15HRevenue | Customer\u001b[3;34H= Stark Enterprises | Product = Sprockets | Region = East] \u001b[4;6H\u001b[1m\u001b[4m\u001b[38;5;3;49m Customer\u001b[24m Date Product Region Date_Month Value\u001b[6;2H\u001b[38;5;6;48;5;237m0 \u001b[38;5;0;48;5;6m Stark Enterprises\u001b[22m\u001b[38;5;15;48;5;237m 2025-02-20 Sprockets East 2025-02 4200\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49m \u001b[7;12H \u001b[7;17H \u001b[8;2H \u001b[9;2H \u001b[10;2H \u001b[36;10H\u001b[38;5;2;49m [_Index Row] \u001b[38;5;4;49m [_Dim Col] \u001b[38;5;5;49m [_Measure Pag] [Customer Pag] \u001b[38;5;8;49m [Date ·] \u001b[38;5;5;49m [Product Pag] [Region Pag] \u001b[38;5;8;49m▶\u001b[39;49m \u001b[37;30H\u001b[38;5;0;48;5;8m1\u001b[37;112H _Dri\u001b[3"]
[35.519, "o", "7;118Hl\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.620559, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.722169, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.823642, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.924946, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.02612, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.127752, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.228896, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.330381, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.432324, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.534209, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.635534, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.726876, "o", "\u001b[4;6H\u001b[1m\u001b[38;5;3;49m Customer\u001b[4m Date\u001b[6;6H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m Stark Enterprises\u001b[1m\u001b[38;5;0;48;5;6m 2025-02-20\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.828227, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.9292, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.02562, "o", "\u001b[4;24H\u001b[1m\u001b[38;5;3;49m Date\u001b[4m Product\u001b[6;24H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m 2025-02-20\u001b[1m\u001b[38;5;0;48;5;6m Sprockets\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.126867, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.227776, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.243248, "o", "\u001b[4;35H\u001b[1m\u001b[38;5;3;49m Product\u001b[4m Region\u001b[6;35H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m Sprockets\u001b[1m\u001b[38;5;0;48;5;6m East\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.34447, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.437218, "o", "\u001b[4;45H\u001b[1m\u001b[38;5;3;49m Region\u001b[4m Date_Month\u001b[6;45H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m East\u001b[1m\u001b[38;5;0;48;5;6m 2025-02\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.538942, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.640498, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.717318, "o", "\u001b[4;52H\u001b[1m\u001b[38;5;3;49m Date_Month\u001b[4m Value\u001b[6;52H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m 2025-02\u001b[1m\u001b[38;5;0;48;5;6m 4200\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.818793, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.920394, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.021784, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.122547, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.22418, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.325665, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.427127, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.528763, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.630238, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.685239, "o", "\u001b[6;63H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m4200 \u001b[37;1H\u001b[24m\u001b[38;5;2;48;5;235medit: 4200▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.786586, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.888352, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.989767, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.091211, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.19311, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.294318, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.395029, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.49683, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.59827, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.699745, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.801287, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.902386, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.003913, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.105799, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.206374, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.307735, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.409433, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.510898, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.612475, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.713387, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.815573, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.824365, "o", "\u001b[6;63H\u001b[1m\u001b[38;5;0;48;5;6m 4200\u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;8m NORMAL Added new record row _Drill \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.926091, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.027453, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.129047, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.229876, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.33153, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.4335, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.535434, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.637062, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.738676, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.840484, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.943155, "o", "\u001b[4;52H\u001b[1m\u001b[4m\u001b[38;5;3;49m Date_Month\u001b[24m Value\u001b[6;52H\u001b[38;5;0;48;5;6m 2025-02\u001b[22m\u001b[38;5;15;48;5;237m 4200\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.04455, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.147113, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.248907, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.350271, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.452008, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.454049, "o", "\u001b[4;45H\u001b[1m\u001b[4m\u001b[38;5;3;49m Region\u001b[24m Date_Month\u001b[6;45H\u001b[38;5;0;48;5;6m East\u001b[22m\u001b[38;5;15;48;5;237m 2025-02\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.555938, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.656672, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.694792, "o", "\u001b[4;35H\u001b[1m\u001b[4m\u001b[38;5;3;49m Product\u001b[24m Region\u001b[6;35H\u001b[38;5;0;48;5;6m Sprockets\u001b[22m\u001b[38;5;15;48;5;237m East\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.796342, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.89699, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.962391, "o", "\u001b[4;24H\u001b[1m\u001b[4m\u001b[38;5;3;49m Date\u001b[24m Product\u001b[6;24H\u001b[38;5;0;48;5;6m 2025-02-20\u001b[22m\u001b[38;5;15;48;5;237m Sprockets\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.06307, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.164443, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.207856, "o", "\u001b[4;6H\u001b[1m\u001b[4m\u001b[38;5;3;49m Customer\u001b[24m Date\u001b[6;6H\u001b[38;5;0;48;5;6m Stark Enterprises\u001b[22m\u001b[38;5;15;48;5;237m 2025-02-20\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.308944, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.4106, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.512276, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.613521, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.715215, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.734956, "o", "\u001b[6;6H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6mStark Enterprises \u001b[37;1H\u001b[24m\u001b[38;5;2;48;5;235medit: Stark Enterprises▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.836886, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.938724, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.040338, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.142082, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.243479, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.345158, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.446823, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.548419, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.650021, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.751645, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.853167, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.954401, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.056002, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.157732, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.258617, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.306905, "o", "\u001b[6;6H\u001b[1m\u001b[38;5;0;48;5;6m Stark Enterprises\u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;8m NORMAL Added new record row _Drill \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.40892, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.510835, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.612239, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.71417, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.816108, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.917769, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.019473, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.120249, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.221745, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.32364, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.425537, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.528656, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.630009, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.730899, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.831981, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.933554, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.035044, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.136541, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.238468, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.340173, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.442019, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.54362, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.617582, "o", "\u001b[4;6H\u001b[1m\u001b[38;5;3;49m Customer\u001b[4m Date\u001b[6;6H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m Stark Enterprises\u001b[1m\u001b[38;5;0;48;5;6m 2025-02-20\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.719176, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.819708, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.901714, "o", "\u001b[4;24H\u001b[1m\u001b[38;5;3;49m Date\u001b[4m Product\u001b[6;24H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m 2025-02-20\u001b[1m\u001b[38;5;0;48;5;6m Sprockets\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.003506, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.106764, "o", "\u001b[4;35H\u001b[1m\u001b[38;5;3;49m Product\u001b[4m Region\u001b[6;35H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m Sprockets\u001b[1m\u001b[38;5;0;48;5;6m East\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.208137, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.309835, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.344493, "o", "\u001b[4;45H\u001b[1m\u001b[38;5;3;49m Region\u001b[4m Date_Month\u001b[6;45H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m East\u001b[1m\u001b[38;5;0;48;5;6m 2025-02\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.445881, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.547204, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.582215, "o", "\u001b[4;52H\u001b[1m\u001b[38;5;3;49m Date_Month\u001b[4m Value\u001b[6;52H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m 2025-02\u001b[1m\u001b[38;5;0;48;5;6m 4200\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.683651, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.785171, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.88631, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.987214, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.088752, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.19032, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.2922, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.393169, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.494592, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.551364, "o", "\u001b[6;63H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m4200 \u001b[37;1H\u001b[24m\u001b[38;5;2;48;5;235medit: 4200▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.653106, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.75426, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.855143, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.956612, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.057339, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.158822, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.260463, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.362035, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.463667, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.565019, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.658576, "o", "\u001b[6;66H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m \u001b[37;10H\u001b[24m\u001b[38;5;2;48;5;235m▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.75945, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.861196, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.962228, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.06407, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.088362, "o", "\u001b[6;65H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m \u001b[37;9H\u001b[24m\u001b[38;5;2;48;5;235m▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.189174, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.290284, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.392056, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.49337, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.595101, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.659247, "o", "\u001b[6;64H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m \u001b[37;8H\u001b[24m\u001b[38;5;2;48;5;235m▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.760985, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.862189, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.96398, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.977937, "o", "\u001b[6;64H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m5\u001b[37;8H\u001b[24m\u001b[38;5;2;48;5;235m5▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.079343, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.146926, "o", "\u001b[6;65H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m0\u001b[37;9H\u001b[24m\u001b[38;5;2;48;5;235m0▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.248174, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.315613, "o", "\u001b[6;66H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m0\u001b[37;10H\u001b[24m\u001b[38;5;2;48;5;235m0▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.417369, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.518587, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.620531, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.639076, "o", "\u001b[6;63H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m \u001b[37;7H\u001b[24m\u001b[38;5;2;48;5;235m▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.740677, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.841462, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.9429, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.044326, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.145842, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.247478, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.347982, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.449704, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.551334, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.653012, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.754874, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.855501, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.957138, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.057576, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.159355, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.26075, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.361942, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.4634, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.564497, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.666245, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.7679, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.868507, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.969901, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.071612, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.172648, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.200869, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.302398, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.403782, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.505197, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.606417, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.708289, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.809292, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.910682, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.011963, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.113483, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.214542, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.31553, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.377483, "o", "\u001b[6;63H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m4\u001b[37;7H\u001b[24m\u001b[38;5;2;48;5;235m4▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.479033, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.580766, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.63521, "o", "\u001b[6;64H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m5\u001b[37;8H\u001b[24m\u001b[38;5;2;48;5;235m5▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.736712, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.80774, "o", "\u001b[6;65H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m0\u001b[37;9H\u001b[24m\u001b[38;5;2;48;5;235m0▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.909121, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.000933, "o", "\u001b[6;66H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m0\u001b[37;10H\u001b[24m\u001b[38;5;2;48;5;235m0▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.10274, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.203875, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.305319, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.406083, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.471834, "o", "\u001b[6;63H\u001b[1m\u001b[4m\u001b[38;5;2;48;5;6m \u001b[37;7H\u001b[24m\u001b[38;5;2;48;5;235m▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.57245, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.673931, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.775416, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.876873, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.978402, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.079364, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.181199, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.28257, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.323021, "o", "\u001b[6;63H\u001b[1m\u001b[38;5;0;48;5;6m 4500\u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;8m NORMAL Added new record row _Drill \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.425018, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.526725, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.62833, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.729186, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.830402, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.931931, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.033936, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.135577, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.236664, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.33801, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.363396, "o", "\u001b[2;9HDefau\u001b[2;15Ht \u001b[3;4H\u001b[38;5;5;49mCustomer\u001b[3;15HStark Enterprises]\u001b[3;34H\u001b[39;49m \u001b[4;6H \u001b[1m\u001b[38;5;3;49m Cost\u001b[4m Revenue\u001b[24m Profit\u001b[22m\u001b[39;49m \u001b[6;2HGadgets East \u001b[3m 10,850 15,600 4,750\u001b[23m \u001b[7;2HWidgets\u001b[7;12HEast\u001b[7;17H\u001b[3m 14,580 24,300 9,720\u001b[8;2H\u001b[23m\u001b[1m\u001b[38;5;6;48;5;237mSprockets East \u001b[22m\u001b[3m\u001b[38;5;15;48;5;237m 2,940\u001b[1m\u001b[38;5;0;48;5;6m 4,500\u001b[22m\u001b[38;5;15;48;5;237m 1,560\u001b[23m\u001b[39;48;5;237m \u001b[9;2H\u001b[38;5;8;49m───────────────────────────────────────────────────────────────────────────────────────────────────────────"]
[59.363433, "o", "───────────\u001b[10;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 44,400 16,030\u001b[36;10H\u001b[22m\u001b[38;5;8;49m [_Index ·] [_Dim ·] \u001b[38;5;4;49m [_Measure Col] \u001b[38;5;5;49m [Customer Pag] \u001b[38;5;8;49m [Date ·] \u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;8;49m [Date_Month ·] \u001b[37;112H\u001b[38;5;0;48;5;8mDefau\u001b[37;118Ht\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.46552, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.567009, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.668443, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.770756, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.872967, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.974045, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.075696, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.177744, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.279764, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.380471, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.482824, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.584652, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.686336, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.787808, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.888707, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.990743, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.092529, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.193873, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.295612, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.397566, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.498529, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.600461, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.70169, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.803645, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.905272, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.007052, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.108297, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.209571, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.311535, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.413278, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.514977, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.616556, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.718025, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.819687, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.920927, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.022617, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.039683, "o", "\u001b[37;1H\u001b[1m\u001b[38;5;3;48;5;235m:▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.140302, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.242332, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.344261, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.352965, "o", "\u001b[37;2H\u001b[1m\u001b[38;5;3;48;5;235mq▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.454163, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.55584, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.656611, "o", "\u001b[37;3H\u001b[1m\u001b[38;5;3;48;5;235m!▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.758111, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.859338, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.955664, "o", "\u001b[?1049l"]
[63.955716, "o", "\u001b[?25h"]
[63.957068, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"]
[63.999517, "o", "\u001b]0;~/git_repos/git.fiddlerwoaroof.com/u/edwlan/improvise\u0007"]
[64.14143, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J---\r\n(0) Mac:edwlan--s059 ~gf/u/edwlan/improvise \u001b[30m\u001b[35m\u001b[39mgit\u001b[35m\u001b[33m->\u001b[35m\u001b[32mmain\u001b[35m\u001b[39m\u001b[00m 2026-04-09 15:06:33\r\n16026:% \u001b[K"]
[64.141514, "o", "\u001b[?2004h"]
[64.491198, "o", "e"]
[64.739691, "o", "\bex"]
[64.9105, "o", "i"]
[65.087513, "o", "t"]
[65.267946, "o", "\u001b[?2004l\r\r\n"]
[65.269911, "o", "\u001b]0;exit\u0007\u001b[2 q"]
{"version": 2, "width": 120, "height": 37, "idle_time_limit": 2.0, "title": "improvise \u2014 drill"}
[0.004285, "o", "\u001b[?1049h"]
[0.005635, "o", "\u001b[1;1H\u001b[1m\u001b[38;5;0;48;5;4m improvise \u00b7 Acme Sales Demo (demo.improv) ?:help :q quit \u001b[2;1H\u001b[22m\u001b[39;49m\u250c\u001b[2;3HView:\u001b[2;9HDefault\u001b[2;17H\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u001b[38;5;8;49m\u250c Categories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u001b[3;1H\u001b[39;49m\u2502\u001b[38;5;5;49m [Customer = Stark Enterprises] \u001b[3;88H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[3;92H_Index\u001b[3;99H(0)\u001b[3;104H\u001b[38;5;8;49m [None \u2205]\u001b[3;120H\u2502\u001b[4;1H\u001b[39;49m\u2502\u001b[4;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost \u001b[24m Revenue Profit \u001b[4;88H\u001b[22m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[4;92H_Dim\u001b[4;97H(0)\u001b[4;102H\u001b[38;5;8;49m [None \u2205]\u001b[4;120H\u2502\u001b[5;1H\u001b[39;49m\u2502\u001b[5;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-02 2025-03 2025-01 2025-02 2025-03 2025-01 2025-02\u001b[5;88H\u001b[22m\u001b[39"]
[0.005646, "o", ";49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[5;92H_Measure\u001b[5;101H(3)\u001b[5;106H\u001b[38;5;4;49m [Col \u2194]\u001b[5;120H\u001b[38;5;8;49m\u2502\u001b[6;1H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[6;92HCustomer\u001b[6;101H(9)\u001b[6;106H\u001b[38;5;5;49m [Page \u2630\u001b[6;115H]\u001b[6;120H\u001b[38;5;8;49m\u2502\u001b[7;1H\u001b[39;49m\u2502\u001b[1m\u001b[38;5;6;48;5;237mGadgets North \u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[23m\u001b[39;48;5;237m \u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[7;92HDate\u001b[7;97H(40)\u001b[7;103H\u001b[38;5;8;49m [None \u2205]\u001b[7;120H\u2502\u001b[8;1H\u001b[39;49m\u2502\u001b[8;12HEast\u001b[8;18H\u001b[3m 5,180\u001b[38;5;8;49m \u001b[39;49m 5,670 7,400\u001b[38;5;8;49m \u001b[39;49m 8,100 2,220\u001b[38;5;8;49m \u001b[8;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[8;92HP"]
[0.005672, "o", "roduct\u001b[8;100H(3)\u001b[8;105H\u001b[38;5;2;49m [Row \u2195]\u001b[8;120H\u001b[38;5;8;49m\u2502\u001b[9;1H\u001b[39;49m\u2502\u001b[9;12HSouth\u001b[9;18H\u001b[3m\u001b[38;5;8;49m \u001b[9;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[9;92HRegion\u001b[9;99H(4)\u001b[9;104H\u001b[38;5;2;49m [Row \u2195]\u001b[9;120H\u001b[38;5;8;49m\u2502\u001b[10;1H\u001b[39;49m\u2502\u001b[10;12HWest\u001b[10;18H\u001b[3m\u001b[38;5;8;49m \u001b[10;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[10;92HDate_Month\u001b[10;103H(3)\u001b[10;108H\u001b[38;5;4;49m [Col \u2194]\u001b[10;120H\u001b[38;5;8;49m\u2502\u001b[11;1H\u001b[39;49m\u2502Widgets\u001b[11;12HNorth\u001b[11;18H\u001b[3m\u001b[38;5;8;49m \u001b[11;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[11;120H\u2502\u001b[12;1H\u001b[39;49m\u2502\u001b[12;12HEast\u001b[12;18H\u001b[3m 7,080\u001b[38;5;8;49m \u001b[39;49m 7,500 11,800\u001b[38;5;8;49m \u001b[39;49m 12,500 4,720\u001b[38;5;8;49m \u001b[12;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[12;120H\u2502\u001b[13;1H\u001b[39;49m\u2502\u001b[13;12HSouth\u001b[13;18H\u001b[3m\u001b[38;5;8;49m "]
[0.005686, "o", " \u001b[13;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[13;120H\u2502\u001b[14;1H\u001b[39;49m\u2502\u001b[14;12HWest\u001b[14;18H\u001b[3m\u001b[38;5;8;49m \u001b[14;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[14;120H\u2502\u001b[15;1H\u001b[39;49m\u2502Sprockets\u001b[15;12HNorth\u001b[15;18H\u001b[3m\u001b[38;5;8;49m \u001b[15;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[15;120H\u2502\u001b[16;1H\u001b[39;49m\u2502\u001b[16;12HEast\u001b[16;18H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 2,940\u001b[38;5;8;49m \u001b[39;49m 4,200\u001b[38;5;8;49m \u001b[39;49m 1,260\u001b[16;88H\u001b[23m\u2502\u001b[38;5;8;49m\u2502\u001b[16;120H\u2502\u001b[17;1H\u001b[39;49m\u2502\u001b[17;12HSouth\u001b[17;18H\u001b[3m\u001b[38;5;8;49m \u001b[17;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[17;120H\u2502\u001b[18;1H\u001b[39;49m\u2502\u001b[18;12HWest\u001b[18;18H\u001b[3m\u001b[38;5;8;49m \u001b[18;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[18;120H\u2502\u001b[19;1H\u001b[39;49m\u2502\u001b[38;5;8;49m"]
[0.005705, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[19;120H\u2502\u001b[20;1H\u001b[39;49m\u2502\u001b[1m\u001b[38;5;3;49mTotal 12,260 2,940 13,170 19,200 4,200 20,600 6,940 1,260\u001b[20;88H\u001b[22m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[20;120H\u2502\u001b[21;1H\u001b[39;49m\u2502\u001b[21;88H\u2502\u001b[38;5;8;49m\u2502\u001b[21;120H\u2502\u001b[22;1H\u001b[39;49m\u2502\u001b[22;88H\u2502\u001b[38;5;8;49m\u2502\u001b[22;120H\u2502\u001b[23;1H\u001b[39;49m\u2502\u001b[23;88H\u2502\u001b[38;5;8;49m\u2502\u001b[23;120H\u2502\u001b[24;1H\u001b[39;49m\u2502\u001b[24;88H\u2502\u001b[38;5;8;49m\u2502\u001b[24;120H\u2502\u001b[25;1H\u001b[39;49m\u2502\u001b[25;88H\u2502\u001b[38;5;8;49m\u2502\u001b[25;120H\u2502\u001b[26;1H\u001b[39;49m\u2502\u001b[26;88H\u2502\u001b[38;5;8;49m\u2502\u001b[26;120H\u2502\u001b[27;1H\u001b[39;49m\u2502\u001b[27;88H\u2502\u001b[38;5;8;49m\u2502\u001b[27;120H\u2502\u001b[28;1H\u001b[39;49m\u2502\u001b[28;88H\u2502\u001b[38;5;8;49m\u2502\u001b[28;120H\u2502\u001b[29;1H\u001b[39;49m\u2502\u001b[29;88H\u2502\u001b[38;5;8;49m\u2502\u001b[29;120H\u2502\u001b[30;1H\u001b[39;49m\u2502\u001b[30;88H\u2502\u001b[38;5;8;49m\u2502\u001b[30;120H\u2502\u001b["]
[0.005729, "o", "31;1H\u001b[39;49m\u2502\u001b[31;88H\u2502\u001b[38;5;8;49m\u2502\u001b[31;120H\u2502\u001b[32;1H\u001b[39;49m\u2502\u001b[32;88H\u2502\u001b[38;5;8;49m\u2502\u001b[32;120H\u2502\u001b[33;1H\u001b[39;49m\u2502\u001b[33;88H\u2502\u001b[38;5;8;49m\u2502\u001b[33;120H\u2502\u001b[34;1H\u001b[39;49m\u2502\u001b[34;88H\u2502\u001b[38;5;8;49m\u2502\u001b[34;120H\u2502\u001b[35;1H\u001b[39;49m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[38;5;8;49m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[36;1H\u001b[38;5;7;49m Tiles: \u001b[36;10H\u001b[38;5;8;49m [_Index \u00b7] [_Dim \u00b7] \u001b[38;5;4;49m [_Measure Col] \u001b[38;5;5;49m [Customer Pag] \u001b[38;5;8;49m [Date \u00b7] \u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;4;49m [Date_Month Col] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL hjkl\u2190\u2191\u2192\u2193:nav ai:edit R:records P:prune F:formulas C:categories V:views T:tiles [:prev-page ]:next-pag\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.108486, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.211251, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.314095, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.416843, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.519561, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.621198, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.723934, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.82714, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.929945, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.032686, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.135402, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.238119, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.340849, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.443552, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.546299, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.608193, "o", "\u001b[36;10H\u001b[1m\u001b[38;5;0;48;5;6m [_Index \u00b7] \u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;5m TILES hl\u2190\u2192:select Enter/Space:cycle r:row c:col p:page Esc/Tab:back Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.710971, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.813682, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.916404, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.019111, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.12183, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.224531, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.327257, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.430035, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.532783, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.614933, "o", "\u001b[36;10H\u001b[38;5;8;49m [_Index \u00b7] \u001b[1m\u001b[38;5;0;48;5;6m [_Dim \u00b7] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.717672, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.820416, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.923164, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.974926, "o", "\u001b[36;22H\u001b[38;5;8;49m [_Dim \u00b7] \u001b[1m\u001b[38;5;0;48;5;6m [_Measure Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.077689, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.180422, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.283161, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.329807, "o", "\u001b[36;32H\u001b[38;5;4;49m [_Measure Col] \u001b[1m\u001b[38;5;0;48;5;6m [Customer Pag] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.432537, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.534125, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.636837, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.6862, "o", "\u001b[1;47H\u001b[1m\u001b[38;5;0;48;5;4m[+]\u001b[3;2H\u001b[22m\u001b[39;49m \u001b[1m\u001b[4m\u001b[38;5;3;49m Cost \u001b[24m Revenue Profit \u001b[4;19H\u001b[4m2025-01\u001b[24m 2025-02 2025-03\u001b[4;43H2025-01\u001b[4;51H2025-02\u001b[4;59H2025-03\u001b[4;67H2025-01\u001b[4;75H2025-02\u001b[5;2H\u001b[22m\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets North \u001b[3m\u001b[38;5;0;48;5;6m 7,210\u001b[22m\u001b[38;5;15;48;5;237m 5,110 4,060 10,300 7,300 5,800 3,090 2,190\u001b[23m\u001b[39;48;5;237m \u001b[6;106H\u001b[38;5;8;49m [None \u2205]\u001b[39;49m \u001b[7;2H East \u001b[3m 11,410 6,440 5,670 16,300 9,200 8,100 4,890 2,760\u001b[23m \u001b[8;12HSou\u001b[8;16Hh\u001b[8;21H\u001b[3m3\u001b[8;23H85\u001b[8;26H 4,270\u001b[8;37H4\u001b[8;39H76\u001b[8;45H5\u001b[8;47H5\u001b[8;50H 6,100\u001b[8;61H6\u001b[8;63H8\u001b[8;69H1\u001b[8;71H65\u001b[8;74H 1,830\u001b[9;12H\u001b[23m"]
[3.68622, "o", "Wes\u001b[9;16H \u001b[9;18H\u001b[3m 4,690 4,970 5,250 6,700 7,100 7,500 2,010 2,130\u001b[10;2H\u001b[23mWidgets\u001b[10;12HNor\u001b[10;16Hh\u001b[10;18H\u001b[3m 12,300 6,600 8,100 20,500 11,000 13,500 8,200 4,400\u001b[11;2H\u001b[23m \u001b[11;12HEas\u001b[11;16H \u001b[11;18H\u001b[3m 15,600 9,000 7,500 26,000 15,000 12,500 10,400 6,000\u001b[12;12H\u001b[23mSou\u001b[12;16Hh\u001b[12;20H\u001b[3m10\u001b[12;23H20\u001b[12;26H 6,120\u001b[12;37H5\u001b[12;39H34\u001b[12;45H7\u001b[12;47H0\u001b[12;50H 10,200\u001b[12;60H 8\u001b[12;63H9\u001b[12;69H6\u001b[12;71H80\u001b[12;74H 4,080\u001b[13;12H\u001b[23mWes\u001b[13;16H \u001b[13;18H\u001b[3m 6,300 6,720 13,560 10,500 11,200 22,600 4,200 4,480\u001b[14;2H\u001b[23mSprockets\u001b[14;12HNor\u001b[14;16Hh\u001b[14;18H\u001b[3m 2,380 2,030\u001b[14;42H 3,400 2,900\u001b[14;66H 1,020 870\u001b[15;2H\u001b[23m \u001b[15;12HEas\u001b[15;16H \u001b[15;26H\u001b[3m 2,940 3,360\u001b[15;50H 4,200 4,800\u001b[15;74H 1,260\u001b[16;12H\u001b[23mSou\u001b[16;16Hh\u001b[16;18H\u001b[3m 1,960\u001b[16;31H17\u001b[16;34H 2,450 2,800\u001b[16;53H3\u001b[16;55H1\u001b[16;58H 3,500 840\u001b[16;77H 93\u001b[17;12H\u001b[23mWes\u001b[17;16H \u001b[17;18H\u001b[3m 2,240 2,520 2,800 3,200 3,60"]
[3.686263, "o", "0 4,000 960 1,080\u001b[18;2H\u001b[23m\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[19;2H\u001b[1m\u001b[38;5;3;49mTotal 78,140 58,890 62,850 122,200 90,900 98,000 44,060 32,010\u001b[22m\u001b[39;49m \u001b[20;2H \u001b[36;59H\u001b[1m\u001b[38;5;0;48;5;6m\u00b7] \u001b[22m\u001b[38;5;8;49m [Date \u00b7] \u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;4;49m [Date_Month Col] \u001b[39;49m \u001b[37;10H\u001b[38;5;0;48;5;5mCustom\u001b[37;17Hr \u2192 None \u001b[37;42H \u001b[37;49H \u001b[37;56H \u001b[37;64H \u001b[37;108H Default\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.789137, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.891999, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.994907, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.097786, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.198656, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.301502, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.404386, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.507282, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.610223, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.712281, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.815179, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.918075, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.020969, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.123902, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.226792, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.329688, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.432581, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.494585, "o", "\u001b[36;48H\u001b[38;5;8;49m [Customer \u00b7] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL Customer \u2192 None Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.5975, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.700438, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.803306, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.906223, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.009094, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.112021, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.214122, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.315364, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.418259, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.521129, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.622197, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.699834, "o", "\u001b[6;2HGadgets North \u001b[3m 7,210 5,110 4,060 10,300 7,300 5,800 3,090 2,190\u001b[23m \u001b[7;2H\u001b[1m\u001b[38;5;6;48;5;237m East \u001b[3m\u001b[38;5;0;48;5;6m 11,410\u001b[22m\u001b[38;5;15;48;5;237m 6,440 5,670 16,300 9,200 8,100 4,890 2,760\u001b[23m\u001b[39;48;5;237m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.802772, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.904516, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.007571, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.110627, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.213753, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.316634, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.419878, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.521306, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.625285, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.710332, "o", "\u001b[2;9H_Dril\u001b[2;15H \u2500\u001b[3;2H\u001b[38;5;5;49m [_Measure = Cost | Product = Gadgets | Region = East | Date_Month = 2025-01] \u001b[39;49m \u001b[3;104H\u001b[38;5;2;49m [Row \u2195]\u001b[39;49m \u001b[4;6H\u001b[1m\u001b[4m\u001b[38;5;3;49m Customer\u001b[24m \u001b[4;27H Date Product Region\u001b[4;51HDate_Month +cat\u001b[4;67HCost Revenue +\u001b[4;102H\u001b[22m\u001b[38;5;4;49m [Col \u2194]\u001b[39;49m \u001b[5;106H\u001b[38;5;3;49m [Fix \u26b2]\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237m0 \u001b[38;5;0;48;5;6m Stark Enterprises\u001b[22m\u001b[38;5;15;48;5;237m 2025-01-31 Gadgets East 2025-01\u001b[38;5;8;48;5;237m \u001b[38;5;15;48;5;237m 5180\u001b[38;5;8;48;5;237m \u001b[39;48;5;237m \u001b[7;2H\u001b[39;49m1 Wonka Industries 2025-01-23 Gadgets East 2025-01\u001b[38;5;8;49m \u001b[39;49m 6230\u001b[38;5;8;49m \u001b[39;49m \u001b[8;12H \u001b[8;18H \u001b[8;105H\u001b[38;5;3;49m [Fix \u26b2]\u001b[9;12H\u001b[39;49m \u001b[9;18H \u001b[9;104H\u001b[38;5;3;49m [Fix \u26b2]\u001b[10;2H\u001b[39;49m \u001b[10;12H \u001b[10;18H "]
[7.710352, "o", " \u001b[10;108H\u001b[38;5;3;49m [Fix \u26b2]\u001b[11;12H\u001b[39;49m \u001b[11;18H \u001b[12;12H \u001b[12;18H \u001b[13;12H \u001b[13;18H \u001b[14;2H \u001b[14;12H \u001b[14;18H \u001b[15;12H \u001b[15;18H \u001b[16;12H \u001b[16;18H \u001b[17;12H \u001b[17;18H \u001b[18;2H \u001b[19;2H \u001b[36;10H\u001b[38;5;2;49m [_Index Row] \u001b[38;5;4;49m [_Dim Col] \u001b[38;5;8;49m [Customer \u00b7] [Date \u00b7] Ctrl+\u2191\u2193\u2190\u2192 to move tiles\u001b[39;49m "]
[7.710465, "o", " \u001b[37;11H\u001b[38;5;0;48;5;8mDrilled into c\u001b[37;26Hll:\u001b[37;30H2\u001b[37;32Hrows\u001b[37;110H _Drill\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.812945, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.915072, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.017259, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.119553, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.221744, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.32391, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.426082, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.528236, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.630426, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.732596, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.834774, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.936958, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.039128, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.141296, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.243466, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.345629, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.447771, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.549931, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.652084, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.754228, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.855757, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.917157, "o", "\u001b[4;6H\u001b[1m\u001b[38;5;3;49m Customer\u001b[4m Date\u001b[6;6H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m Stark Enterprises\u001b[1m\u001b[38;5;0;48;5;6m 2025-01-31\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.01932, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.120658, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.221855, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.324001, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.371232, "o", "\u001b[4;24H\u001b[1m\u001b[38;5;3;49m Date\u001b[4m Product\u001b[6;24H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m 2025-01-31\u001b[1m\u001b[38;5;0;48;5;6m Gadgets\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.473507, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.576085, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.676895, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.779318, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.827297, "o", "\u001b[4;35H\u001b[1m\u001b[38;5;3;49m Product\u001b[4m Region\u001b[6;35H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m Gadgets\u001b[1m\u001b[38;5;0;48;5;6m East\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.929457, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.031648, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.133996, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.23653, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.284843, "o", "\u001b[4;43H\u001b[1m\u001b[38;5;3;49m Region\u001b[4m Date_Month\u001b[6;43H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m East\u001b[1m\u001b[38;5;0;48;5;6m 2025-01\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.386221, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.488609, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.591232, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.693773, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.744869, "o", "\u001b[4;50H\u001b[1m\u001b[38;5;3;49m Date_Month\u001b[4m +cat\u001b[6;50H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m 2025-01\u001b[1m\u001b[38;5;0;48;5;6m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.847416, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.950034, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.052515, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.154354, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.201487, "o", "\u001b[4;61H\u001b[1m\u001b[38;5;3;49m +cat\u001b[4m Cost\u001b[6;61H\u001b[22m\u001b[24m\u001b[38;5;8;48;5;237m \u001b[1m\u001b[38;5;0;48;5;6m 5180\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.303477, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.406072, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.508279, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.608689, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.710832, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.813003, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.913226, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.013574, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.115748, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.217931, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.320089, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.421722, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.523898, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.626086, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.728251, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.830429, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.932603, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.034762, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.065883, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.167595, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.269758, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.371928, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.474085, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.574606, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.67679, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.778963, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.881118, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.98327, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.084772, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.185296, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.287456, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.389618, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.491797, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.593978, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.696247, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.798955, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.901301, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.002321, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
+149 -825
View File
@@ -1,825 +1,149 @@
{"version": 2, "width": 120, "height": 37, "timestamp": 1775772426, "idle_time_limit": 2.0, "env": {"SHELL": "/bin/zsh", "TERM": "screen-256color"}}
[0.22204, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"]
[0.229576, "o", "\u001b]0;~/git_repos/git.fiddlerwoaroof.com/u/edwlan/improvise\u0007"]
[0.348862, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J---\r\n(0) Mac:edwlan--s059 ~gf/u/edwlan/improvise \u001b[30m\u001b[35m\u001b[39mgit\u001b[35m\u001b[33m->\u001b[35m\u001b[32mmain\u001b[35m\u001b[39m\u001b[00m 2026-04-09 15:07:06\r\n16026:% \u001b[K"]
[0.348973, "o", "\u001b[?2004h"]
[4.839346, "o", "\u001b[3m./target/release/improvise examples/demo.improv\u001b[23m"]
[5.712315, "o", "\u001b[47D\u001b[23m.\u001b[23m/\u001b[23mt\u001b[23ma\u001b[23mr\u001b[23mg\u001b[23me\u001b[23mt\u001b[23m/\u001b[23mr\u001b[23me\u001b[23ml\u001b[23me\u001b[23ma\u001b[23ms\u001b[23me\u001b[23m/\u001b[23mi\u001b[23mm\u001b[23mp\u001b[23mr\u001b[23mo\u001b[23mv\u001b[23mi\u001b[23ms\u001b[23me\u001b[23m \u001b[23me\u001b[23mx\u001b[23ma\u001b[23mm\u001b[23mp\u001b[23ml\u001b[23me\u001b[23ms\u001b[23m/\u001b[23md\u001b[23me\u001b[23mm\u001b[23mo\u001b[23m.\u001b[23mi\u001b[23mm\u001b[23mp\u001b[23mr\u001b[23mo\u001b[23mv\u001b[?2004l\r\r\n"]
[5.713215, "o", "\u001b]0;./target/release/improvise examples/demo.improv\u0007\u001b[2 q"]
[5.754065, "o", "\u001b[?1049h"]
[5.756407, "o", "\u001b[1;1H\u001b[1m\u001b[38;5;0;48;5;4m improvise · Acme Sales Demo (demo.improv) ?:help :q quit \u001b[2;1H\u001b[22m\u001b[39;49m┌\u001b[2;3HView:\u001b[2;9HDefault\u001b[2;17H───────────────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[3;1H│\u001b[38;5;5;49m [Customer = Stark Enterprises] \u001b[3;120H\u001b[39;49m│\u001b[4;1H│\u001b[4;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost \u001b[24m Revenue Profit \u001b[4;120H\u001b[22m\u001b[39;49m│\u001b[5;1H│\u001b[5;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-02 2025-03 2025-01 2025-02 2025-03 2025-01 2025-02 2025-03\u001b[5;120H\u001b[22m\u001b[39;49m│\u001b[6;1H│\u001b[38;5;8;49m────────────────────────────────────────────────"]
[5.756495, "o", "──────────────────────────────────────────────────────────────────────\u001b[39;49m│\u001b[7;1H│\u001b[1m\u001b[38;5;6;48;5;237mGadgets North \u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[23m\u001b[39;48;5;237m \u001b[39;49m│\u001b[8;1H│\u001b[8;12HEast\u001b[8;18H\u001b[3m 5,180\u001b[38;5;8;49m \u001b[39;49m 5,670 7,400\u001b[38;5;8;49m \u001b[39;49m 8,100 2,220\u001b[38;5;8;49m \u001b[39;49m 2,430\u001b[8;120H\u001b[23m│\u001b[9;1H│\u001b[9;12HSouth\u001b[9;18H\u001b[3m\u001b[38;5;8;49m \u001b[9;120H\u001b[23m\u001b[39;49m│\u001b[10;1H│\u001b[10;12HWest\u001b[10;18H\u001b[3m\u001b[38;5;8;49m \u001b[10;120H\u001b[23m\u001b[39;49m│\u001b[11;1H│Widgets\u001b[11;12HNorth\u001b[11;18H\u001b[3m\u001b[38;5;8;49m "]
[5.756562, "o", " \u001b[11;120H\u001b[23m\u001b[39;49m│\u001b[12;1H│\u001b[12;12HEast\u001b[12;18H\u001b[3m 7,080\u001b[38;5;8;49m \u001b[39;49m 7,500 11,800\u001b[38;5;8;49m \u001b[39;49m 12,500 4,720\u001b[38;5;8;49m \u001b[39;49m 5,000\u001b[12;120H\u001b[23m│\u001b[13;1H│\u001b[13;12HSouth\u001b[13;18H\u001b[3m\u001b[38;5;8;49m \u001b[13;120H\u001b[23m\u001b[39;49m│\u001b[14;1H│\u001b[14;12HWest\u001b[14;18H\u001b[3m\u001b[38;5;8;49m \u001b[14;120H\u001b[23m\u001b[39;49m│\u001b[15;1H│Sprockets\u001b[15;12HNorth\u001b[15;18H\u001b[3m\u001b[38;5;8;49m \u001b[15;120H\u001b[23m\u001b[39;49m│\u001b[16;1H│\u001b[16;12HEast\u001b[16;18H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 2,940\u001b[38;5;8;49m \u001b[39;49m 4,200\u001b[38;5;8;49m \u001b[39;49m 1,260\u001b[38;5;8;49m \u001b[16;120H\u001b[23m\u001b[39;49m│\u001b[17;1H│\u001b[17;12HSouth\u001b[17;18H\u001b[3m\u001b[38;5;8;49m \u001b[17;120H\u001b[23m\u001b[39;49m│\u001b[18;1H│\u001b[18;12HWest"]
[5.756628, "o", "\u001b[18;18H\u001b[3m\u001b[38;5;8;49m \u001b[18;120H\u001b[23m\u001b[39;49m│\u001b[19;1H│\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[39;49m│\u001b[20;1H│\u001b[1m\u001b[38;5;3;49mTotal 12,260 2,940 13,170 19,200 4,200 20,600 6,940 1,260 7,430\u001b[20;120H\u001b[22m\u001b[39;49m│\u001b[21;1H│\u001b[21;120H│\u001b[22;1H│\u001b[22;120H│\u001b[23;1H│\u001b[23;120H│\u001b[24;1H│\u001b[24;120H│\u001b[25;1H│\u001b[25;120H│\u001b[26;1H│\u001b[26;120H│\u001b[27;1H│\u001b[27;120H│\u001b[28;1H│\u001b[28;120H│\u001b[29;1H│\u001b[29;120H│\u001b[30;1H│\u001b[30;120H│\u001b[31;1H│\u001b[31;120H│\u001b[32;1H│\u001b[32;120H│\u001b[33;1H│\u001b[33;120H│\u001b[34;1H│\u001b[34;120H│\u001b[35;1H└───────────────────"]
[5.756698, "o", "───────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[36;1H\u001b[38;5;7;49m Tiles: \u001b[36;10H\u001b[38;5;8;49m [_Index ·] [_Dim ·] \u001b[38;5;4;49m [_Measure Col] \u001b[38;5;5;49m [Customer Pag] \u001b[38;5;8;49m [Date ·] \u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;4;49m [Date_Month Col] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL hjkl:nav i:edit R:records P:prune F/C/V:panels T:tiles [:]:page >:drill ::cmd Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.859411, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.963254, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.067489, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.170023, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.27251, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.375969, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.479677, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.582112, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.685348, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.787542, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.866052, "o", "\u001b[4;17H\u001b[1m\u001b[4m\u001b[38;5;3;49m \u001b[4;21HCost \u001b[4;41H\u001b[24m Revenue \u001b[4;67HProfit \u001b[4;89H\u001b[22m\u001b[39;49m \u001b[5;17H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-02 2025-03 2025-01 2025-02 2025-03 2025-01 2025-02 2025-03\u001b[22m\u001b[39;49m \u001b[7;12H\u001b[1m\u001b[38;5;6;48;5;237mEas\u001b[7;16H \u001b[3m\u001b[38;5;0;48;5;6m \u001b[7;20H5,180\u001b[22m\u001b[38;5;8;48;5;237m \u001b[7;33H\u001b[38;5;15;48;5;237m 5,670 7,400\u001b[7;57H 8,100 2,220\u001b[7;81H 2,430\u001b[23m\u001b[39;48;5;237m \u001b[8;2H\u001b[39;49mWidgets\u001b[8;17H\u001b[3m \u001b[8;20H7,080\u001b[38;5;8;49m \u001b[8;33H\u001b[39;49m \u001b[8;36H7,500 \u001b[8;43H11,80\u001b[8;49H\u001b[38;5;8;49m \u001b[8;57H\u001b[39;49m \u001b[8;59H12,50\u001b[8;65H \u001b[8;68H4,7\u001b[8;72H0\u001b[38;5;8;49m \u001b[8;81H\u001b[39;49m \u001b[8;84H5,000\u001b[23m \u001b[9;2HSprockets\u001b[9;12HEas\u001b[9;16H \u001b[3m\u001b[38;5;8;49m \u001b[9;25H\u001b[39;49m 2,940\u001b[9;49H 4,200\u001b[9;73H 1,260\u001b[9;89H\u001b[23m \u001b[10;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────"]
[6.866248, "o", "────────────────────────────────\u001b[11;2H\u001b[1m\u001b[38;5;3;49mTotal 12,260 2,940 13,170 19,200 4,200 20,600 6,940 1,260 7,430\u001b[22m\u001b[39;49m \u001b[12;12H \u001b[12;18H \u001b[13;12H \u001b[13;18H \u001b[14;12H \u001b[14;18H \u001b[15;2H \u001b[15;12H \u001b[15;18H \u001b[16;12H \u001b[16;18H \u001b[17;12H \u001b[17;18H \u001b[18;12H \u001b[18;18H \u001b[19;2H \u001b[20;2H "]
[6.866345, "o", " \u001b[37;11H\u001b[38;5;0;48;5;8mHiding empty rows/columns \u001b[37;40H \u001b[37;49H \u001b[37;63H \u001b[37;72H \u001b[37;82H \u001b[37;91H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.967808, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.07036, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.171986, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.273477, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.37608, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.477589, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.579865, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.681762, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.784444, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.887075, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.989043, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.091087, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.192621, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.295349, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.396347, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.499325, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.601635, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.703762, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.805045, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.907044, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.008857, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.110585, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.212366, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.315082, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.417176, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.518617, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.620581, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.723247, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.825828, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.927309, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.029287, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.131114, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.232595, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.334463, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.436432, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.537342, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.586615, "o", "\u001b[36;10H\u001b[1m\u001b[38;5;0;48;5;6m [_Index ·] \u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;5m TILES Hiding empty rows/columns Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.688844, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.79055, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.891459, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.992994, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.094817, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.196506, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.208979, "o", "\u001b[36;10H\u001b[38;5;8;49m [_Index ·] \u001b[1m\u001b[38;5;0;48;5;6m [_Dim ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.310676, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.412609, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.472655, "o", "\u001b[36;22H\u001b[38;5;8;49m [_Dim ·] \u001b[1m\u001b[38;5;0;48;5;6m [_Measure Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.57519, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.621489, "o", "\u001b[36;32H\u001b[38;5;4;49m [_Measure Col] \u001b[1m\u001b[38;5;0;48;5;6m [Customer Pag] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.723727, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.810324, "o", "\u001b[36;48H\u001b[38;5;5;49m [Customer Pag] \u001b[1m\u001b[38;5;0;48;5;6m [Date ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.912742, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.992715, "o", "\u001b[36;64H\u001b[38;5;8;49m [Date ·] \u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.095391, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.197498, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.225504, "o", "\u001b[36;74H\u001b[38;5;2;49m [Product Row] \u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.327415, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.429585, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.534313, "o", "\u001b[36;89H\u001b[38;5;2;49m [Region Row] \u001b[1m\u001b[38;5;0;48;5;6m [Date_Month Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.636065, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.738586, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.840575, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.942807, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.045034, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.147374, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.248519, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.350293, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.411399, "o", "\u001b[1;47H\u001b[1m\u001b[38;5;0;48;5;4m[+]\u001b[4;20H\u001b[4m\u001b[38;5;3;49mCost\u001b[24m Revenue Profit\u001b[22m\u001b[39;49m \u001b[5;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets East \u001b[3m\u001b[38;5;0;48;5;6m 10,850\u001b[22m\u001b[38;5;15;48;5;237m 15,500 4,650\u001b[23m\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49mWidgets East \u001b[3m 14,580 24,300 9,720\u001b[23m \u001b[8;2HSprockets\u001b[8;19H\u001b[3m2,940 4,200 \u001b[8;34H1,260\u001b[23m \u001b[9;2H\u001b[38;5;8;49m────────────"]
[13.411561, "o", "──────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[10;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 44,000 15,630\u001b[22m\u001b[39;49m \u001b[11;2H \u001b[36;116H\u001b[1m\u001b[38;5;0;48;5;6m·] \u001b[22m\u001b[39;49m \u001b[37;10H\u001b[38;5;0;48;5;5mDate_Month →\u001b[37;23HN\u001b[37;25Hne \u001b[37;110HDefault \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.513136, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.61531, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.717724, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.819203, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.920313, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.021863, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.122842, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.224088, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.325723, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.426976, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.527862, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.629536, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.731181, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.777459, "o", "\u001b[36;103H\u001b[38;5;8;49m [Date_Month ·] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL Date_Month → None Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.879201, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.980529, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.081904, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.182728, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.284144, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.386367, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.488405, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.590322, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.691881, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.771463, "o", "\u001b[2;88H┐\u001b[38;5;3;49m┌ Formulas [n]ew [d]elete ─────┐\u001b[3;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[1m\u001b[38;5;0;48;5;3m Profit = Revenue - Cost\u001b[3;120H\u001b[22m\u001b[38;5;3;49m│\u001b[4;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[4;120H│\u001b[5;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[39;49m \u001b[38;5;3;49m│\u001b[6;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[39;49m \u001b[38;5;3;49m│\u001b[7;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[7;120H│\u001b[8;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[8;120H│\u001b[9;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[39;49m \u001b[38;5;3;49m│\u001b[10;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[10;120H│\u001b[11;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[11;120H│\u001b[12;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[12;120H│\u001b[13;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[13;120H│\u001b[14;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[14;120H│\u001b[15;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[15;120H│\u001b[16;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[16;120H│\u001b[17;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[17;120H│\u001b[18;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[18;120H│\u001b[19;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[19;120H│"]
[15.771523, "o", "\u001b[20;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[20;120H│\u001b[21;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[21;120H│\u001b[22;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[22;120H│\u001b[23;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[23;120H│\u001b[24;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[24;120H│\u001b[25;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[25;120H│\u001b[26;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[26;120H│\u001b[27;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[27;120H│\u001b[28;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[28;120H│\u001b[29;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[29;120H│\u001b[30;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[30;120H│\u001b[31;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[31;120H│\u001b[32;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[32;120H│\u001b[33;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[33;120H│\u001b[34;88H\u001b[39;49m│\u001b[38;5;3;49m│\u001b[34;120H│\u001b[35;88H\u001b[39;49m┘\u001b[38;5;3;49m└──────────────────────────────┘\u001b[37;3H\u001b[38;5;0;48;5;8mF\u001b[37;7HU\u001b[37;9HAS Date_Month\u001b[37;24H→ None\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.873015, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.974298, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.075518, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.17702, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.278763, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.380421, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.482397, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.584035, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.68617, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.788334, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.890119, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.992122, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.09391, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.195828, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.297212, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.399072, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.500797, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.602599, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.704051, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.806336, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.90867, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.01054, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.112401, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.134611, "o", "\u001b[34;90H\u001b[38;5;3;49m┄ Enter formula (Name = expr)\u001b[37;1H\u001b[1m\u001b[38;5;6;48;5;235mformula: ▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.236595, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.338804, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.440583, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.541464, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.642436, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.656428, "o", "\u001b[37;10H\u001b[1m\u001b[38;5;6;48;5;235mM▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.757466, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.858857, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.957266, "o", "\u001b[37;11H\u001b[1m\u001b[38;5;6;48;5;235ma▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.058764, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.104126, "o", "\u001b[37;12H\u001b[1m\u001b[38;5;6;48;5;235mr▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.206143, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.254003, "o", "\u001b[37;13H\u001b[1m\u001b[38;5;6;48;5;235mg▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.355945, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.40299, "o", "\u001b[37;14H\u001b[1m\u001b[38;5;6;48;5;235mi▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.505037, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.550273, "o", "\u001b[37;15H\u001b[1m\u001b[38;5;6;48;5;235mn▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.651795, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.731805, "o", "\u001b[37;16H\u001b[1m\u001b[38;5;6;48;5;235m ▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.833103, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.88046, "o", "\u001b[37;17H\u001b[1m\u001b[38;5;6;48;5;235m=▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.982487, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.0286, "o", "\u001b[37;18H\u001b[1m\u001b[38;5;6;48;5;235m ▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.129755, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.180513, "o", "\u001b[37;19H\u001b[1m\u001b[38;5;6;48;5;235m1▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.282654, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.334596, "o", "\u001b[37;20H\u001b[1m\u001b[38;5;6;48;5;235m0▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.436242, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.484785, "o", "\u001b[37;21H\u001b[1m\u001b[38;5;6;48;5;235m0▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.58654, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.664649, "o", "\u001b[37;22H\u001b[1m\u001b[38;5;6;48;5;235m ▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.765442, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.867084, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.96863, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.991979, "o", "\u001b[37;23H\u001b[1m\u001b[38;5;6;48;5;235m*▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.09353, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.161257, "o", "\u001b[37;24H\u001b[1m\u001b[38;5;6;48;5;235m ▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.263347, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.364397, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.465102, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.566609, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.636594, "o", "\u001b[37;25H\u001b[1m\u001b[38;5;6;48;5;235mP▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.73766, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.839314, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.930228, "o", "\u001b[37;26H\u001b[1m\u001b[38;5;6;48;5;235mr▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.030863, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.080686, "o", "\u001b[37;27H\u001b[1m\u001b[38;5;6;48;5;235mo▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.1822, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.232387, "o", "\u001b[37;28H\u001b[1m\u001b[38;5;6;48;5;235mf▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.334144, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.432432, "o", "\u001b[37;29H\u001b[1m\u001b[38;5;6;48;5;235mi▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.534534, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.602656, "o", "\u001b[37;30H\u001b[1m\u001b[38;5;6;48;5;235mt▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.704246, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.749303, "o", "\u001b[37;31H\u001b[1m\u001b[38;5;6;48;5;235m ▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.850361, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.900622, "o", "\u001b[37;32H\u001b[1m\u001b[38;5;6;48;5;235m/▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.001894, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.071145, "o", "\u001b[37;33H\u001b[1m\u001b[38;5;6;48;5;235m ▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.172849, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.273367, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.374896, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.379148, "o", "\u001b[37;34H\u001b[1m\u001b[38;5;6;48;5;235mR▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.480866, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.582066, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.684013, "o", "\u001b[37;35H\u001b[1m\u001b[38;5;6;48;5;235me▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.784848, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.834019, "o", "\u001b[37;36H\u001b[1m\u001b[38;5;6;48;5;235mv▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.935533, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.000423, "o", "\u001b[37;37H\u001b[1m\u001b[38;5;6;48;5;235me▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.101663, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.187318, "o", "\u001b[37;38H\u001b[1m\u001b[38;5;6;48;5;235mn▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.288471, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.374963, "o", "\u001b[37;39H\u001b[1m\u001b[38;5;6;48;5;235mu▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.47588, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.578051, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.679745, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.781518, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.883965, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.985622, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.087356, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.189121, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.290385, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.392349, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.493825, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.595221, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.696678, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.798409, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.900396, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.001989, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.103513, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.205321, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.280519, "o", "\u001b[37;40H\u001b[1m\u001b[38;5;6;48;5;235me▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.382821, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.48443, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.586182, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.68818, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.790294, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.892088, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.958354, "o", "\u001b[4;39H\u001b[1m\u001b[38;5;3;49m Margin\u001b[4;90H\u001b[22m\u001b[38;5;2;49m Margin = 100 * Profit / Rev…\u001b[6;39H\u001b[3m\u001b[38;5;15;48;5;237m 30\u001b[7;39H\u001b[39;49m 40\u001b[8;39H 30\u001b[10;39H\u001b[23m\u001b[1m\u001b[38;5;3;49m 100\u001b[34;90H\u001b[22m\u001b[39;49m \u001b[37;1H\u001b[38;5;0;48;5;8m FORMULAS Formula added Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.060639, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.162564, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.264269, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.36574, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.467341, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.568744, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.670688, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.7723, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.873949, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.975642, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.077335, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.178827, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.280529, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.382127, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.48383, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.586228, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.687929, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.790028, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.823665, "o", "\u001b[2;89H\u001b[38;5;8;49m┌ Formulas [n]ew [d]elete ─────┐\u001b[3;89H│\u001b[38;5;2;49m Profit = Revenue - Cost\u001b[3;120H\u001b[38;5;8;49m│\u001b[4;89H│\u001b[4;120H│\u001b[5;89H│\u001b[5;120H│\u001b[6;89H│\u001b[6;120H│\u001b[7;89H│\u001b[7;120H│\u001b[8;89H│\u001b[8;120H│\u001b[9;89H│\u001b[9;120H│\u001b[10;89H│\u001b[10;120H│\u001b[11;89H│\u001b[11;120H│\u001b[12;89H│\u001b[12;120H│\u001b[13;89H│\u001b[13;120H│\u001b[14;89H│\u001b[14;120H│\u001b[15;89H│\u001b[15;120H│\u001b[16;89H│\u001b[16;120H│\u001b[17;89H│\u001b[17;120H│\u001b[18;89H│\u001b[18;120H│\u001b[19;89H│\u001b[19;120H│\u001b[20;89H│\u001b[20;120H│\u001b[21;89H│\u001b[21;120H│\u001b[22;89H│\u001b[22;120H│\u001b[23;89H│\u001b[23;120H│\u001b[24;89H│\u001b[24;120H│\u001b[25;89H│\u001b[25;120H│\u001b[26;89H│\u001b[26;120H│\u001b[27;89H│\u001b[27;120H│\u001b[28;89H│\u001b[28;120H│\u001b[29;89H│\u001b[29;120H│\u001b[30;89H│\u001b[30;120H│\u001b[31;89H│\u001b[31;120H│\u001b[32;89H│\u001b[32;120H│\u001b[33;89H│\u001b[33;120H│\u001b[34;89H│\u001b[34;120H│\u001b[35;89H└──────────────────────────────┘\u001b[37;3H\u001b[38;5;0;48;5;8mN\u001b[37;7HA\u001b[37;9H Formula \u001b[37;20Hdde\u001b[37;24H \u001b[39m\u001b[49m\u001b[59"]
[28.82381, "o", "m\u001b[0m\u001b[?25l"]
[28.925823, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.028152, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.129687, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.23137, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.333203, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.434603, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.536387, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.638191, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.74009, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.841736, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.943467, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.045355, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.146794, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.248304, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.350033, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.452178, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.553839, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.655545, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.757685, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.859652, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.961651, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.063178, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.164853, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.26659, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.368454, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.469947, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.571386, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.673047, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.774632, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.83504, "o", "\u001b[2;89H\u001b[38;5;3;49m┌ Formulas [n]ew [d]elete ─────┐\u001b[3;89H│\u001b[1m\u001b[38;5;0;48;5;3m Profit = Revenue - Cost\u001b[3;120H\u001b[22m\u001b[38;5;3;49m│\u001b[4;89H│\u001b[4;120H│\u001b[5;89H│\u001b[5;120H│\u001b[6;89H│\u001b[6;120H│\u001b[7;89H│\u001b[7;120H│\u001b[8;89H│\u001b[8;120H│\u001b[9;89H│\u001b[9;120H│\u001b[10;89H│\u001b[10;120H│\u001b[11;89H│\u001b[11;120H│\u001b[12;89H│\u001b[12;120H│\u001b[13;89H│\u001b[13;120H│\u001b[14;89H│\u001b[14;120H│\u001b[15;89H│\u001b[15;120H│\u001b[16;89H│\u001b[16;120H│\u001b[17;89H│\u001b[17;120H│\u001b[18;89H│\u001b[18;120H│\u001b[19;89H│\u001b[19;120H│\u001b[20;89H│\u001b[20;120H│\u001b[21;89H│\u001b[21;120H│\u001b[22;89H│\u001b[22;120H│\u001b[23;89H│\u001b[23;120H│\u001b[24;89H│\u001b[24;120H│\u001b[25;89H│\u001b[25;120H│\u001b[26;89H│\u001b[26;120H│\u001b[27;89H│\u001b[27;120H│\u001b[28;89H│\u001b[28;120H│\u001b[29;89H│\u001b[29;120H│\u001b[30;89H│\u001b[30;120H│\u001b[31;89H│\u001b[31;120H│\u001b[32;89H│\u001b[32;120H│\u001b[33;89H│\u001b[33;120H│\u001b[34;89H│\u001b[34;120H│\u001b[35;89H└──────────────────────────────┘\u001b[37;3H\u001b[38;5;0;48;5;8mF\u001b[37;7HU\u001b[37;9HAS Formul\u001b[37;20H ad\u001b[37;24Hed\u001b"]
[31.83521, "o", "[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.93635, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.038453, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.140305, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.24179, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.343021, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.444953, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.546884, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.648532, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.750338, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.759627, "o", "\u001b[2;88H────────────────────────────────┐\u001b[3;88H \u001b[3;120H│\u001b[4;88H │\u001b[5;88H\u001b[38;5;8;49m────────────────────────────────\u001b[39;49m│\u001b[6;88H\u001b[39;48;5;237m \u001b[39;49m│\u001b[7;88H \u001b[7;120H│\u001b[8;88H \u001b[8;120H│\u001b[9;88H\u001b[38;5;8;49m────────────────────────────────\u001b[39;49m│\u001b[10;88H \u001b[10;120H│\u001b[11;88H \u001b[11;120H│\u001b[12;88H \u001b[12;120H│\u001b[13;88H \u001b[13;120H│\u001b[14;88H \u001b[14;120H│\u001b[15;88H \u001b[15;120H│\u001b[16;88H \u001b[16;120H│\u001b[17;88H \u001b[17;120H│\u001b[18;88H \u001b[18;120H│\u001b[19;88H \u001b[19;120H│\u001b[20;88H \u001b[20;120H│\u001b[21;88H \u001b[21;120H│\u001b[22;88H \u001b[22;120H│\u001b[23;88H \u001b[23;120H│\u001b[24;88H \u001b[24;120H│\u001b[25;88H \u001b[25;120H│\u001b[26;88H \u001b[26;120H│\u001b[27;88H \u001b[27;120H│\u001b[28;88H \u001b[28;120H│\u001b[29;88H \u001b[29;120H│\u001b[30;88H \u001b[30;120H│\u001b[31;88H \u001b[3"]
[32.759661, "o", "1;120H│\u001b[32;88H \u001b[32;120H│\u001b[33;88H \u001b[33;120H│\u001b[34;88H \u001b[34;120H│\u001b[35;88H────────────────────────────────┘\u001b[37;3H\u001b[38;5;0;48;5;8mN\u001b[37;7HA\u001b[37;9H Formula \u001b[37;20Hdde\u001b[37;24H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.861687, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.963943, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.065622, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.167382, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.269343, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.370929, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.472205, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.574072, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.675496, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.77751, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.878963, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.981217, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.082469, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.18416, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.285853, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.387215, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.488822, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.590148, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.691868, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.793678, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.894883, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.996624, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.098854, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.199728, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.30097, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.402643, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.504418, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.605968, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.707659, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.813626, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.915915, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.017878, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.119781, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.221525, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.32263, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.424295, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.525566, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.627568, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.729398, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.830794, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.9326, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.956636, "o", "\u001b[4;17H\u001b[1m\u001b[38;5;3;49m Cost\u001b[4m Revenue\u001b[6;17H\u001b[22m\u001b[24m\u001b[3m\u001b[38;5;15;48;5;237m 10,850\u001b[1m\u001b[38;5;0;48;5;6m 15,500\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.057713, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.159175, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.260616, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.362003, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.45116, "o", "\u001b[4;24H\u001b[1m\u001b[38;5;3;49m Revenue\u001b[4m Profit\u001b[6;24H\u001b[22m\u001b[24m\u001b[3m\u001b[38;5;15;48;5;237m 15,500\u001b[1m\u001b[38;5;0;48;5;6m 4,650\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.553269, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.654984, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.756544, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.855164, "o", "\u001b[4;32H\u001b[1m\u001b[38;5;3;49m Profit\u001b[4m Margin\u001b[6;32H\u001b[22m\u001b[24m\u001b[3m\u001b[38;5;15;48;5;237m 4,650\u001b[1m\u001b[38;5;0;48;5;6m 30\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.95702, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.05867, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.16023, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.261882, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.364382, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.465665, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.568143, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.66929, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.770404, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.872114, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.9737, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.075442, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.177113, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.279174, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.381483, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.483532, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.585266, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.68695, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.788157, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.890074, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.991552, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.092834, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.194587, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.283998, "o", "\u001b[36;10H\u001b[1m\u001b[38;5;0;48;5;6m [_Index ·] \u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;5m TILES Formula added Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.386092, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.487642, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.5894, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.691098, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.792446, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.894254, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.995483, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.048638, "o", "\u001b[36;10H\u001b[38;5;8;49m [_Index ·] \u001b[1m\u001b[38;5;0;48;5;6m [_Dim ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.150203, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.252282, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.354288, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.363494, "o", "\u001b[36;22H\u001b[38;5;8;49m [_Dim ·] \u001b[1m\u001b[38;5;0;48;5;6m [_Measure Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.465135, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.567014, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.652538, "o", "\u001b[36;32H\u001b[38;5;4;49m [_Measure Col] \u001b[1m\u001b[38;5;0;48;5;6m [Customer Pag] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.753272, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.854376, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.886425, "o", "\u001b[36;48H\u001b[38;5;5;49m [Customer Pag] \u001b[1m\u001b[38;5;0;48;5;6m [Date ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.988321, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.089981, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.191526, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.210178, "o", "\u001b[36;64H\u001b[38;5;8;49m [Date ·] \u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.312329, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.414458, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.516377, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.523473, "o", "\u001b[36;74H\u001b[38;5;2;49m [Product Row] \u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.624892, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.726668, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.828869, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.929605, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.969045, "o", "\u001b[36;89H\u001b[38;5;2;49m [Region Row] \u001b[1m\u001b[38;5;0;48;5;6m [Date_Month ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.071061, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.172731, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.274463, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.375888, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.47684, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.578927, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.680089, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.759548, "o", "\u001b[36;89H\u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[22m\u001b[38;5;8;49m [Date_Month ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.86162, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.963419, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.064814, "o", "\u001b[36;74H\u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[22m\u001b[38;5;2;49m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.165578, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.266915, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.368077, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.467449, "o", "\u001b[36;64H\u001b[1m\u001b[38;5;0;48;5;6m [Date ·] \u001b[22m\u001b[38;5;2;49m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.569373, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.6706, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.772271, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.873952, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.975198, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.067862, "o", "\u001b[4;17H \u001b[1m\u001b[38;5;3;49m Cost Revenue Profit\u001b[4m Margin\u001b[6;2H\u001b[24m\u001b[38;5;6;48;5;237m2025-03-25 Gadgets East \u001b[22m\u001b[3m\u001b[38;5;15;48;5;237m 5,670 8,100 2,430\u001b[1m\u001b[38;5;0;48;5;6m 30\u001b[7;2H\u001b[22m\u001b[23m\u001b[39;49m2025-03-05 Widgets East \u001b[3m 7,500 12,500\u001b[7;44H 5,000 40\u001b[8;2H\u001b[23m2025-01-31 Gadgets East \u001b[3m 5,180 7,400\u001b[8;44H 2,220 30\u001b[9;2H\u001b[23m2025-02-20 Sprockets East \u001b[3m 2,940 4,200 1,260 30\u001b[23m \u001b[10;2H2025-01-17 Widgets East \u001b[3m 7,080 11,800 4,720 40\u001b[11;2H\u001b[23m\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[12;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 44,000 15,630 170"]
[45.067912, "o", "\u001b[36;71H\u001b[38;5;0;48;5;6mRow] \u001b[22m\u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;8;49m [Date_Month ·] \u001b[37;10H\u001b[38;5;0;48;5;5mDate → Row \u001b[37;110HDefault \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.169513, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.271027, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.372769, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.47433, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.575726, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.677188, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.779086, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.880875, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.982323, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.084014, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.185947, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.287438, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.388634, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.490413, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.59213, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.693451, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.795674, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.897368, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.999478, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.100406, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.202354, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.303499, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.405377, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.507349, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.608859, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.710466, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.769032, "o", "\u001b[36;64H\u001b[38;5;2;49m [Date Row] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL Date → Row Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.870773, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.973227, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.074952, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.176627, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.278078, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.379913, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.48167, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.583195, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.68488, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.786081, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.888008, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.989799, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.090852, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.192203, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.293577, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.395558, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.497376, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.598701, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.700034, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.800738, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.902188, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.004731, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.10652, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.208391, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.310331, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.412538, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.514326, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.616773, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.717698, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.820295, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.921874, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.023865, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.125694, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.227335, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.328825, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.430927, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.479132, "o", "\u001b[4;43H\u001b[1m\u001b[4m\u001b[38;5;3;49m Profit\u001b[24m Margin\u001b[6;43H\u001b[3m\u001b[38;5;0;48;5;6m 2,430\u001b[22m\u001b[38;5;15;48;5;237m 30\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.581056, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.682894, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.784319, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.886355, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.988029, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.09004, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.191833, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.294121, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.396186, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.447223, "o", "\u001b[4;35H\u001b[1m\u001b[4m\u001b[38;5;3;49m Revenue\u001b[24m Profit\u001b[6;35H\u001b[3m\u001b[38;5;0;48;5;6m 8,100\u001b[22m\u001b[38;5;15;48;5;237m 2,430\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.548933, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.650639, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.753129, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.855032, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.956843, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.992371, "o", "\u001b[4;28H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost\u001b[24m Revenue\u001b[6;28H\u001b[3m\u001b[38;5;0;48;5;6m 5,670\u001b[22m\u001b[38;5;15;48;5;237m 8,100\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.093934, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.195476, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.297821, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.39909, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.5012, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.603706, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.705669, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.807421, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[53.909072, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.010788, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.113124, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.214937, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.315779, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.401838, "o", "\u001b[2;9H_Dril\u001b[2;15H ─\u001b[3;4H\u001b[38;5;5;49m_Measure\u001b[3;15HCost | Customer = Stark Enterprises | Date = 2025-03-25 | Product = Gadgets | Region = East] \u001b[4;6H\u001b[1m\u001b[4m\u001b[38;5;3;49m Customer\u001b[24m Date\u001b[4;36HProduct\u001b[4;44HRegion\u001b[4;51HD\u001b[4;53Hte_Month Value\u001b[6;2H\u001b[38;5;6;48;5;237m0 \u001b[38;5;0;48;5;6m Stark Enterprises\u001b[22m\u001b[38;5;15;48;5;237m 2025-03-25 Gadgets East 2025-03 5670\u001b[7;2H\u001b[39;49m \u001b[7;13H \u001b[7;23H \u001b[7;28H \u001b[8;2H \u001b[8;13H \u001b[8;23H \u001b[8;28H \u001b[9;2H \u001b[9;13H \u001b[9;23H \u001b[9;28H \u001b[10;2H \u001b[10;13H \u001b[10;23H \u001b[10;28H \u001b[11;2H \u001b[12;2H \u001b[36;10H\u001b[38;5;2;49m [_Index Row] \u001b[38;5;4;49m [_Dim Col] \u001b[38;5;5;49m [_Measure Pag] [Customer Pag] [Date Pag] [Produc"]
[54.402009, "o", "t Pag] [Region Pag] \u001b[38;5;8;49m▶\u001b[39;49m \u001b[37;12H\u001b[38;5;0;48;5;8mrilled into\u001b[37;24Hcell:\u001b[37;30H1\u001b[37;32Hrows\u001b[37;110H _Drill\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.503698, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.605285, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.706675, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.807631, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[54.909386, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.011169, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.113111, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.214654, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.316369, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.417789, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.51931, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.620988, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.722632, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.823944, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[55.925315, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.027107, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.128956, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.230678, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.332344, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.433936, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.535729, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.637293, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.738954, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.840527, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[56.942072, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.043456, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.150712, "o", "\u001b[2;9HDefau\u001b[2;15Ht \u001b[3;4H\u001b[38;5;5;49mCustomer\u001b[3;15HStark Enterprises] \u001b[39;49m \u001b[4;6H \u001b[1m\u001b[4m\u001b[38;5;3;49m Cost\u001b[4;36H\u001b[24mRevenue\u001b[4;44HProfit\u001b[4;51HM\u001b[4;53Hrgin\u001b[22m\u001b[39;49m \u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237m2025-03-25 Gadgets East \u001b[3m\u001b[38;5;0;48;5;6m 5,670\u001b[22m\u001b[38;5;15;48;5;237m 8,100 2,430 30\u001b[23m\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49m2025-03-05\u001b[7;13HWidgets\u001b[7;23HEast\u001b[7;28H\u001b[3m 7,500 12,500 5,000 40\u001b[8;2H\u001b[23m2025-01-31\u001b[8;13HGadgets\u001b[8;23HEast\u001b[8;28H\u001b[3m 5,180 7,400 2,220 30\u001b[9;2H\u001b[23m2025-02-20\u001b[9;13HSprockets\u001b[9;23HEast\u001b[9;28H\u001b[3m 2,940 4,200 1,260 30\u001b[10;2H\u001b[23m2025-01-17\u001b[10;13HWidgets\u001b[10;23HEast\u001b[10;28H\u001b[3m 7,080 11,800 4,720 40\u001b[11;2H\u001b[23m\u001b[38;5;8;49m─────────────────────────────────────────────────────────────────────"]
[57.150756, "o", "─────────────────────────────────────────────────\u001b[12;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 44,000 15,630 170\u001b[36;10H\u001b[22m\u001b[38;5;8;49m [_Index ·] [_Dim ·] \u001b[38;5;4;49m [_Measure Col] \u001b[38;5;5;49m [Customer Pag] \u001b[38;5;2;49m [Date Row] [Product Row] [Region Row] \u001b[38;5;8;49m [Date_Month ·] \u001b[37;112H\u001b[38;5;0;48;5;8mDefau\u001b[37;118Ht\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.252651, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.354764, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.455662, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.557383, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.658952, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.760706, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.862365, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[57.964025, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.066018, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.117152, "o", "\u001b[3;16H\u001b[38;5;5;49moylent Ltd] \u001b[39;49m \u001b[4;28H \u001b[4;31H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost\u001b[24m Revenue Profit Margin\u001b[6;8H\u001b[38;5;6;48;5;237m2\u001b[6;11H2\u001b[6;13HSprockets\u001b[6;23HSou\u001b[6;27Hh \u001b[6;30H\u001b[3m\u001b[38;5;0;48;5;6m 2,170\u001b[6;38H\u001b[22m\u001b[38;5;15;48;5;237m 3,1\u001b[6;43H0\u001b[6;45H 930\u001b[6;55H 30\u001b[7;10H\u001b[23m\u001b[39;49m18\u001b[7;13HGa\u001b[7;23HSou\u001b[7;27Hh \u001b[7;30H\u001b[3m 4,760\u001b[7;37H 6,8\u001b[7;43H0\u001b[7;45H 2,\u001b[7;49H40\u001b[7;55H 30\u001b[8;10H\u001b[23m14\u001b[8;23HSou\u001b[8;27Hh \u001b[8;30H\u001b[3m 3,\u001b[8;34H50\u001b[8;38H 5,5\u001b[8;43H0\u001b[8;45H 1,650\u001b[8;55H 30\u001b[9;10H\u001b[23m03\u001b[9;13HWidgets \u001b[9;23HSou\u001b[9;27Hh \u001b[9;30H\u001b[3m 6,120\u001b[9;38H10,2\u001b[9;43H0\u001b[9;45H 4,080\u001b[9;55H 40\u001b[10;10H\u001b[23m09\u001b[10;23HSou\u001b[10;27Hh \u001b[10;30H\u001b[3m 5,\u001b[10;34H80\u001b[10;37H 9,8\u001b[10;43H0\u001b[10;45H 3,920\u001b[10;55H 40\u001b[12;29H\u001b[23m\u001b[1m\u001b[38;5;3;49m 22,\u001b[12;34H80\u001b[12;37H 35,4\u001b[12;43H0 12,620\u001b[12;54H 170\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.218907, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.320446, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.422194, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.523868, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.625832, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.727316, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.828823, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.871774, "o", "\u001b[3;15H\u001b[38;5;5;49mW\u001b[3;17Hnka Industries] \u001b[4;28H\u001b[1m\u001b[4m\u001b[38;5;3;49m \u001b[4;31HCost\u001b[24m Revenue Profit Margin\u001b[22m\u001b[39;49m \u001b[6;8H\u001b[1m\u001b[38;5;6;48;5;237m1\u001b[6;11H3\u001b[6;13HGadgets \u001b[6;23HEas\u001b[6;27H \u001b[3m\u001b[38;5;0;48;5;6m \u001b[6;30H6,230\u001b[22m\u001b[38;5;15;48;5;237m \u001b[6;38H8,90\u001b[6;43H \u001b[6;45H2,670 \u001b[6;55H30\u001b[23m\u001b[39;48;5;237m \u001b[7;8H\u001b[39;49m1\u001b[7;10H06\u001b[7;13HWi\u001b[7;23HEas\u001b[7;27H \u001b[3m \u001b[7;30H8,520 \u001b[7;37H14,20\u001b[7;43H \u001b[7;45H5,680 \u001b[7;55H40\u001b[23m \u001b[8;8H2\u001b[8;11H0\u001b[8;13HWi\u001b[8;23HEas\u001b[8;27H \u001b[3m \u001b[8;30H9,000 \u001b[8;37H15,00\u001b[8;43H \u001b[8;45H6,000 \u001b[8;55H40\u001b[23m \u001b[9;8H3\u001b[9;10H14\u001b[9;13HSprockets\u001b[9;23HEas\u001b[9;27H \u001b[3m \u001b[9;30H3,360 \u001b[9;38H4,80\u001b[9;43H \u001b[9;45H1,440 \u001b[9;55H30\u001b[23m \u001b[10;8H2\u001b[10;10H28\u001b[10;13HGa\u001b[10;23HEas\u001b[10;27H \u001b[3m \u001b[10;30H6,440 \u001b[10;38H9,20\u001b[10;43H \u001b[10;45H2,760 \u001b[10;55H30\u001b[23m \u001b[12;29H\u001b[1m\u001b[38;5;3;49m33,550 \u001b[12;37H52,10\u001b[12;43H 18,550 \u001b[12;54H170\u001b[22m\u001b[39;49m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[58.973652, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.07548, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.177244, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.278352, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.380072, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.481511, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.596012, "o", "\u001b[3;15H\u001b[38;5;5;49mCyberdy\u001b[3;23He Syst\u001b[3;30Hms] \u001b[6;8H\u001b[1m\u001b[38;5;6;48;5;237m3\u001b[6;10H12\u001b[6;23HWe\u001b[6;30H\u001b[3m\u001b[38;5;0;48;5;6m5\u001b[6;33H5\u001b[6;38H\u001b[22m\u001b[38;5;15;48;5;237m7\u001b[6;40H5\u001b[6;47H25\u001b[7;8H\u001b[23m\u001b[39;49m3\u001b[7;10H30\u001b[7;23HWe\u001b[7;30H\u001b[3m5\u001b[7;32H88\u001b[7;37H 9\u001b[7;40H8\u001b[7;45H3\u001b[7;47H92\u001b[8;8H\u001b[23m1\u001b[8;11H9\u001b[8;13HGa\u001b[8;23HWe\u001b[8;30H\u001b[3m4\u001b[8;32H69\u001b[8;37H 6\u001b[8;40H7\u001b[8;45H2\u001b[8;48H1\u001b[8;55H3\u001b[9;8H\u001b[23m2\u001b[9;10H2\u001b[9;23HWe\u001b[9;30H\u001b[3m2\u001b[9;32H52\u001b[9;38H3\u001b[9;40H6\u001b[9;47H08\u001b[10;10H\u001b[23m06\u001b[10;13HWi\u001b[10;23HWe\u001b[10;32H\u001b[3m72\u001b[10;37H11\u001b[10;45H4\u001b[10;47H48\u001b[10;55H4\u001b[12;29H\u001b[23m\u001b[1m\u001b[38;5;3;49m25\u001b[12;32H06\u001b[12;37H38\u001b[12;40H8\u001b[12;45H3\u001b[12;47H74\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.697594, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.798754, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[59.901337, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.002239, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.104595, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.206801, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.308609, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.324926, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.42746, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.529292, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.631466, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.733806, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.835985, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[60.937904, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.039574, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.141223, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.158547, "o", "\u001b[4;28H\u001b[1m\u001b[38;5;3;49m Cost\u001b[4m Revenue\u001b[6;28H\u001b[22m\u001b[24m\u001b[3m\u001b[38;5;15;48;5;237m 5,250\u001b[1m\u001b[38;5;0;48;5;6m 7,500\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.260014, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.361614, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.463241, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.565458, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.66735, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.769039, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.870889, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[61.972547, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.073955, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.174929, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.276844, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.378546, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.480073, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.581655, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.683563, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.710581, "o", "\u001b[36;10H\u001b[1m\u001b[38;5;0;48;5;6m [_Index ·] \u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;5m TILES Drilled into cell: 1 rows Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.812801, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[62.914448, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.016597, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.118395, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.141392, "o", "\u001b[36;10H\u001b[38;5;8;49m [_Index ·] \u001b[1m\u001b[38;5;0;48;5;6m [_Dim ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.243176, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.344884, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.447866, "o", "\u001b[36;22H\u001b[38;5;8;49m [_Dim ·] \u001b[1m\u001b[38;5;0;48;5;6m [_Measure Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.548899, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.650249, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.717023, "o", "\u001b[36;32H\u001b[38;5;4;49m [_Measure Col] \u001b[1m\u001b[38;5;0;48;5;6m [Customer Pag] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.818713, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[63.919536, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[64.02132, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[64.122911, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[64.184957, "o", "\u001b[3;2H \u001b[1m\u001b[4m\u001b[38;5;3;49m Cost \u001b[4;28H\u001b[22m\u001b[24m\u001b[39;49m \u001b[4;30H\u001b[1m\u001b[38;5;3;49mStark Enterprises\u001b[4m Soylent Ltd\u001b[24m Wonka Industries Cyberdyne Systems Acme Corp\u001b[6;8H\u001b[38;5;6;48;5;237m1\u001b[6;10H23\u001b[6;23HEa\u001b[6;28H \u001b[22m\u001b[3m\u001b[38;5;8;48;5;237m \u001b[1m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;15;48;5;237m 6,230\u001b[38;5;8;48;5;237m \u001b[7;10H\u001b[23m\u001b[39;49m25\u001b[7;13HGa\u001b[7;23HEa\u001b[7;28H \u001b[7;30H\u001b[3m \u001b[7;38H 5,670\u001b[38;5;8;49m \u001b[8;11H\u001b[23m\u001b[39;49m1\u001b[8;13HWi\u001b[8;28H \u001b[3m\u001b[38;5;8;49m \u001b[9;8H\u001b[23m\u001b[39;49m3\u001b[9;10H12\u001b[9;13HGadgets \u001b[9;28H \u001b[3m\u001b[38;5;8;49m \u001b[39;49m 5,250\u001b[38;5;8;49m \u001b[10;8H\u001b[23m\u001b[39;49m1\u001b[10;23HEa\u001b[10;28H \u001b[3m\u001b[38;5;8;49m \u001b[39;49m 8"]
[64.185061, "o", ",520\u001b[38;5;8;49m \u001b[11;2H\u001b[23m\u001b[39;49m2025-01-15 Widgets North \u001b[3m\u001b[38;5;8;49m \u001b[39;49m 7,200\u001b[23m \u001b[12;2H2025-03-30 Widgets West \u001b[3m\u001b[38;5;8;49m \u001b[39;49m 5,880\u001b[38;5;8;49m \u001b[13;2H\u001b[23m\u001b[39;49m2025-03-10\u001b[13;13HWidgets\u001b[13;23HNorth\u001b[13;29H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 8,100\u001b[14;2H\u001b[23m2025-03-05\u001b[14;13HWidgets\u001b[14;23HEast\u001b[14;29H\u001b[3m 7,500\u001b[38;5;8;49m \u001b[15;2H\u001b[23m\u001b[39;49m2025-01-19\u001b[15;13HGadgets\u001b[15;23HWest\u001b[15;29H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 4,690\u001b[38;5;8;49m \u001b[16;2H\u001b[23m\u001b[39;49m2025-03-28\u001b[16;13HSprockets\u001b[16;23HSouth\u001b[16;29H\u001b[3m\u001b[38;5;8;49m \u001b[17;2H\u001b[23m\u001b[39;49m"]
[64.185124, "o", "2025-01-22\u001b[17;13HWidgets\u001b[17;23HNorth\u001b[17;29H\u001b[3m\u001b[38;5;8;49m \u001b[18;2H\u001b[23m\u001b[39;49m2025-01-30\u001b[18;13HGadgets\u001b[18;23HNorth\u001b[18;29H\u001b[3m\u001b[38;5;8;49m \u001b[19;2H\u001b[23m\u001b[39;49m2025-01-12\u001b[19;13HSprockets\u001b[19;23HNorth\u001b[19;29H\u001b[3m\u001b[38;5;8;49m \u001b[20;2H\u001b[23m\u001b[39;49m2025-02-15\u001b[20;13HGadgets\u001b[20;23HSouth\u001b[20;29H\u001b[3m\u001b[38;5;8;49m \u001b[21;2H\u001b[23m\u001b[39;49m2025-03-07\u001b[21;13HWidgets\u001b[21;23HSouth\u001b[21;29H\u001b[3m\u001b[38;5;8;49m \u001b[22;2H\u001b[23m\u001b[39;49m2025-01-31\u001b[22;13HGadgets\u001b[22;23HEast\u001b[22;29H\u001b[3m 5,180\u001b[38;5;8;49m \u001b[23;2H\u001b[23m\u001b[39;49m2025-01-27\u001b[23;13HSprockets\u001b[23;23HWest\u001b[23;29H\u001b[3m\u001b[38;5;8;49m "]
[64.185176, "o", " \u001b[24;2H\u001b[23m\u001b[39;49m2025-01-28\u001b[24;13HSprockets\u001b[24;23HSouth\u001b[24;29H\u001b[3m\u001b[38;5;8;49m \u001b[25;2H\u001b[23m\u001b[39;49m2025-03-03\u001b[25;13HWidgets\u001b[25;23HWest\u001b[25;29H\u001b[3m\u001b[38;5;8;49m \u001b[26;2H\u001b[23m\u001b[39;49m2025-03-19\u001b[26;13HGadgets\u001b[26;23HNorth\u001b[26;29H\u001b[3m\u001b[38;5;8;49m \u001b[27;2H\u001b[23m\u001b[39;49m2025-02-05\u001b[27;13HWidgets\u001b[27;23HNorth\u001b[27;29H\u001b[3m\u001b[38;5;8;49m \u001b[28;2H\u001b[23m\u001b[39;49m2025-02-22\u001b[28;13HSprockets\u001b[28;23HSouth\u001b[28;29H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 2,170\u001b[38;5;8;49m \u001b[29;2H\u001b[23m\u001b[39;49m2025-02-25\u001b[29;13HSprockets\u001b[29;23HNorth\u001b[29;29H\u001b[3m\u001b[38;5;8;49m "]
[64.185201, "o", "\u001b[30;2H\u001b[23m\u001b[39;49m2025-03-18\u001b[30;13HGadgets\u001b[30;23HSouth\u001b[30;29H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 4,760\u001b[38;5;8;49m \u001b[31;2H\u001b[23m\u001b[39;49m2025-02-20\u001b[31;13HSprockets\u001b[31;23HEast\u001b[31;29H\u001b[3m 2,940\u001b[38;5;8;49m \u001b[32;2H\u001b[23m\u001b[39;49m2025-02-24\u001b[32;13HSprockets\u001b[32;23HWest\u001b[32;29H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 2,520\u001b[38;5;8;49m \u001b[33;2H\u001b[23m\u001b[39;49m2025-01-14\u001b[33;13HGadgets\u001b[33;23HSouth\u001b[33;29H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 3,850\u001b[38;5;8;49m \u001b[34;2H\u001b[23m\u001b[39;49m2025-03-21\u001b[34;13HSprockets\u001b[34;23HWest\u001b[34;29H\u001b[3m\u001b[38;5;8;49m \u001b[36;59H\u001b[23m\u001b[1m\u001b[38;5;0;48;5;6mCol\u001b[37;10H\u001b[22m\u001b[38;5;0;48;5;5mCustomer → Col \u001b[37;29H \u001b[37;31H \u001b[37;110HDefault \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[64.293903, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[64.403428, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[64.513866, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[64.623157, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[64.734355, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[64.844972, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[64.953157, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[65.062288, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[65.171044, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[65.277506, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[65.385493, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[65.492312, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[65.604282, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[65.71478, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[65.827714, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[65.934953, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[66.041651, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[66.148548, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[66.256987, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[66.364621, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[66.474381, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[66.58384, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[66.710711, "o", "\u001b[36;48H\u001b[38;5;4;49m [Customer Col] \u001b[1m\u001b[38;5;0;48;5;6m [Date Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[66.818615, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[66.925978, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[67.034783, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[67.141116, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[67.247723, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[67.356797, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[67.464647, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[67.575, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[67.682954, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[67.789934, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[67.86314, "o", "\u001b[3;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m \u001b[3;32HCost\u001b[3;43H \u001b[3;104H \u001b[4;18H\u001b[24m Stark Enterprises\u001b[4m Soylent Ltd\u001b[24m Wonka Industries\u001b[4;66HCyberdyn\u001b[4;75H Syst\u001b[4;81Hms Acme Corp Oceanic Airlines\u001b[6;2H\u001b[38;5;6;48;5;237mGadgets North \u001b[22m\u001b[3m\u001b[38;5;8;48;5;237m \u001b[6;36H\u001b[1m\u001b[38;5;0;48;5;6m \u001b[6;48H\u001b[22m\u001b[38;5;8;48;5;237m \u001b[6;83H\u001b[38;5;15;48;5;237m 9,450\u001b[6;104H\u001b[38;5;8;48;5;237m \u001b[7;2H\u001b[23m\u001b[39;49m East \u001b[3m \u001b[7;30H10,850\u001b[38;5;8;49m \u001b[7;48H\u001b[39;49m 12,670\u001b[7;104H\u001b[38;5;8;49m \u001b[8;2H\u001b[23m\u001b[39;49m South \u001b[3m\u001b[38;5;8;49m \u001b[8;36H\u001b[39;49m 8,610\u001b[8;104H\u001b[38;5;8;49m \u001b[9;2H\u001b[23m\u001b[39;49m West \u001b[3m\u001b[38;5;8;49m \u001b[9;65H\u001b[39;49m \u001b[9;78H9,940\u001b[38;5;8;49m \u001b[39;49m 4,970\u001b[10;2H\u001b[23mWidgets North \u001b[3m\u001b[38;5;8;49m \u001b[10;59H \u001b[10;83H\u001b[39;49m 15,300\u001b[10;104H\u001b[38;5;8;49m \u001b[11;2H\u001b[23m\u001b[39;49m East \u001b["]
[67.863188, "o", "3m 14,580\u001b[11;48H 17,520\u001b[11;94H\u001b[38;5;8;49m \u001b[12;2H\u001b[23m\u001b[39;49m South \u001b[3m\u001b[38;5;8;49m \u001b[12;36H\u001b[39;49m 12,000\u001b[12;76H\u001b[38;5;8;49m \u001b[12;104H \u001b[13;2H\u001b[23m\u001b[39;49m West \u001b[3m\u001b[38;5;8;49m \u001b[13;65H\u001b[39;49m 12,600\u001b[13;93H \u001b[13;99H 13,980\u001b[14;2H\u001b[23mSprockets North \u001b[3m\u001b[38;5;8;49m \u001b[14;104H \u001b[15;2H\u001b[23m\u001b[39;49m East \u001b[3m 2,940\u001b[15;48H 3,360\u001b[15;76H\u001b[38;5;8;49m \u001b[15;104H \u001b[16;2H\u001b[23m\u001b[39;49m South \u001b[3m\u001b[38;5;8;49m \u001b[16;36H\u001b[39;49m 2,170\u001b[16;104H\u001b[38;5;8;49m \u001b[17;2H\u001b[23m\u001b[39;49m West \u001b[3m\u001b[38;5;8;49m \u001b[17;65H\u001b[39;49m 2,520\u001b[17;93H 5,040\u001b[18;2H\u001b[23m\u001b[38;5;8;49m──────────────────────────────────────────────────────────────"]
[67.863311, "o", "────────────────────────────────────────────────────────\u001b[19;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 22,780 33,550 25,060 24,750 23,990\u001b[20;2H\u001b[22m\u001b[39;49m \u001b[20;13H \u001b[20;23H \u001b[20;29H \u001b[21;2H \u001b[21;13H \u001b[21;23H \u001b[21;29H \u001b[22;2H \u001b[22;13H \u001b[22;23H \u001b[22;29H \u001b[23;2H \u001b[23;13H \u001b[23;23H \u001b[23;29H \u001b[24;2H \u001b[24;13H \u001b[24;23H \u001b[24;29H \u001b[25;2H \u001b[25;13H \u001b[25;23H \u001b[25;29H "]
[67.863477, "o", " \u001b[26;2H \u001b[26;13H \u001b[26;23H \u001b[26;29H \u001b[27;2H \u001b[27;13H \u001b[27;23H \u001b[27;29H \u001b[28;2H \u001b[28;13H \u001b[28;23H \u001b[28;29H \u001b[29;2H \u001b[29;13H \u001b[29;23H \u001b[29;29H \u001b[30;2H \u001b[30;13H \u001b[30;23H \u001b[30;29H \u001b[31;2H \u001b[31;13H \u001b[31;23H \u001b[31;29H \u001b[32;2H \u001b[32;13H \u001b[32;23H \u001b[32;29H \u001b[33;2H \u001b[33;13H \u001b[33;23H \u001b[33;29H "]
[67.86354, "o", " \u001b[34;2H \u001b[34;13H \u001b[34;23H \u001b[34;29H \u001b[36;71H\u001b[1m\u001b[38;5;0;48;5;6m·] \u001b[22m\u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;8;49m [Date_Month ·] \u001b[39;49m \u001b[37;10H\u001b[38;5;0;48;5;5mDate → None \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[67.967252, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[68.071254, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[68.175017, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[68.280402, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[68.384869, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[68.490994, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[68.594645, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[68.698504, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[68.802472, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[68.906976, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[69.010324, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[69.114855, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[69.218682, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[69.321925, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[69.425338, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[69.530655, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[69.635327, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[69.739063, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[69.835646, "o", "\u001b[36;64H\u001b[38;5;8;49m [Date ·] \u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[69.939257, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[70.042787, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[70.146403, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[70.221065, "o", "\u001b[36;74H\u001b[38;5;2;49m [Product Row] \u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[70.325462, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[70.429923, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[70.53483, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[70.63921, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[70.699314, "o", "\u001b[3;12H\u001b[1m\u001b[4m\u001b[38;5;3;49m \u001b[3;26HCost\u001b[3;32H \u001b[3;110H \u001b[4;12H\u001b[24m Stark\u001b[4;19HEnterpris\u001b[4;29Hs\u001b[4m Soylent Ltd\u001b[24m Wonka\u001b[4;49HIndustries Cyb\u001b[4;64Hrdyne Syst\u001b[4;75Hms Acme Corp Oceanic Airlines Umbrella Co\u001b[6;12H\u001b[22m\u001b[3m\u001b[38;5;15;48;5;237m 10,850\u001b[1m\u001b[38;5;0;48;5;6m \u001b[6;37H8,610\u001b[22m\u001b[38;5;15;48;5;237m 12,670 9,940 9,450\u001b[6;88H 4,970 4,270\u001b[7;2H\u001b[23m\u001b[39;49mWidgets\u001b[7;12H\u001b[3m \u001b[7;24H14,580 12,000 \u001b[7;53H17,520 12,600 15,300 13,980 9,660\u001b[8;2H\u001b[23mSprockets\u001b[8;12H\u001b[3m 2,940 \u001b[8;37H2,170\u001b[8;43H 3,360 2,520\u001b[8;87H 5,040 4,410\u001b[9;2H\u001b[23m\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────"]
[70.699374, "o", "────────────────────\u001b[10;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 22,780 33,550 25,060 24,750 23,990 18,340\u001b[11;12H\u001b[22m\u001b[39;49m \u001b[11;18H \u001b[12;12H \u001b[12;18H \u001b[13;12H \u001b[13;18H \u001b[14;2H \u001b[14;12H \u001b[14;18H \u001b[15;12H \u001b[15;18H \u001b[16;12H \u001b[16;18H \u001b[17;12H \u001b[17;18H \u001b[18;2H "]
[70.699547, "o", " \u001b[19;2H \u001b[36;98H\u001b[1m\u001b[38;5;0;48;5;6m·] \u001b[22m\u001b[38;5;8;49m [Date_Month ·] \u001b[39;49m \u001b[37;10H\u001b[38;5;0;48;5;5mRegion\u001b[37;17H→ None\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[70.801355, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[70.903566, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[71.006414, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[71.109107, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[71.211226, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[71.312839, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[71.415164, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[71.517285, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[71.618708, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[71.720755, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[71.82319, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[71.926319, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[72.028504, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[72.1316, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[72.234303, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[72.33685, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[72.438765, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[72.540175, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[72.635683, "o", "\u001b[36;89H\u001b[38;5;8;49m [Region ·] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL Region → None Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[72.737283, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[72.839113, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[72.941124, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[73.044152, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[73.146225, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[73.248372, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[73.350589, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[73.452793, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[73.555584, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[73.658217, "o", "\u001b[3;12H \u001b[1m\u001b[4m\u001b[38;5;3;49m Gadgets\u001b[24m Widgets Sprockets\u001b[22m\u001b[39;49m \u001b[4;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[5;2H\u001b[1m\u001b[38;5;6;48;5;237mCost Stark Enterprises \u001b[3m\u001b[38;5;0;48;5;6m 10,850\u001b[22m\u001b[38;5;15;48;5;237m 14,580 2,940\u001b[23m\u001b[39;48;5;237m \u001b[6;2H\u001b[39;49m Soylent Ltd \u001b[3m 8,610 12,000 2,170\u001b[23m \u001b[7;2H \u001b[7;10HWonka Industries \u001b[3m 12,670 17,520\u001b[7;49H3,360\u001b[23m \u001b[8;2H Cy"]
[73.658358, "o", "berdyne Systems \u001b[3m \u001b[8;31H9,940\u001b[8;37H 12,600\u001b[8;49H2,520\u001b[23m \u001b[9;2H Acme Corp \u001b[3m 9,450 15,300\u001b[38;5;8;49m \u001b[23m\u001b[39;49m \u001b[10;2H Oceanic Airlines \u001b[3m 4,970 13,980 5,040\u001b[23m \u001b[11;10HUmbrella\u001b[11;19HCo\u001b[11;28H\u001b[3m 4,270 9,660 4,410\u001b[12;10H\u001b[23mGlobex\u001b[12;17HInc\u001b[12;28H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 5,100 4,410\u001b[13;10H\u001b[23mInitech\u001b[13;28H\u001b[3m 6,930 6,600\u001b[38;5;8;49m \u001b[14;2H\u001b[23m\u001b[39;49mRevenue\u001b[14;10HStark\u001b[14;16HEnterprises\u001b[14;28H\u001b[3m 15,500 24,300 4,200\u001b[15;10H\u001b[23mSoylent\u001b[15;18HLtd\u001b[15;28H\u001b[3m 12,300 20,000 3,100\u001b[16;10H\u001b[23mWonka\u001b[16;16HIndustries\u001b[16;28H\u001b[3m 18,100 29,200 4,800\u001b[17;10H\u001b[23mCyberdyne\u001b[17;20HSystems\u001b[17;28H\u001b[3m 14,200 21,000 3,600\u001b[18;10H\u001b[23mAcme\u001b[18;15HCorp\u001b[18;28H\u001b[3m 13,500 25,500\u001b[38;5;8;49m "]
[73.658473, "o", " \u001b[19;10H\u001b[23m\u001b[39;49mOceanic\u001b[19;18HAirlines\u001b[19;28H\u001b[3m 7,100 23,300 7,200\u001b[20;10H\u001b[23mUmbrella\u001b[20;19HCo\u001b[20;28H\u001b[3m 6,100 16,100 6,300\u001b[21;10H\u001b[23mGlobex\u001b[21;17HInc\u001b[21;28H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 8,500 6,300\u001b[22;10H\u001b[23mInitech\u001b[22;28H\u001b[3m 9,900 11,000\u001b[38;5;8;49m \u001b[23;2H\u001b[23m\u001b[39;49mProfit\u001b[23;10HStark\u001b[23;16HEnterprises\u001b[23;28H\u001b[3m 4,650 9,720 1,260\u001b[24;10H\u001b[23mSoylent\u001b[24;18HLtd\u001b[24;28H\u001b[3m 3,690 8,000 930\u001b[25;10H\u001b[23mWonka\u001b[25;16HIndustries\u001b[25;28H\u001b[3m 5,430 11,680 1,440\u001b[26;10H\u001b[23mCyberdyne\u001b[26;20HSystems\u001b[26;28H\u001b[3m 4,260 8,400 1,080\u001b[27;10H\u001b[23mAcme\u001b[27;15HCorp\u001b[27;28H\u001b[3m 4,050 10,200\u001b[38;5;8;49m \u001b[28;10H\u001b[23m\u001b[39;49mOceanic\u001b[28;18HAirlines\u001b[28;28H\u001b[3m 2,130 9,320 2,160\u001b[29;10H\u001b[23mUmbrella\u001b[29;19HCo\u001b[29;28H\u001b[3m 1,830 6,440 1,890\u001b[30;10H\u001b[23mGlobex\u001b[30;17HInc\u001b[30;28H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 3,400 1,890\u001b[31;10H\u001b[23mInitech\u001b[31;28H\u001b[3m 2,970 4,400\u001b[38;5;8;49m \u001b[32;2H\u001b[23"]
[73.658556, "o", "m\u001b[39;49mMargin\u001b[32;10HStark\u001b[32;16HEnterprises\u001b[32;28H\u001b[3m 30 40 30\u001b[33;10H\u001b[23mSoylent\u001b[33;18HLtd\u001b[33;28H\u001b[3m 30 40 30\u001b[34;10H\u001b[23mWonka\u001b[34;16HIndustries\u001b[34;28H\u001b[3m 30 40 30\u001b[36;32H\u001b[23m\u001b[38;5;2;49m [_Measure Row] [Customer Row] \u001b[36;74H\u001b[38;5;4;49m [Product Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[73.759967, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[73.862561, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[73.965616, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[74.068047, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[74.170187, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[74.272638, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[74.374756, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[74.4771, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[74.579809, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[74.682278, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[74.784151, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[74.885701, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[74.988411, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[75.090938, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[75.193462, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[75.296878, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[75.398736, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[75.501676, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[75.604535, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[75.70834, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[75.811134, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[75.913363, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[76.015107, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[76.11667, "o", "\u001b[3;12H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost \u001b[4;2H\u001b[22m\u001b[24m\u001b[39;49m \u001b[1m\u001b[4m\u001b[38;5;3;49m Stark Enterprises\u001b[24m Soylent Ltd Wonka Industries Cyberdyne Systems Acme Corp Oceanic Airlines Umbrella Co\u001b[22m\u001b[39;49m \u001b[5;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets \u001b[3m\u001b[38;5;0;48;5;6m 10,850\u001b[22m\u001b[38;5;15;48;5;237m 8,610 12,670 9,940 9,450 4,970 4,270\u001b[23m\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49mWidgets\u001b[7;10H \u001b[3m 14,580 12,000 \u001b[7;49H 17,520 12,600 15,300 13,980 "]
[76.116905, "o", " 9,660\u001b[8;2H\u001b[23mSprockets \u001b[3m 2,940\u001b[8;31H \u001b[8;37H2,170 \u001b[8;49H 3,360 2,520\u001b[38;5;8;49m \u001b[39;49m 5,040 4,410\u001b[9;2H\u001b[23m\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[10;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 22,780 33,550 25,060 24,750 23,990 18,340\u001b[11;10H\u001b[22m\u001b[39;49m \u001b[11;19H \u001b[11;28H \u001b[12;10H \u001b[12;17H \u001b[12;28H \u001b[13;10H \u001b[13;28H \u001b[14;2H \u001b[14;10H \u001b[14;16H \u001b[14;28H \u001b[15;10H \u001b[15;18H \u001b[15;28H \u001b[16;10H \u001b[16;"]
[76.117215, "o", "16H \u001b[16;28H \u001b[17;10H \u001b[17;20H \u001b[17;28H \u001b[18;10H \u001b[18;15H \u001b[18;28H \u001b[19;10H \u001b[19;18H \u001b[19;28H \u001b[20;10H \u001b[20;19H \u001b[20;28H \u001b[21;10H \u001b[21;17H \u001b[21;28H \u001b[22;10H \u001b[22;28H \u001b[23;2H \u001b[23;10H \u001b[23;16H \u001b[23;28H \u001b[24;10H \u001b[24;18H \u001b[24;28H \u001b[25;10H \u001b[25;16H \u001b[25;28H \u001b[26;10H \u001b[26;20H \u001b[26;28H \u001b[27;10H \u001b[27;15H \u001b[27;28H \u001b[28;10H \u001b[28;18H \u001b[28;28H \u001b[29;10H \u001b[29;19H \u001b[29;28H \u001b[30;10H \u001b[30;17H \u001b[30;28H \u001b[31;10H \u001b[31;28H \u001b[32;2H \u001b[32;10H \u001b[32;16H \u001b[32;28H "]
[76.117371, "o", " \u001b[33;10H \u001b[33;18H \u001b[33;28H \u001b[34;10H \u001b[34;16H \u001b[34;28H \u001b[36;32H\u001b[38;5;4;49m [_Measure Col] [Customer Col] \u001b[36;74H\u001b[38;5;2;49m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[76.220162, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[76.322015, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[76.423613, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[76.525244, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[76.627242, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[76.728873, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[76.831933, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[76.934917, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[77.036315, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[77.13847, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[77.242733, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[77.345432, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[77.448448, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[77.550404, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[77.653764, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[77.756099, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[77.859408, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[77.961709, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[78.063238, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[78.165254, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[78.267493, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[78.369832, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[78.472128, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[78.574391, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[78.676144, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[78.777862, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[78.880914, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[78.983394, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[79.085396, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[79.188456, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[79.290135, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[79.391212, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[79.492775, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[79.595048, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[79.697259, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[79.800184, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[79.902419, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.005145, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.107077, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.209791, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.311753, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.348684, "o", "\u001b[37;1H\u001b[1m\u001b[38;5;3;48;5;235m:▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.450215, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.553719, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.656004, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.757524, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.8535, "o", "\u001b[37;2H\u001b[1m\u001b[38;5;3;48;5;235mq▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[80.955594, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[81.057237, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[81.159725, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[81.261826, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[81.363603, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[81.406014, "o", "\u001b[37;3H\u001b[1m\u001b[38;5;3;48;5;235m!▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[81.507994, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[81.609703, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[81.712177, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[81.798741, "o", "\u001b[?1049l\u001b[?25h"]
[81.80075, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"]
[81.842957, "o", "\u001b]0;~/git_repos/git.fiddlerwoaroof.com/u/edwlan/improvise\u0007"]
[81.985112, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J---\r\n(0) Mac:edwlan--s059 ~gf/u/edwlan/improvise \u001b[30m\u001b[35m\u001b[39mgit\u001b[35m\u001b[33m->\u001b[35m\u001b[32mmain\u001b[35m\u001b[39m\u001b[00m 2026-04-09 15:08:28\r\n16027:% \u001b[K"]
[81.985205, "o", "\u001b[?2004h"]
[82.780045, "o", "\u001b[?2004l\r\r\n"]
{"version": 2, "width": 120, "height": 37, "idle_time_limit": 2.0, "title": "improvise \u2014 formulas"}
[0.014548, "o", "\u001b[?1049h"]
[0.017833, "o", "\u001b[1;1H\u001b[1m\u001b[38;5;0;48;5;4m improvise \u00b7 Acme Sales Demo (demo.improv) ?:help :q quit \u001b[2;1H\u001b[22m\u001b[39;49m\u250c\u001b[2;3HView:\u001b[2;9HDefault\u001b[2;17H\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u001b[38;5;8;49m\u250c Categories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u001b[3;1H\u001b[39;49m\u2502\u001b[38;5;5;49m [Customer = Stark Enterprises] \u001b[3;88H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[3;92H_Index\u001b[3;99H(0)\u001b[3;104H\u001b[38;5;8;49m [None \u2205]\u001b[3;120H\u2502\u001b[4;1H\u001b[39;49m\u2502\u001b[4;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost \u001b[24m Revenue Profit \u001b[4;88H\u001b[22m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[4;92H_Dim\u001b[4;97H(0)\u001b[4;102H\u001b[38;5;8;49m [None \u2205]\u001b[4;120H\u2502\u001b[5;1H\u001b[39;49m\u2502\u001b[5;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-02 2025-03 2025-01 2025-02 2025-03 2025-01 2025-02\u001b[5;88H\u001b[22m\u001b[39"]
[0.017866, "o", ";49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[5;92H_Measure\u001b[5;101H(3)\u001b[5;106H\u001b[38;5;4;49m [Col \u2194]\u001b[5;120H\u001b[38;5;8;49m\u2502\u001b[6;1H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[6;92HCustomer\u001b[6;101H(9)\u001b[6;106H\u001b[38;5;5;49m [Page \u2630\u001b[6;115H]\u001b[6;120H\u001b[38;5;8;49m\u2502\u001b[7;1H\u001b[39;49m\u2502\u001b[1m\u001b[38;5;6;48;5;237mGadgets North \u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[23m\u001b[39;48;5;237m \u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[7;92HDate\u001b[7;97H(40)\u001b[7;103H\u001b[38;5;8;49m [None \u2205]\u001b[7;120H\u2502\u001b[8;1H\u001b[39;49m\u2502\u001b[8;12HEast\u001b[8;18H\u001b[3m 5,180\u001b[38;5;8;49m \u001b[39;49m 5,670 7,400\u001b[38;5;8;49m \u001b[39;49m 8,100 2,220\u001b[38;5;8;49m \u001b[8;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[8;92HP"]
[0.017873, "o", "roduct\u001b[8;100H(3)\u001b[8;105H\u001b[38;5;2;49m [Row \u2195]\u001b[8;120H\u001b[38;5;8;49m\u2502\u001b[9;1H\u001b[39;49m\u2502\u001b[9;12HSouth\u001b[9;18H\u001b[3m\u001b[38;5;8;49m \u001b[9;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[9;92HRegion\u001b[9;99H(4)\u001b[9;104H\u001b[38;5;2;49m [Row \u2195]\u001b[9;120H\u001b[38;5;8;49m\u2502\u001b[10;1H\u001b[39;49m\u2502\u001b[10;12HWest\u001b[10;18H\u001b[3m\u001b[38;5;8;49m \u001b[10;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[10;92HDate_Month\u001b[10;103H(3)\u001b[10;108H\u001b[38;5;4;49m [Col \u2194]\u001b[10;120H\u001b[38;5;8;49m\u2502\u001b[11;1H\u001b[39;49m\u2502Widgets\u001b[11;12HNorth\u001b[11;18H\u001b[3m\u001b[38;5;8;49m \u001b[11;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[11;120H\u2502\u001b[12;1H\u001b[39;49m\u2502\u001b[12;12HEast\u001b[12;18H\u001b[3m 7,080\u001b[38;5;8;49m \u001b[39;49m 7,500 11,800\u001b[38;5;8;49m \u001b[39;49m 12,500 4,720\u001b[38;5;8;49m \u001b[12;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[12;120H\u2502\u001b[13;1H\u001b[39;49m\u2502\u001b[13;12HSouth\u001b[13;18H\u001b[3m\u001b[38;5;8;49m "]
[0.017922, "o", " \u001b[13;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[13;120H\u2502\u001b[14;1H\u001b[39;49m\u2502\u001b[14;12HWest\u001b[14;18H\u001b[3m\u001b[38;5;8;49m \u001b[14;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[14;120H\u2502\u001b[15;1H\u001b[39;49m\u2502Sprockets\u001b[15;12HNorth\u001b[15;18H\u001b[3m\u001b[38;5;8;49m \u001b[15;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[15;120H\u2502\u001b[16;1H\u001b[39;49m\u2502\u001b[16;12HEast\u001b[16;18H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 2,940\u001b[38;5;8;49m \u001b[39;49m 4,200\u001b[38;5;8;49m \u001b[39;49m 1,260\u001b[16;88H\u001b[23m\u2502\u001b[38;5;8;49m\u2502\u001b[16;120H\u2502\u001b[17;1H\u001b[39;49m\u2502\u001b[17;12HSouth\u001b[17;18H\u001b[3m\u001b[38;5;8;49m \u001b[17;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[17;120H\u2502\u001b[18;1H\u001b[39;49m\u2502\u001b[18;12HWest\u001b[18;18H\u001b[3m\u001b[38;5;8;49m \u001b[18;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[18;120H\u2502\u001b[19;1H\u001b[39;49m\u2502\u001b[38;5;8;49m"]
[0.017953, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[19;120H\u2502\u001b[20;1H\u001b[39;49m\u2502\u001b[1m\u001b[38;5;3;49mTotal 12,260 2,940 13,170 19,200 4,200 20,600 6,940 1,260\u001b[20;88H\u001b[22m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[20;120H\u2502\u001b[21;1H\u001b[39;49m\u2502\u001b[21;88H\u2502\u001b[38;5;8;49m\u2502\u001b[21;120H\u2502\u001b[22;1H\u001b[39;49m\u2502\u001b[22;88H\u2502\u001b[38;5;8;49m\u2502\u001b[22;120H\u2502\u001b[23;1H\u001b[39;49m\u2502\u001b[23;88H\u2502\u001b[38;5;8;49m\u2502\u001b[23;120H\u2502\u001b[24;1H\u001b[39;49m\u2502\u001b[24;88H\u2502\u001b[38;5;8;49m\u2502\u001b[24;120H\u2502\u001b[25;1H\u001b[39;49m\u2502\u001b[25;88H\u2502\u001b[38;5;8;49m\u2502\u001b[25;120H\u2502\u001b[26;1H\u001b[39;49m\u2502\u001b[26;88H\u2502\u001b[38;5;8;49m\u2502\u001b[26;120H\u2502\u001b[27;1H\u001b[39;49m\u2502\u001b[27;88H\u2502\u001b[38;5;8;49m\u2502\u001b[27;120H\u2502\u001b[28;1H\u001b[39;49m\u2502\u001b[28;88H\u2502\u001b[38;5;8;49m\u2502\u001b[28;120H\u2502\u001b[29;1H\u001b[39;49m\u2502\u001b[29;88H\u2502\u001b[38;5;8;49m\u2502\u001b[29;120H\u2502\u001b[30;1H\u001b[39;49m\u2502\u001b[30;88H\u2502\u001b[38;5;8;49m\u2502\u001b[30;120H\u2502\u001b["]
[0.017989, "o", "31;1H\u001b[39;49m\u2502\u001b[31;88H\u2502\u001b[38;5;8;49m\u2502\u001b[31;120H\u2502\u001b[32;1H\u001b[39;49m\u2502\u001b[32;88H\u2502\u001b[38;5;8;49m\u2502\u001b[32;120H\u2502\u001b[33;1H\u001b[39;49m\u2502\u001b[33;88H\u2502\u001b[38;5;8;49m\u2502\u001b[33;120H\u2502\u001b[34;1H\u001b[39;49m\u2502\u001b[34;88H\u2502\u001b[38;5;8;49m\u2502\u001b[34;120H\u2502\u001b[35;1H\u001b[39;49m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[38;5;8;49m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[36;1H\u001b[38;5;7;49m Tiles: \u001b[36;10H\u001b[38;5;8;49m [_Index \u00b7] [_Dim \u00b7] \u001b[38;5;4;49m [_Measure Col] \u001b[38;5;5;49m [Customer Pag] \u001b[38;5;8;49m [Date \u00b7] \u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;4;49m [Date_Month Col] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL hjkl\u2190\u2191\u2192\u2193:nav ai:edit R:records P:prune F:formulas C:categories V:views T:tiles [:prev-page ]:next-pag\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.120799, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.223951, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.326843, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.429589, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.532664, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.63654, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.738908, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.842847, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.94676, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.050559, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.15455, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.258358, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.362249, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.466074, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.569866, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.607399, "o", "\u001b[2;89H\u001b[38;5;3;49m\u250c Formulas [n]ew [d]elete \u2500\u2500\u2500\u2500\u2500\u2510\u001b[3;89H\u2502\u001b[1m\u001b[38;5;0;48;5;3m Profit = Revenue - Cost\u001b[3;120H\u001b[22m\u001b[38;5;3;49m\u2502\u001b[4;89H\u2502\u001b[39;49m \u001b[4;92H \u001b[4;97H \u001b[4;102H \u001b[4;120H\u001b[38;5;3;49m\u2502\u001b[5;89H\u2502\u001b[39;49m \u001b[5;92H \u001b[5;101H \u001b[5;106H \u001b[5;120H\u001b[38;5;3;49m\u2502\u001b[6;89H\u2502\u001b[39;49m \u001b[6;92H \u001b[6;101H \u001b[6;106H \u001b[6;115H \u001b[6;120H\u001b[38;5;3;49m\u2502\u001b[7;89H\u2502\u001b[39;49m \u001b[7;92H \u001b[7;97H \u001b[7;103H \u001b[7;120H\u001b[38;5;3;49m\u2502\u001b[8;89H\u2502\u001b[39;49m \u001b[8;92H \u001b[8;100H \u001b[8;105H \u001b[8;120H\u001b[38;5;3;49m\u2502\u001b[9;89H\u2502\u001b[39;49m \u001b[9;92H \u001b[9;99H \u001b[9;104H \u001b[9;120H\u001b[38;5;3;49m\u2502\u001b[10;89H\u2502\u001b[39;49m \u001b[10;92H \u001b[10;103H \u001b[10;108H \u001b[10;120H\u001b[38;5;3;49m\u2502\u001b[11;89H\u2502\u001b[11;120H\u2502\u001b[12;89H\u2502\u001b[12;120H\u2502\u001b[13;89H\u2502\u001b[13;120H\u2502\u001b[14;89H\u2502\u001b[14;120H\u2502\u001b[15;89H\u2502\u001b[15;120H\u2502\u001b[16;89H\u2502\u001b[16;120H\u2502\u001b[17;89H\u2502\u001b[17;120H\u2502\u001b[18;89H\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[19;89H\u001b[38;5;8;"]
[1.607453, "o", "49m\u250c Categories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u001b[20;90H\u001b[39;49m\u25b6\u001b[20;92H_Index\u001b[20;99H(0)\u001b[20;104H\u001b[38;5;8;49m [None \u2205]\u001b[21;90H\u001b[39;49m\u25b6\u001b[21;92H_Dim\u001b[21;97H(0)\u001b[21;102H\u001b[38;5;8;49m [None \u2205]\u001b[22;90H\u001b[39;49m\u25b6\u001b[22;92H_Measure\u001b[22;101H(3)\u001b[22;106H\u001b[38;5;4;49m [Col \u2194]\u001b[23;90H\u001b[39;49m\u25b6\u001b[23;92HCustomer\u001b[23;101H(9)\u001b[23;106H\u001b[38;5;5;49m [Page \u2630\u001b[23;115H]\u001b[24;90H\u001b[39;49m\u25b6\u001b[24;92HDate\u001b[24;97H(40)\u001b[24;103H\u001b[38;5;8;49m [None \u2205]\u001b[25;90H\u001b[39;49m\u25b6\u001b[25;92HProduct\u001b[25;100H(3)\u001b[25;105H\u001b[38;5;2;49m [Row \u2195]\u001b[26;90H\u001b[39;49m\u25b6\u001b[26;92HRegion\u001b[26;99H(4)\u001b[26;104H\u001b[38;5;2;49m [Row \u2195]\u001b[27;90H\u001b[39;49m\u25b6\u001b[27;92HDate_Month\u001b[27;103H(3)\u001b[27;108H\u001b[38;5;4;49m [Col \u2194]\u001b[37;3H\u001b[38;5;0;48;5;8mF\u001b[37;7HU\u001b[37;9HAS ano:new Del/d\u001b[37;28Hdelete jk\u2191\u2193:nav\u001b[37;45H Esc/Tab:back \u001b[37;66H \u001b[37;80H \u001b[37;89H \u001b[37;98H Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.709336, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.81078, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.913634, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.017181, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.120812, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.224672, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.328579, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.432541, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.536434, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.638767, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.742846, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.846891, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.950429, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.054209, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.158105, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.262011, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.365848, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.469167, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.573205, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.677078, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.780906, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.884564, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.988459, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.014648, "o", "\u001b[2;89H\u001b[38;5;8;49m\u250c Formulas [n]ew [d]elete \u2500\u2500\u2500\u2500\u2500\u2510\u001b[3;89H\u2502\u001b[38;5;2;49m Profit = Revenue - Cost\u001b[3;120H\u001b[38;5;8;49m\u2502\u001b[4;89H\u2502\u001b[4;120H\u2502\u001b[5;89H\u2502\u001b[5;120H\u2502\u001b[6;89H\u2502\u001b[6;120H\u2502\u001b[7;89H\u2502\u001b[7;120H\u2502\u001b[8;89H\u2502\u001b[8;120H\u2502\u001b[9;89H\u2502\u001b[9;120H\u2502\u001b[10;89H\u2502\u001b[10;120H\u2502\u001b[11;89H\u2502\u001b[11;120H\u2502\u001b[12;89H\u2502\u001b[12;120H\u2502\u001b[13;89H\u2502\u001b[13;120H\u2502\u001b[14;89H\u2502\u001b[14;120H\u2502\u001b[15;89H\u2502\u001b[15;120H\u2502\u001b[16;89H\u2502\u001b[16;120H\u2502\u001b[17;89H\u2502\u001b[17;120H\u2502\u001b[18;89H\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[37;3H\u001b[38;5;0;48;5;8mN\u001b[37;7HA\u001b[37;9H hjkl\u2190\u2191\u2192\u2193:nav ai\u001b[37;28Hedit R:records \u001b[37;45HP:prune F:formulas\u001b[37;66HC:categories\u001b[37;80HV:views\u001b[37;89HT:tiles\u001b[37;98H[:prev-page ]:next-pag\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.117617, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.22097, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.323673, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.424523, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.527237, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.629958, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.732688, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.835783, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.936791, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.013824, "o", "\u001b[36;10H\u001b[1m\u001b[38;5;0;48;5;6m [_Index \u00b7] \u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;5m TILES hl\u2190\u2192:select Enter/Space:cycle r:row c:col p:page Esc/Tab:back Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.116492, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.219167, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.321881, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.424669, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.52738, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.63015, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.732965, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.835763, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.938557, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.023889, "o", "\u001b[36;10H\u001b[38;5;8;49m [_Index \u00b7] \u001b[1m\u001b[38;5;0;48;5;6m [_Dim \u00b7] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.126643, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.229444, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.332227, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.384077, "o", "\u001b[36;22H\u001b[38;5;8;49m [_Dim \u00b7] \u001b[1m\u001b[38;5;0;48;5;6m [_Measure Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.486869, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.589656, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.692469, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.74409, "o", "\u001b[36;32H\u001b[38;5;4;49m [_Measure Col] \u001b[1m\u001b[38;5;0;48;5;6m [Customer Pag] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.846867, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.949624, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.05235, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.103954, "o", "\u001b[36;48H\u001b[38;5;5;49m [Customer Pag] \u001b[1m\u001b[38;5;0;48;5;6m [Date \u00b7] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.206645, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.309316, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.411998, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.464009, "o", "\u001b[36;64H\u001b[38;5;8;49m [Date \u00b7] \u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.566754, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.669433, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.772238, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.824496, "o", "\u001b[36;74H\u001b[38;5;2;49m [Product Row] \u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.928486, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.032516, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.136284, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.186709, "o", "\u001b[36;89H\u001b[38;5;2;49m [Region Row] \u001b[1m\u001b[38;5;0;48;5;6m [Date_Month Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.290586, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.393388, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.497385, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.545994, "o", "\u001b[1;47H\u001b[1m\u001b[38;5;0;48;5;4m[+]\u001b[4;18H\u001b[22m\u001b[39;49m \u001b[4;29H\u001b[1m\u001b[4m\u001b[38;5;3;49mCost\u001b[24m Revenue Profit\u001b[22m\u001b[39;49m \u001b[5;2H\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets North 2025-01 \u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[23m\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49m 2025-02 \u001b[3m\u001b[38;5;8;49m \u001b[23m\u001b[39;49m \u001b[8;12H \u001b[8;18H2025-03 \u001b[8;34H\u001b[3m\u001b[38;5;8;49m \u001b[23m\u001b[39;49m \u001b[9;12HEas\u001b[9;16H \u001b[9;18H2025-01 \u001b[3m 5,180 7,400 2,220\u001b[23m \u001b[10;12H \u001b[10;18H2025-02 \u001b[10;48H \u001b[11;2H "]
[8.546048, "o", " \u001b[11;12H \u001b[11;18H2025-03 \u001b[3m 5,670 8,100 2,430\u001b[23m \u001b[12;12HSou\u001b[12;16Hh\u001b[12;18H2025-01 \u001b[12;34H\u001b[3m\u001b[38;5;8;49m \u001b[23m\u001b[39;49m \u001b[13;12H \u001b[13;18H2025-02 \u001b[13;48H \u001b[14;12H \u001b[14;18H2025-03 \u001b[14;48H \u001b[15;2H \u001b[15;12HWes\u001b[15;16H \u001b[15;18H2025-01 \u001b[15;48H \u001b[16;12H \u001b[16;18H2025-02 \u001b[3m\u001b[38;5;8;49m \u001b[16;48H\u001b[23m\u001b[39;49m \u001b[17;12H \u001b[17;18H2025-03 \u001b[17;48H \u001b[18;2HWidgets\u001b[18;12HNor\u001b[18;16Hh\u001b[18;18H2025-01 \u001b[18;48H \u001b[19;2H 2025-02 \u001b[3m\u001b[38;5;8;49m \u001b[23m\u001b[39;49m \u001b[20;2H 2025-03 \u001b[3m\u001b[38;5;8;49m \u001b[23m\u001b[39;49m \u001b[21;12HEast\u001b[21;18H2025-01\u001b[21;26H\u001b[3m 7,080 11,800 4,720"]
[8.546111, "o", "\u001b[22;18H\u001b[23m2025-02\u001b[22;26H\u001b[3m\u001b[38;5;8;49m \u001b[23;18H\u001b[23m\u001b[39;49m2025-03\u001b[23;26H\u001b[3m 7,500 12,500 5,000\u001b[24;12H\u001b[23mSouth\u001b[24;18H2025-01\u001b[24;26H\u001b[3m\u001b[38;5;8;49m \u001b[25;18H\u001b[23m\u001b[39;49m2025-02\u001b[25;26H\u001b[3m\u001b[38;5;8;49m \u001b[26;18H\u001b[23m\u001b[39;49m2025-03\u001b[26;26H\u001b[3m\u001b[38;5;8;49m \u001b[27;12H\u001b[23m\u001b[39;49mWest\u001b[27;18H2025-01\u001b[27;26H\u001b[3m\u001b[38;5;8;49m \u001b[27;108H\u001b[23m\u001b[38;5;2;49m [Row \u2195]\u001b[28;18H\u001b[39;49m2025-02\u001b[28;26H\u001b[3m\u001b[38;5;8;49m \u001b[29;18H\u001b[23m\u001b[39;49m2025-03\u001b[29;26H\u001b[3m\u001b[38;5;8;49m \u001b[30;2H\u001b[23m\u001b[39;49mSprockets\u001b[30;12HNorth\u001b[30;18H2025-01\u001b[30;26H\u001b[3m\u001b[38;5;8;49m \u001b[31;18H\u001b[23m\u001b[39;49m2025-02\u001b[31;26H\u001b[3m\u001b[38;5;8;49m \u001b[32;18H\u001b[23m\u001b[39;49m2025-03\u001b[32;26H\u001b[3m\u001b[38;5;8;49m \u001b[33;12H\u001b[23m\u001b[39;49mEast\u001b[33;18H2025-01\u001b[33;26H\u001b[3m\u001b[38;5;8;49m \u001b[34;18H\u001b[23m\u001b[39;49m2025-02\u001b[34;26H\u001b[3m 2,940 4,200 1,260\u001b[36;"]
[8.546122, "o", "116H\u001b[23m\u001b[1m\u001b[38;5;0;48;5;6mR\u001b[36;118Hw\u001b[37;10H\u001b[22m\u001b[38;5;0;48;5;5mDate_Month \u2192\u001b[37;23HRow \u001b[37;42H \u001b[37;49H \u001b[37;56H \u001b[37;64H \u001b[37;108H Default\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.650023, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.75393, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.85762, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.961581, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.064973, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.168668, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.27269, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.375364, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.478076, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.581333, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.684849, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.788637, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.892466, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.99636, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.099546, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.202229, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.304929, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.407612, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.5103, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.613091, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.715814, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.818501, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.921187, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.023874, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.126548, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.145769, "o", "\u001b[36;103H\u001b[38;5;2;49m [Date_Month Row] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL Date_Month \u2192 Row Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.248551, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.351332, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.454116, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.556881, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.659673, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.762462, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.865224, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.967972, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.070761, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.173486, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.276277, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.37898, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.481068, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.583793, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.686495, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.7892, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.891901, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.994604, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.0958, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.198485, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.30126, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.403943, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.506636, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
+99 -210
View File
@@ -1,210 +1,99 @@
{"version": 2, "width": 120, "height": 37, "timestamp": 1775770896, "idle_time_limit": 2.0, "env": {"SHELL": "/bin/zsh", "TERM": "screen-256color"}}
[0.236073, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"]
[0.243595, "o", "\u001b]0;~/git_repos/git.fiddlerwoaroof.com/u/edwlan/improvise\u0007"]
[0.36932, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J---\r\n(0) Mac:edwlan--s059 ~gf/u/edwlan/improvise \u001b[30m\u001b[35m\u001b[39mgit\u001b[35m\u001b[33m->\u001b[35m\u001b[32mmain\u001b[35m\u001b[39m\u001b[00m 2026-04-09 14:41:37\r\n16023:% \u001b[K"]
[0.369422, "o", "\u001b[?2004h"]
[6.04003, "o", "\u001b[3m./target/release/improvise import examples/demo.csv\u001b[23m"]
[6.780344, "o", "\u001b[51D\u001b[23m.\u001b[23m/\u001b[23mt\u001b[23ma\u001b[23mr\u001b[23mg\u001b[23me\u001b[23mt\u001b[23m/\u001b[23mr\u001b[23me\u001b[23ml\u001b[23me\u001b[23ma\u001b[23ms\u001b[23me\u001b[23m/\u001b[23mi\u001b[23mm\u001b[23mp\u001b[23mr\u001b[23mo\u001b[23mv\u001b[23mi\u001b[23ms\u001b[23me\u001b[23m \u001b[23mi\u001b[23mm\u001b[23mp\u001b[23mo\u001b[23mr\u001b[23mt\u001b[23m \u001b[23me\u001b[23mx\u001b[23ma\u001b[23mm\u001b[23mp\u001b[23ml\u001b[23me\u001b[23ms\u001b[23m/\u001b[23md\u001b[23me\u001b[23mm\u001b[23mo\u001b[23m.\u001b[23mc\u001b[23ms\u001b[23mv"]
[6.780389, "o", "\u001b[?2004l\r\r\n"]
[6.781567, "o", "\u001b]0;./target/release/improvise import examples/demo.csv\u0007\u001b[2 q"]
[6.824759, "o", "\u001b[?1049h"]
[6.825908, "o", "\u001b[1;1H\u001b[1m\u001b[38;5;0;48;5;4m improvise · New Model ?:help :q quit \u001b[2;1H\u001b[22m\u001b[39;49m┌\u001b[2;3HView:\u001b[2;9HDefault\u001b[2;17H───────────────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[3;1H│\u001b[3;6H\u001b[1m\u001b[4m\u001b[38;5;3;49m Value\u001b[3;120H\u001b[22m\u001b[24m\u001b[39;49m│\u001b[4;1H│\u001b[38;5;8;49m───────────────────\u001b[38;5;5;49m┌ Import Wizard — Review Fields ───────────────────────────────────────────────┐\u001b[38;5;8;49m───────────────────\u001b[39;49m│\u001b[5;1H│\u001b[5;21H\u001b[38;5;5;49m│\u001b[38;5;3;49mReview field proposals (Space toggle, c cycle kin"]
[6.825948, "o", "d):\u001b[5;100H\u001b[38;5;5;49m│\u001b[5;120H\u001b[39;49m│\u001b[6;1H│\u001b[6;21H\u001b[38;5;5;49m│\u001b[4m\u001b[38;5;7;49m Field Kind Accept\u001b[6;100H\u001b[24m\u001b[38;5;5;49m│\u001b[6;120H\u001b[39;49m│\u001b[7;1H│\u001b[7;21H\u001b[38;5;5;49m│\u001b[1m\u001b[38;5;0;48;5;6m Cost Measure (numeric) [✓]\u001b[7;100H\u001b[22m\u001b[38;5;5;49m│\u001b[7;120H\u001b[39;49m│\u001b[8;1H│\u001b[8;21H\u001b[38;5;5;49m│\u001b[38;5;2;49m Customer Category (dimension) [✓]\u001b[8;100H\u001b[38;5;5;49m│\u001b[8;120H\u001b[39;49m│\u001b[9;1H│\u001b[9;21H\u001b[38;5;5;49m│ Date Time Category [✓]\u001b[9;100H│\u001b[9;120H\u001b[39;49m│\u001b[10;1H│\u001b[10;21H\u001b[38;5;5;49m│\u001b[38;5;2;49m Product Category (dimension) [✓]\u001b[10;100H\u001b[38;5;5;49m│\u001b[10;120H\u001b[39;49m│\u001b[11;1H│\u001b[11;21H\u001b[38;5;5;49m│\u001b[38;5;2;49m Region Category (dimension) [✓]\u001b[11;100H\u001b[38;5;5;49m│\u001b[11;120H\u001b[39;49m│\u001b[12;1H│\u001b[12;21H\u001b[38;5;5;49m│\u001b[38;5;6;49m Revenue Measure (numeric) [✓]\u001b[12;100H\u001b[38;5;5;49m│\u001b[12;120H\u001b[39;49m│\u001b[13;1H│\u001b[13;2"]
[6.826095, "o", "1H\u001b[38;5;5;49m│\u001b[13;100H│\u001b[13;120H\u001b[39;49m│\u001b[14;1H│\u001b[14;21H\u001b[38;5;5;49m│\u001b[14;100H│\u001b[14;120H\u001b[39;49m│\u001b[15;1H│\u001b[15;21H\u001b[38;5;5;49m│\u001b[15;100H│\u001b[15;120H\u001b[39;49m│\u001b[16;1H│\u001b[16;21H\u001b[38;5;5;49m│\u001b[16;100H│\u001b[16;120H\u001b[39;49m│\u001b[17;1H│\u001b[17;21H\u001b[38;5;5;49m│\u001b[17;100H│\u001b[17;120H\u001b[39;49m│\u001b[18;1H│\u001b[18;21H\u001b[38;5;5;49m│\u001b[18;100H│\u001b[18;120H\u001b[39;49m│\u001b[19;1H│\u001b[19;21H\u001b[38;5;5;49m│\u001b[19;100H│\u001b[19;120H\u001b[39;49m│\u001b[20;1H│\u001b[20;21H\u001b[38;5;5;49m│\u001b[20;100H│\u001b[20;120H\u001b[39;49m│\u001b[21;1H│\u001b[21;21H\u001b[38;5;5;49m│\u001b[21;100H│\u001b[21;120H\u001b[39;49m│\u001b[22;1H│\u001b[22;21H\u001b[38;5;5;49m│\u001b[22;100H│\u001b[22;120H\u001b[39;49m│\u001b[23;1H│\u001b[23;21H\u001b[38;5;5;49m│\u001b[23;100H│\u001b[23;120H\u001b[39;49m│\u001b[24;1H│\u001b[24;21H\u001b[38;5;5;49m│\u001b[24;100H│\u001b[24;120H\u001b[39;49m│\u001b[25;1H│\u001b[25;21H\u001b[38;5;5;49m│\u001b[25;100H│\u001b[25;120H\u001b[39;49m│\u001b[26;1H│\u001b[26;21H\u001b[38;5;5;49m│\u001b[26;100H│\u001b[26;120H\u001b[39;49m│\u001b[27;1H│\u001b[27;21H\u001b[38;5;5;49m│\u001b[27;100H│\u001b[27;120H\u001b[39;49m│\u001b[28;1H│\u001b[28;21H\u001b[38;5;5;49m│\u001b[28;100H│\u001b[28;120H\u001b[39;49m│"]
[6.826171, "o", "\u001b[29;1H│\u001b[29;21H\u001b[38;5;5;49m│\u001b[29;100H│\u001b[29;120H\u001b[39;49m│\u001b[30;1H│\u001b[30;21H\u001b[38;5;5;49m│\u001b[30;100H│\u001b[30;120H\u001b[39;49m│\u001b[31;1H│\u001b[31;21H\u001b[38;5;5;49m│\u001b[31;100H│\u001b[31;120H\u001b[39;49m│\u001b[32;1H│\u001b[32;21H\u001b[38;5;5;49m│\u001b[38;5;8;49mEnter: next Space: toggle c: cycle kind Esc: cancel\u001b[32;100H\u001b[38;5;5;49m│\u001b[32;120H\u001b[39;49m│\u001b[33;1H│\u001b[33;21H\u001b[38;5;5;49m└──────────────────────────────────────────────────────────────────────────────┘\u001b[33;120H\u001b[39;49m│\u001b[34;1H│\u001b[34;120H│\u001b[35;1H└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[36;1H"]
[6.826265, "o", "\u001b[38;5;7;49m Tiles: \u001b[36;10H\u001b[38;5;2;49m [_Index Row] \u001b[38;5;4;49m [_Dim Col] \u001b[38;5;8;49m [_Measure ·] Ctrl+↑↓←→ to move tiles\u001b[37;1H\u001b[38;5;0;48;5;8m IMPORT Space:toggle c:cycle Enter:next Esc:cancel Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.927868, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.028723, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.130263, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.231693, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.333159, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.433716, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.535238, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.636386, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.73786, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.839624, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.941139, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.04274, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.144036, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.245449, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.346985, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.448858, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.550336, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.576166, "o", "\u001b[4;39H\u001b[38;5;5;49mDate Components \u001b[5;22H\u001b[38;5;3;49mS\u001b[5;24Hlect\u001b[5;29Hdate component\u001b[5;45Hto extract (Spa\u001b[5;61He toggle):\u001b[39;49m \u001b[6;22H\u001b[1m\u001b[38;5;5;49m Date (format: %Y-%m-%d)\u001b[22m\u001b[39;49m \u001b[7;24H\u001b[1m\u001b[38;5;0;48;5;6m [ ]\u001b[7;30HYear\u001b[22m\u001b[39;49m \u001b[8;22H\u001b[38;5;8;49m [ ] Month\u001b[39;49m \u001b[9;22H\u001b[38;5;8;49m [ ] Quarter\u001b[39;49m \u001b[10;22H \u001b[11;22H \u001b[12;22H \u001b[32;22H\u001b[38;5;8;49mSpace\u001b[32;29Htoggle Enter: next\u001b[32;50HEsc: \u001b[32;56Hancel\u001b[39;49m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.677413, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.778735, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.880265, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.981784, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.083632, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.185292, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.286565, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.388026, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.489709, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.591377, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.692806, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.794433, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.895905, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.948598, "o", "\u001b[4;39H\u001b[38;5;5;49mFormulas ───────\u001b[5;22H\u001b[38;5;3;49mD\u001b[5;24Hfine\u001b[5;29Hformulas (optional):\u001b[39;49m \u001b[6;22H\u001b[38;5;8;49m (no formulas yet)\u001b[39;49m \u001b[7;22H \u001b[8;22H\u001b[38;5;8;49mExamples:\u001b[39;49m \u001b[9;24H\u001b[38;5;8;49mDiff = Cos\u001b[9;35H - Revenue\u001b[10;22H Total = SUM(Cost)\u001b[11;22H Ratio = Cost / Revenue\u001b[32;22Hn: n\u001b[32;27Hw\u001b[32;29Hf\u001b[32;31Hrmula\u001b[32;37H d: delete \u001b[32;49HEnter: next Esc: cancel\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.050037, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.151224, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.252897, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.354645, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.456403, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.558415, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.659907, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.761363, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.862866, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.964162, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.06573, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.166744, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.267259, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.368721, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.382289, "o", "\u001b[4;39H\u001b[38;5;5;49mName Model \u001b[5;22H\u001b[38;5;3;49mModel name:\u001b[39;49m \u001b[6;22H\u001b[38;5;2;49m> Imported Model█\u001b[39;49m \u001b[8;23H\u001b[38;5;8;49mnter to import, Esc to cancel\u001b[9;22H\u001b[39;49m \u001b[10;22H \u001b[11;22H \u001b[32;22H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.482638, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.584593, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.686176, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.788053, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.889981, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.991675, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.093291, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.194383, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.295384, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.395968, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.497456, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.599038, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.700412, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.801355, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.902735, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.922022, "o", "\u001b[1;17H\u001b[1m\u001b[38;5;0;48;5;4mImported Model\u001b[1;32H[+]\u001b[3;2H\u001b[22m\u001b[38;5;5;49m [Product = Gadgets | Region = South] \u001b[4;2H\u001b[39;49m \u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-02-03\u001b[24m 2025-02-25 2025-01-22 2025-03-28 2025-01-31 2025-01-27 2025-01-11 2025-02-10 2025-03-14\u001b[22m\u001b[39;49m \u001b[5;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mAcme Corp \u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[39;48;5;237m \u001b[7;2H\u001b[39;49mWonka\u001b[7;8HIndustries\u001b[7;20H\u001b[38;5;8;49m \u001b[8;2H\u001b[39;49mInit"]
[12.922264, "o", "ech\u001b[8;20H\u001b[38;5;8;49m \u001b[8;28H \u001b[8;31H \u001b[8;39H \u001b[8;43H \u001b[8;46H \u001b[9;2H\u001b[39;49mUmbrella\u001b[9;11HCo\u001b[9;20H\u001b[38;5;8;49m \u001b[10;2H\u001b[39;49mGlobex\u001b[10;9HInc\u001b[10;20H\u001b[38;5;8;49m \u001b[11;2H\u001b[39;49mCyberdyne\u001b[11;12HSystems\u001b[11;20H\u001b[38;5;8;49m \u001b[12;2H\u001b[39;49mSoylent\u001b[12;10HLtd\u001b[12;20H\u001b[38;5;8;49m \u001b[13;2H\u001b[39;49mStark\u001b[13;8HEnterprises\u001b[13;20H\u001b[38;5;8;49m \u001b[14;2H\u001b[39;49mOceanic\u001b[14;10HAirlines\u001b[14;20H\u001b[38;5;8;49m "]
[12.922541, "o", " \u001b[15;2H──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[16;2H\u001b[1m\u001b[38;5;3;49mTotal 0 0 0 0 0 0 0 0 0\u001b[17;21H\u001b[22m\u001b[39;49m \u001b[17;100H \u001b[18;21H \u001b[18;100H \u001b[19;21H \u001b[19;100H \u001b[20;21H \u001b[20;100H \u001b[21;21H \u001b[21;100H \u001b[22;21H \u001b[22;100H \u001b[23;21H \u001b[23;100H \u001b[24;21H \u001b[24;100H \u001b[25;21H \u001b[25;100H \u001b[26;21H \u001b[26;100H \u001b[27;21H \u001b[27;100H \u001b[28;21H \u001b[28;100H \u001b[29;21H \u001b[29;100H \u001b[30;21H \u001b[30;100H \u001b[31;21H \u001b[31;100H \u001b[32;21H \u001b[32;100H \u001b[33;21H \u001b[36;10H\u001b[38;5;8;49m [_Index ·] [_Dim ·] [_Measure ·] \u001b[38;5;2;"]
[12.922705, "o", "49m [Customer Row] \u001b[38;5;4;49m [Date Col] \u001b[38;5;5;49m [Product Pag] [Region Pag] \u001b[37;3H\u001b[38;5;0;48;5;8mNORMAL\u001b[37;11HImport succ\u001b[37;23Hssful! Press :w <path>\u001b[37;46Hto save. \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.025884, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.129779, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.233754, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.336214, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.440232, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.543166, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.645506, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.748407, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.851524, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.954923, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.058244, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.160825, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.264044, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.366997, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.471142, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.573197, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.676755, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.77958, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.883251, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.987297, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.090783, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.194845, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.298104, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.40055, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.469084, "o", "\u001b[4;14H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01-14\u001b[24m 2025-0\u001b[4;33H-1\u001b[4;36H 2025-03-18\u001b[22m\u001b[39;49m \u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mUmbrella Co\u001b[6;14H\u001b[38;5;0;48;5;6m \u001b[6;25H\u001b[22m\u001b[38;5;15;48;5;237m 10,370\u001b[6;47H\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49mS\u001b[7;4Hylent Ltd \u001b[7;20H9,350\u001b[7;36H 11,560 \u001b[8;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[9;2H\u001b[1m\u001b[38;5;3;49mTotal 9,350 10,370 11,560\u001b[22m\u001b[39;49m "]
[15.469306, "o", " \u001b[10;2H \u001b[10;9H \u001b[10;20H \u001b[11;2H \u001b[11;12H \u001b[11;20H \u001b[12;2H \u001b[12;10H \u001b[12;20H \u001b[13;2H \u001b[13;8H \u001b[13;20H \u001b[14;2H \u001b[14;10H \u001b[14;20H \u001b[15;2H \u001b[16;2H \u001b[37;11H\u001b[38;5;0;48;5;8mHiding\u001b[37;18Hempty rows/columns \u001b[37;39H \u001b[37;46H \u001b[37;49H \u001b[39"]
[15.469451, "o", "m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.570197, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.671986, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.780803, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.875273, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.976917, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.078891, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.179977, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.281679, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.383194, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.483934, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.585778, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.686585, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.78829, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.890249, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.992328, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.093415, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.19456, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.296044, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.398176, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.500056, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.600932, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.701717, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.8031, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.904288, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.005978, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.107622, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.209146, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.310776, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.412516, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.51388, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.615399, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.716846, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.817638, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.91938, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.020843, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.122743, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.150145, "o", "\u001b[4;14H\u001b[1m\u001b[38;5;3;49m 2025-01-14\u001b[4m 2025-02-15\u001b[6;14H\u001b[22m\u001b[24m\u001b[38;5;8;48;5;237m \u001b[1m\u001b[38;5;0;48;5;6m 10,370\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.252006, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.353965, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.456545, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.557, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.658912, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.742183, "o", "\u001b[4;25H\u001b[1m\u001b[38;5;3;49m 2025-02-15\u001b[4m 2025-03-18\u001b[6;25H\u001b[22m\u001b[24m\u001b[38;5;15;48;5;237m 10,370\u001b[1m\u001b[38;5;0;48;5;6m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.843853, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.945171, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.046281, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.147441, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.18382, "o", "\u001b[6;2HUmbrella Co \u001b[38;5;8;49m \u001b[39;49m 10,370\u001b[38;5;8;49m \u001b[39;49m \u001b[7;2H\u001b[1m\u001b[38;5;6;48;5;237mSoylent Ltd \u001b[22m\u001b[38;5;15;48;5;237m 9,350\u001b[38;5;8;48;5;237m \u001b[1m\u001b[38;5;0;48;5;6m 11,560\u001b[22m\u001b[39;48;5;237m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.285624, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.386859, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.488387, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.590209, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.651419, "o", "\u001b[4;25H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-02-15\u001b[24m 2025-03-18\u001b[7;25H\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;15;48;5;237m 11,560\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.753642, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.855676, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.957502, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.059422, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.160676, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.192868, "o", "\u001b[4;14H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01-14\u001b[24m 2025-02-15\u001b[7;14H\u001b[38;5;0;48;5;6m 9,350\u001b[22m\u001b[38;5;8;48;5;237m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.294455, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.396139, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.497157, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.598115, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.700028, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.800846, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.902346, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.004411, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.085017, "o", "\u001b[37;1H\u001b[1m\u001b[38;5;3;48;5;235m:▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.186447, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.288585, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.390454, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.49188, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.533815, "o", "\u001b[37;2H\u001b[1m\u001b[38;5;3;48;5;235mq▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.635568, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.73671, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.838266, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.921803, "o", "\u001b[37;3H\u001b[1m\u001b[38;5;3;48;5;235m!▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.023789, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.125305, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.226879, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.329421, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.430789, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.531493, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.633286, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.73513, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.836317, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.937902, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.039833, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.141634, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.242633, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.303273, "o", "\u001b[?1049l"]
[24.303377, "o", "\u001b[?25h"]
[24.304571, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"]
[24.338067, "o", "\u001b]0;~/git_repos/git.fiddlerwoaroof.com/u/edwlan/improvise\u0007"]
[24.468723, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J---\r\n(0) Mac:edwlan--s059 ~gf/u/edwlan/improvise \u001b[30m\u001b[35m\u001b[39mgit\u001b[35m\u001b[33m->\u001b[35m\u001b[32mmain\u001b[35m\u001b[39m\u001b[00m 2026-04-09 14:42:01\r\n16024:% \u001b[K"]
[24.468837, "o", "\u001b[?2004h"]
[25.113653, "o", "e"]
[25.344547, "o", "\bex"]
[25.514123, "o", "i"]
[25.686254, "o", "t"]
[25.857353, "o", "\u001b[?2004l\r\r\n"]
[25.858441, "o", "\u001b]0;exit\u0007\u001b[2 q"]
{"version": 2, "width": 120, "height": 37, "idle_time_limit": 2.0, "title": "improvise \u2014 import"}
[0.004821, "o", "\u001b[?1049h"]
[0.005717, "o", "\u001b[1;1H\u001b[1m\u001b[38;5;0;48;5;4m improvise \u00b7 New Model ?:help :q quit \u001b[2;1H\u001b[22m\u001b[39;49m\u250c\u001b[2;3HView:\u001b[2;9HDefault\u001b[2;17H\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u001b[38;5;8;49m\u250c Categories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u001b[3;1H\u001b[39;49m\u2502\u001b[3;6H\u001b[1m\u001b[4m\u001b[38;5;3;49m \u001b[3;88H\u001b[22m\u001b[24m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[3;92H_Index\u001b[3;99H(0)\u001b[3;104H\u001b[38;5;8;49m [None \u2205]\u001b[3;120H\u2502\u001b[4;1H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[38;5;5;49m\u250c Import Wizard \u2014 Review Fields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u001b[4;102H\u001b[38;5;8;49m [None \u2205]\u001b[4;120H\u2502\u001b[5;1H\u001b[39;49m\u2502\u001b[1m\u001b[38;5;6;48;5;"]
[0.00574, "o", "237m \u001b[38;5;0;48;5;6m \u001b[22m\u001b[39;48;5;237m \u001b[38;5;5;49m\u2502\u001b[38;5;3;49mReview field proposals (Space toggle, c cycle kind):\u001b[5;100H\u001b[38;5;5;49m\u2502\u001b[39;49m(0)\u001b[5;106H\u001b[38;5;5;49m [Page \u2630\u001b[5;115H]\u001b[5;120H\u001b[38;5;8;49m\u2502\u001b[6;1H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[38;5;5;49m\u2502\u001b[4m\u001b[38;5;7;49m Field Kind Accept\u001b[6;100H\u001b[24m\u001b[38;5;5;49m\u2502\u001b[6;120H\u001b[38;5;8;49m\u2502\u001b[7;1H\u001b[39;49m\u2502\u001b[1m\u001b[38;5;3;49mTota 0\u001b[7;21H\u001b[22m\u001b[38;5;5;49m\u2502\u001b[1m\u001b[38;5;0;48;5;6m Cost Measure (numeric) [\u2713]\u001b[7;100H\u001b[22m\u001b[38;5;5;49m\u2502\u001b[7;120H\u001b[38;5;8;49m\u2502\u001b[8;1H\u001b[39;49m\u2502\u001b[8;21H\u001b[38;5;5;49m\u2502\u001b[38;5;2;49m Customer Category (dimension) [\u2713]\u001b[8;100H\u001b[38;5;5;49m\u2502\u001b[8;120H\u001b[38;5;8;49m\u2502\u001b[9;1H\u001b[39;49m\u2502\u001b[9;21H\u001b[38;5;5;49m\u2502 Date Time Category [\u2713]\u001b[9;100H\u2502\u001b[9;120H\u001b[38;5;8;49m\u2502\u001b[10;1H\u001b[39;49m\u2502\u001b[10;21H\u001b[38;5;5;49m\u2502\u001b[38;5;2;49m Product Category (dimension) [\u2713]\u001b[10"]
[0.005763, "o", ";100H\u001b[38;5;5;49m\u2502\u001b[10;120H\u001b[38;5;8;49m\u2502\u001b[11;1H\u001b[39;49m\u2502\u001b[11;21H\u001b[38;5;5;49m\u2502\u001b[38;5;2;49m Region Category (dimension) [\u2713]\u001b[11;100H\u001b[38;5;5;49m\u2502\u001b[11;120H\u001b[38;5;8;49m\u2502\u001b[12;1H\u001b[39;49m\u2502\u001b[12;21H\u001b[38;5;5;49m\u2502\u001b[38;5;6;49m Revenue Measure (numeric) [\u2713]\u001b[12;100H\u001b[38;5;5;49m\u2502\u001b[12;120H\u001b[38;5;8;49m\u2502\u001b[13;1H\u001b[39;49m\u2502\u001b[13;21H\u001b[38;5;5;49m\u2502\u001b[13;100H\u2502\u001b[13;120H\u001b[38;5;8;49m\u2502\u001b[14;1H\u001b[39;49m\u2502\u001b[14;21H\u001b[38;5;5;49m\u2502\u001b[14;100H\u2502\u001b[14;120H\u001b[38;5;8;49m\u2502\u001b[15;1H\u001b[39;49m\u2502\u001b[15;21H\u001b[38;5;5;49m\u2502\u001b[15;100H\u2502\u001b[15;120H\u001b[38;5;8;49m\u2502\u001b[16;1H\u001b[39;49m\u2502\u001b[16;21H\u001b[38;5;5;49m\u2502\u001b[16;100H\u2502\u001b[16;120H\u001b[38;5;8;49m\u2502\u001b[17;1H\u001b[39;49m\u2502\u001b[17;21H\u001b[38;5;5;49m\u2502\u001b[17;100H\u2502\u001b[17;120H\u001b[38;5;8;49m\u2502\u001b[18;1H\u001b[39;49m\u2502\u001b[18;21H\u001b[38;5;5;49m\u2502\u001b[18;100H\u2502\u001b[18;120H\u001b[38;5;8;49m\u2502\u001b[19;1H\u001b[39;49m\u2502\u001b[19;21H\u001b[38;5;5;49m\u2502\u001b[19;100H\u2502\u001b[19;120H\u001b[38;5;8;49m\u2502\u001b[20;1H\u001b[39;49m\u2502\u001b[20;21H\u001b[38;5;5;49m\u2502\u001b[20;100H\u2502\u001b[20;120H\u001b[38;5;8;49m\u2502\u001b[21;1H\u001b[39;49m\u2502\u001b[21;21H\u001b[38;5;5;49m\u2502\u001b[21;100H\u2502\u001b[21;"]
[0.005784, "o", "120H\u001b[38;5;8;49m\u2502\u001b[22;1H\u001b[39;49m\u2502\u001b[22;21H\u001b[38;5;5;49m\u2502\u001b[22;100H\u2502\u001b[22;120H\u001b[38;5;8;49m\u2502\u001b[23;1H\u001b[39;49m\u2502\u001b[23;21H\u001b[38;5;5;49m\u2502\u001b[23;100H\u2502\u001b[23;120H\u001b[38;5;8;49m\u2502\u001b[24;1H\u001b[39;49m\u2502\u001b[24;21H\u001b[38;5;5;49m\u2502\u001b[24;100H\u2502\u001b[24;120H\u001b[38;5;8;49m\u2502\u001b[25;1H\u001b[39;49m\u2502\u001b[25;21H\u001b[38;5;5;49m\u2502\u001b[25;100H\u2502\u001b[25;120H\u001b[38;5;8;49m\u2502\u001b[26;1H\u001b[39;49m\u2502\u001b[26;21H\u001b[38;5;5;49m\u2502\u001b[26;100H\u2502\u001b[26;120H\u001b[38;5;8;49m\u2502\u001b[27;1H\u001b[39;49m\u2502\u001b[27;21H\u001b[38;5;5;49m\u2502\u001b[27;100H\u2502\u001b[27;120H\u001b[38;5;8;49m\u2502\u001b[28;1H\u001b[39;49m\u2502\u001b[28;21H\u001b[38;5;5;49m\u2502\u001b[28;100H\u2502\u001b[28;120H\u001b[38;5;8;49m\u2502\u001b[29;1H\u001b[39;49m\u2502\u001b[29;21H\u001b[38;5;5;49m\u2502\u001b[29;100H\u2502\u001b[29;120H\u001b[38;5;8;49m\u2502\u001b[30;1H\u001b[39;49m\u2502\u001b[30;21H\u001b[38;5;5;49m\u2502\u001b[30;100H\u2502\u001b[30;120H\u001b[38;5;8;49m\u2502\u001b[31;1H\u001b[39;49m\u2502\u001b[31;21H\u001b[38;5;5;49m\u2502\u001b[31;100H\u2502\u001b[31;120H\u001b[38;5;8;49m\u2502\u001b[32;1H\u001b[39;49m\u2502\u001b[32;21H\u001b[38;5;5;49m\u2502\u001b[38;5;8;49mEnter: next Space: toggle c: cycle kind \u2190 back Esc: cancel \u001b[38;5;5;49m\u2502\u001b[32;120H\u001b[38;5;8;49m\u2502\u001b[33;1H\u001b[39;49m\u2502\u001b[33;21H\u001b[38;5;5;49m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500"]
[0.005803, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[33;120H\u001b[38;5;8;49m\u2502\u001b[34;1H\u001b[39;49m\u2502\u001b[34;88H\u2502\u001b[38;5;8;49m\u2502\u001b[34;120H\u2502\u001b[35;1H\u001b[39;49m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[38;5;8;49m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[36;1H\u001b[38;5;7;49m Tiles: \u001b[36;10H\u001b[38;5;8;49m [_Index \u00b7] [_Dim \u00b7] \u001b[38;5;5;49m [_Measure Pag] \u001b[38;5;8;49m Ctrl+\u2191\u2193\u2190\u2192 to move tiles\u001b[37;1H\u001b[38;5;0;48;5;8m IMPORT Space:toggle c:cycle Enter:next Esc:cancel Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.108052, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.210206, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.312358, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.414536, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.516694, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.618854, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.721003, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.823147, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.925321, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.027474, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.129638, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.231794, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.333954, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.403261, "o", "\u001b[4;39H\u001b[38;5;5;49mDate Components \u001b[5;22H\u001b[38;5;3;49mS\u001b[5;24Hlect\u001b[5;29Hdate component\u001b[5;45Hto extract (Spa\u001b[5;61He toggle):\u001b[39;49m \u001b[6;22H\u001b[1m\u001b[38;5;5;49m Date (format: %Y-%m-%d)\u001b[22m\u001b[39;49m \u001b[7;24H\u001b[1m\u001b[38;5;0;48;5;6m [ ]\u001b[7;30HYear\u001b[22m\u001b[39;49m \u001b[8;22H\u001b[38;5;8;49m [ ] Month\u001b[39;49m \u001b[9;22H\u001b[38;5;8;49m [ ] Quarter\u001b[39;49m \u001b[10;22H \u001b[11;22H \u001b[12;22H \u001b[32;22H\u001b[38;5;8;49mSpace\u001b[32;29Htoggle Enter: next\u001b[32;50H\u2190 back Esc: cancel \u001b[32;73H \u001b[32;78H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.505408, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.607546, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.708543, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.810673, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.912815, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.014961, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.117138, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.219285, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.321465, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.423646, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.526098, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.613326, "o", "\u001b[4;39H\u001b[38;5;5;49mFormulas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[5;22H\u001b[38;5;3;49mD\u001b[5;24Hfine\u001b[5;29Hformulas (optional):\u001b[39;49m \u001b[6;22H\u001b[38;5;8;49m (no formulas yet)\u001b[39;49m \u001b[7;22H \u001b[8;22H\u001b[38;5;8;49mExamples:\u001b[39;49m \u001b[9;24H\u001b[38;5;8;49mDiff = Cos\u001b[9;35H - Revenue\u001b[10;22H Total = SUM(Cost)\u001b[11;22H Ratio = Cost / Revenue\u001b[32;22Hn: n\u001b[32;27Hw\u001b[32;29Hf\u001b[32;31Hrmula\u001b[32;37H d: delete \u001b[32;49HEnter: next \u2190 ba\u001b[32;67Hk \u001b[32;70HEsc:\u001b[32;75Hcancel\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.715504, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.817699, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.920007, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.022151, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.12483, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.225962, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.328229, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.432693, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.533282, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.635462, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.7377, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.822632, "o", "\u001b[4;39H\u001b[38;5;5;49mName Model \u001b[5;22H\u001b[38;5;3;49mModel name:\u001b[39;49m \u001b[6;22H\u001b[38;5;2;49m> Imported Model\u2588\u001b[39;49m \u001b[8;23H\u001b[38;5;8;49mnter: build \u2190 back Esc: cancel\u001b[9;22H\u001b[39;49m \u001b[10;22H \u001b[11;22H \u001b[32;22H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.92477, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.02702, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.129142, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.230551, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.331505, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.433656, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.535824, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.637975, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.740139, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.842279, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.944433, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.023575, "o", "\u001b[4;39H\u001b[38;5;5;49mConfirm \u2500\u2500\u2500\u001b[5;22H\u001b[38;5;2;49mImported Model \u2014 4 categories, 2 measures\u001b[6;22H\u001b[39;49m \u001b[8;22H \u001b[32;22H\u001b[38;5;8;49mEnter: accept \u2190/Esc: back \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.125727, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.227889, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.330049, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.430444, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.532612, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.634784, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.736931, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.839078, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.94124, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.04341, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.14557, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.233632, "o", "\u001b[1;17H\u001b[1m\u001b[38;5;0;48;5;4mImported Model\u001b[1;32H[+]\u001b[3;2H\u001b[22m\u001b[38;5;5;49m [_Measure = Cost | Product = Widgets | Region = North] \u001b[4;2H\u001b[39;49m \u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01-15\u001b[24m 2025-01-22 2025-01-08 2025-01-30 2025-01-12 2025-02-05\u001b[22m\u001b[39;49m \u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6 _Dim (0) \u001b[5;2H\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[5;92H_Measure \u001b[5;102H2\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mAcme Corp \u001b[38;5;0;48;5;6m 7,200\u001b[22m\u001b[38;5;8;48;5;237m \u001b[39;48;5;237m \u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[6;92HCustomer (9)\u001b[6;106H\u001b[38;5;2;49m [Row \u2195]\u001b[7;2H\u001b[39;49mGlobex Inc\u001b[7;20H\u001b[38;5;8;49m \u001b[7;37H\u001b[39;49m5,100\u001b[38;5;8;49m "]
[6.23365, "o", " \u001b[7;88H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[7;92HDate\u001b[7;97H(40)\u001b[7;103H\u001b[38;5;4;49m [Col \u2194]\u001b[8;2H\u001b[39;49mInitech\u001b[8;20H\u001b[38;5;8;49m \u001b[8;81H\u001b[39;49m6,600\u001b[8;88H\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[8;92HProduct\u001b[8;100H(3)\u001b[8;105H\u001b[38;5;5;49m [Page \u2630\u001b[8;114H]\u001b[9;2H\u001b[39;49mSoylent\u001b[9;10HLtd\u001b[9;20H\u001b[38;5;8;49m \u001b[9;88H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[9;92HRegion\u001b[9;99H(4)\u001b[9;104H\u001b[38;5;5;49m [Page \u2630\u001b[9;113H]\u001b[10;2H\u001b[39;49mUmbrella\u001b[10;11HCo\u001b[10;20H\u001b[38;5;8;49m \u001b[10;88H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[10;100H\u001b[39;49m \u001b[11;2HWonka\u001b[11;8HIndustries\u001b[11;20H\u001b[38;5;8;49m \u001b[11;88H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[11;100H\u001b[39;49m \u001b[12;2HStark\u001b[12;8HEnterprises\u001b[12;20H\u001b[38;5;8;49m \u001b[12;88H\u001b[39;49m\u2502\u001b[38;5;8;49m"]
[6.233671, "o", "\u2502\u001b[12;100H\u001b[39;49m \u001b[13;2HOceanic\u001b[13;10HAirlines\u001b[13;20H\u001b[38;5;8;49m \u001b[13;88H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[13;100H\u001b[39;49m \u001b[14;2HCyberdyne\u001b[14;12HSystems\u001b[14;20H\u001b[38;5;8;49m \u001b[14;88H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[14;100H\u001b[39;49m \u001b[15;2H\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[15;100H\u001b[39;49m \u001b[16;2H\u001b[1m\u001b[38;5;3;49mTotal 7,200 5,100 0 0 0 6,600\u001b[16;88H\u001b[22m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[16;100H\u001b[39;49m \u001b[17;21H \u001b[17;88H\u2502\u001b[38;5;8;49m\u2502\u001b[17;100H\u001b[39;49m \u001b[18;21H \u001b[18;88H\u2502\u001b[38;5;8;49m\u2502\u001b[18;100H\u001b[39;49m \u001b[19;21H \u001b[19;88H\u2502\u001b[38;5;8;49m\u2502\u001b[19;100H\u001b[39;49m \u001b[20;21H \u001b[20;88H\u2502\u001b["]
[6.233703, "o", "38;5;8;49m\u2502\u001b[20;100H\u001b[39;49m \u001b[21;21H \u001b[21;88H\u2502\u001b[38;5;8;49m\u2502\u001b[21;100H\u001b[39;49m \u001b[22;21H \u001b[22;88H\u2502\u001b[38;5;8;49m\u2502\u001b[22;100H\u001b[39;49m \u001b[23;21H \u001b[23;88H\u2502\u001b[38;5;8;49m\u2502\u001b[23;100H\u001b[39;49m \u001b[24;21H \u001b[24;88H\u2502\u001b[38;5;8;49m\u2502\u001b[24;100H\u001b[39;49m \u001b[25;21H \u001b[25;88H\u2502\u001b[38;5;8;49m\u2502\u001b[25;100H\u001b[39;49m \u001b[26;21H \u001b[26;88H\u2502\u001b[38;5;8;49m\u2502\u001b[26;100H\u001b[39;49m \u001b[27;21H \u001b[27;88H\u2502\u001b[38;5;8;49m\u2502\u001b[27;100H\u001b[39;49m \u001b[28;21H \u001b[28;88H\u2502\u001b[38;5;8;49m\u2502\u001b[28;100H\u001b[39;49m \u001b[29;21H \u001b[29;88H\u2502\u001b[38;5;8;49m\u2502\u001b[29;100H\u001b[39;49m \u001b[30;21H \u001b[30;88H\u2502\u001b[38;5;8;49m\u2502\u001b[30;100H\u001b[39;49m \u001b[31;21H \u001b[31;88H\u2502\u001b[38;5;8;49m\u2502\u001b[31;100H\u001b[39;49m \u001b[32;21H \u2502\u001b[38;5;8;49m\u2502\u001b[39;49m \u001b[33;21H \u2502\u001b[38;5;8;49m\u2502\u001b[39;49m \u001b[36;48H\u001b[38;5;2;49m [Customer Row] \u001b[38;5;4;49m [Date Col] \u001b[38;5;5;49m [Product Pag] [Region Pag] \u001b[37;3H\u001b[38;5;0;48;5;8mNORMAL\u001b[37;11HImport succ\u001b[37;23Hssful! Press :w <path>\u001b[37;46"]
[6.233714, "o", "Hto save. \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.335974, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.439534, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.542714, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.64428, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.746394, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.849834, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.953166, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.056487, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.159873, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.263221, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.365492, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.468818, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.570782, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.634521, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.737821, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.841115, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.945818, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.050845, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.158455, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.260439, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.362595, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.466533, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.570302, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.681278, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.778235, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.882984, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
+123 -522
View File
@@ -1,522 +1,123 @@
{"version": 2, "width": 120, "height": 37, "timestamp": 1775772168, "idle_time_limit": 2.0, "env": {"SHELL": "/bin/zsh", "TERM": "screen-256color"}}
[0.195175, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"]
[0.202866, "o", "\u001b]0;~/git_repos/git.fiddlerwoaroof.com/u/edwlan/improvise\u0007"]
[0.324014, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J---\r\n(0) Mac:edwlan--s059 ~gf/u/edwlan/improvise \u001b[30m\u001b[35m\u001b[39mgit\u001b[35m\u001b[33m->\u001b[35m\u001b[32mmain\u001b[35m\u001b[39m\u001b[00m 2026-04-09 15:02:48\r\n16023:% \u001b[K"]
[0.324144, "o", "\u001b[?2004h"]
[2.844656, "o", "\u001b[3m./target/release/improvise examples/demo.improv\u001b[23m"]
[4.032794, "o", "\u001b[47D\u001b[23m.\u001b[23m/\u001b[23mt\u001b[23ma\u001b[23mr\u001b[23mg\u001b[23me\u001b[23mt\u001b[23m/\u001b[23mr\u001b[23me\u001b[23ml\u001b[23me\u001b[23ma\u001b[23ms\u001b[23me\u001b[23m/\u001b[23mi\u001b[23mm\u001b[23mp\u001b[23mr\u001b[23mo\u001b[23mv\u001b[23mi\u001b[23ms\u001b[23me\u001b[23m \u001b[23me\u001b[23mx\u001b[23ma\u001b[23mm\u001b[23mp\u001b[23ml\u001b[23me\u001b[23ms\u001b[23m/\u001b[23md\u001b[23me\u001b[23mm\u001b[23mo\u001b[23m.\u001b[23mi\u001b[23mm\u001b[23mp\u001b[23mr\u001b[23mo\u001b[23mv\u001b[?2004l"]
[4.03284, "o", "\r\r\n"]
[4.03374, "o", "\u001b]0;./target/release/improvise examples/demo.improv\u0007\u001b[2 q"]
[4.07901, "o", "\u001b[?1049h"]
[4.081296, "o", "\u001b[1;1H\u001b[1m\u001b[38;5;0;48;5;4m improvise · Acme Sales Demo (demo.improv) ?:help :q quit \u001b[2;1H\u001b[22m\u001b[39;49m┌\u001b[2;3HView:\u001b[2;9HDefault\u001b[2;17H───────────────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[3;1H│\u001b[38;5;5;49m [Customer = Stark Enterprises] \u001b[3;120H\u001b[39;49m│\u001b[4;1H│\u001b[4;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost \u001b[24m Revenue Profit \u001b[4;120H\u001b[22m\u001b[39;49m│\u001b[5;1H│\u001b[5;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-02 2025-03 2025-01 2025-02 2025-03 2025-01 2025-02 2025-03\u001b[5;120H\u001b[22m\u001b[39;49m│\u001b[6;1H│\u001b[38;5;8;49m────────────────────────────────────────────────"]
[4.081395, "o", "──────────────────────────────────────────────────────────────────────\u001b[39;49m│\u001b[7;1H│\u001b[1m\u001b[38;5;6;48;5;237mGadgets North \u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[23m\u001b[39;48;5;237m \u001b[39;49m│\u001b[8;1H│\u001b[8;12HEast\u001b[8;18H\u001b[3m 5,180\u001b[38;5;8;49m \u001b[39;49m 5,670 7,400\u001b[38;5;8;49m \u001b[39;49m 8,100 2,220\u001b[38;5;8;49m \u001b[39;49m 2,430\u001b[8;120H\u001b[23m│\u001b[9;1H│\u001b[9;12HSouth\u001b[9;18H\u001b[3m\u001b[38;5;8;49m \u001b[9;120H\u001b[23m\u001b[39;49m│\u001b[10;1H│\u001b[10;12HWest\u001b[10;18H\u001b[3m\u001b[38;5;8;49m \u001b[10;120H\u001b[23m\u001b[39;49m│\u001b[11;1H│Widgets\u001b[11;12HNorth\u001b[11;18H\u001b[3m\u001b[38;5;8;49m "]
[4.081493, "o", " \u001b[11;120H\u001b[23m\u001b[39;49m│\u001b[12;1H│\u001b[12;12HEast\u001b[12;18H\u001b[3m 7,080\u001b[38;5;8;49m \u001b[39;49m 7,500 11,800\u001b[38;5;8;49m \u001b[39;49m 12,500 4,720\u001b[38;5;8;49m \u001b[39;49m 5,000\u001b[12;120H\u001b[23m│\u001b[13;1H│\u001b[13;12HSouth\u001b[13;18H\u001b[3m\u001b[38;5;8;49m \u001b[13;120H\u001b[23m\u001b[39;49m│\u001b[14;1H│\u001b[14;12HWest\u001b[14;18H\u001b[3m\u001b[38;5;8;49m \u001b[14;120H\u001b[23m\u001b[39;49m│\u001b[15;1H│Sprockets\u001b[15;12HNorth\u001b[15;18H\u001b[3m\u001b[38;5;8;49m \u001b[15;120H\u001b[23m\u001b[39;49m│\u001b[16;1H│\u001b[16;12HEast\u001b[16;18H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 2,940\u001b[38;5;8;49m \u001b[39;49m 4,200\u001b[38;5;8;49m \u001b[39;49m 1,260\u001b[38;5;8;49m \u001b[16;120H\u001b[23m\u001b[39;49m│\u001b[17;1H│\u001b[17;12HSouth\u001b[17;18H\u001b[3m\u001b[38;5;8;49m \u001b[17;120H\u001b[23m\u001b[39;49m│\u001b[18;1H│\u001b[18;12HWest"]
[4.081554, "o", "\u001b[18;18H\u001b[3m\u001b[38;5;8;49m \u001b[18;120H\u001b[23m\u001b[39;49m│\u001b[19;1H│\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[39;49m│\u001b[20;1H│\u001b[1m\u001b[38;5;3;49mTotal 12,260 2,940 13,170 19,200 4,200 20,600 6,940 1,260 7,430\u001b[20;120H\u001b[22m\u001b[39;49m│\u001b[21;1H│\u001b[21;120H│\u001b[22;1H│\u001b[22;120H│\u001b[23;1H│\u001b[23;120H│\u001b[24;1H│\u001b[24;120H│\u001b[25;1H│\u001b[25;120H│\u001b[26;1H│\u001b[26;120H│\u001b[27;1H│\u001b[27;120H│\u001b[28;1H│\u001b[28;120H│\u001b[29;1H│\u001b[29;120H│\u001b[30;1H│\u001b[30;120H│\u001b[31;1H│\u001b[31;120H│\u001b[32;1H│\u001b[32;120H│\u001b[33;1H│\u001b[33;120H│\u001b[34;1H│\u001b[34;120H│\u001b[35;1H└───────────────────"]
[4.081585, "o", "───────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[36;1H\u001b[38;5;7;49m Tiles: \u001b[36;10H\u001b[38;5;8;49m [_Index ·] [_Dim ·] \u001b[38;5;4;49m [_Measure Col] \u001b[38;5;5;49m [Customer Pag] \u001b[38;5;8;49m [Date ·] \u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;4;49m [Date_Month Col] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL hjkl:nav i:edit R:records P:prune F/C/V:panels T:tiles [:]:page >:drill ::cmd Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.183606, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.286393, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.390472, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.492202, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.594054, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.696758, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.799994, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.902051, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.005122, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.108799, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.212099, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.314466, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.417267, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.518871, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.621814, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.711323, "o", "\u001b[4;17H\u001b[1m\u001b[4m\u001b[38;5;3;49m \u001b[4;21HCost \u001b[4;41H\u001b[24m Revenue \u001b[4;67HProfit \u001b[4;89H\u001b[22m\u001b[39;49m \u001b[5;17H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-02 2025-03 2025-01 2025-02 2025-03 2025-01 2025-02 2025-03\u001b[22m\u001b[39;49m \u001b[7;12H\u001b[1m\u001b[38;5;6;48;5;237mEas\u001b[7;16H \u001b[3m\u001b[38;5;0;48;5;6m \u001b[7;20H5,180\u001b[22m\u001b[38;5;8;48;5;237m \u001b[7;33H\u001b[38;5;15;48;5;237m 5,670 7,400\u001b[7;57H 8,100 2,220\u001b[7;81H 2,430\u001b[23m\u001b[39;48;5;237m \u001b[8;2H\u001b[39;49mWidgets\u001b[8;17H\u001b[3m \u001b[8;20H7,080\u001b[38;5;8;49m \u001b[8;33H\u001b[39;49m \u001b[8;36H7,500 \u001b[8;43H11,80\u001b[8;49H\u001b[38;5;8;49m \u001b[8;57H\u001b[39;49m \u001b[8;59H12,50\u001b[8;65H \u001b[8;68H4,7\u001b[8;72H0\u001b[38;5;8;49m \u001b[8;81H\u001b[39;49m \u001b[8;84H5,000\u001b[23m \u001b[9;2HSprockets\u001b[9;12HEas\u001b[9;16H \u001b[3m\u001b[38;5;8;49m \u001b[9;25H\u001b[39;49m 2,940\u001b[9;49H 4,200\u001b[9;73H 1,260\u001b[9;89H\u001b[23m \u001b[10;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────"]
[5.711359, "o", "────────────────────────────────\u001b[11;2H\u001b[1m\u001b[38;5;3;49mTotal 12,260 2,940 13,170 19,200 4,200 20,600 6,940 1,260 7,430\u001b[22m\u001b[39;49m \u001b[12;12H \u001b[12;18H \u001b[13;12H \u001b[13;18H \u001b[14;12H \u001b[14;18H \u001b[15;2H \u001b[15;12H \u001b[15;18H \u001b[16;12H \u001b[16;18H \u001b[17;12H \u001b[17;18H \u001b[18;12H \u001b[18;18H \u001b[19;2H \u001b[20;2H "]
[5.711509, "o", " \u001b[37;11H\u001b[38;5;0;48;5;8mHiding empty rows/columns \u001b[37;40H \u001b[37;49H \u001b[37;63H \u001b[37;72H \u001b[37;82H \u001b[37;91H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.813121, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.91487, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.017513, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.118507, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.220366, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.322685, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.424938, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.526253, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.628282, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.730046, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.831779, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.933073, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.034386, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.136138, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.237624, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.339346, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.44047, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.54239, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.569716, "o", "\u001b[36;10H\u001b[1m\u001b[38;5;0;48;5;6m [_Index ·] \u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;5m TILES Hiding empty rows/columns Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.670732, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.772512, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.873528, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.974952, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.076658, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.178544, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.279559, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.381257, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.482995, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.585266, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.686385, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.788117, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.890743, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.988024, "o", "\u001b[36;10H\u001b[38;5;8;49m [_Index ·] \u001b[1m\u001b[38;5;0;48;5;6m [_Dim ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.089784, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.192696, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.295117, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.397096, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.448442, "o", "\u001b[36;22H\u001b[38;5;8;49m [_Dim ·] \u001b[1m\u001b[38;5;0;48;5;6m [_Measure Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.550536, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.652481, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.754194, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.778141, "o", "\u001b[36;32H\u001b[38;5;4;49m [_Measure Col] \u001b[1m\u001b[38;5;0;48;5;6m [Customer Pag] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.880846, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.983079, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.084998, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.110623, "o", "\u001b[36;48H\u001b[38;5;5;49m [Customer Pag] \u001b[1m\u001b[38;5;0;48;5;6m [Date ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.212706, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.314212, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.415972, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.517497, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.62025, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.721782, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.797927, "o", "\u001b[36;64H\u001b[38;5;8;49m [Date ·] \u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.90034, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.00188, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.1041, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.20582, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.278559, "o", "\u001b[36;74H\u001b[38;5;2;49m [Product Row] \u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.38008, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.481759, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.583563, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.627199, "o", "\u001b[36;89H\u001b[38;5;2;49m [Region Row] \u001b[1m\u001b[38;5;0;48;5;6m [Date_Month Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.729661, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.831725, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.933955, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.034905, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.137145, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.238572, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.34034, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.441291, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.543123, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.644683, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.746513, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.847672, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.949646, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.051292, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.153276, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.256497, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.358742, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.46022, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.56178, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.663495, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.764432, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.86667, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.968591, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.069929, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.172681, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.274414, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.377178, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.478959, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.58086, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.682785, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.769142, "o", "\u001b[1;47H\u001b[1m\u001b[38;5;0;48;5;4m[+]\u001b[4;17H\u001b[22m\u001b[39;49m \u001b[4;28H\u001b[1m\u001b[4m\u001b[38;5;3;49mCost\u001b[24m Revenue Profit\u001b[22m\u001b[39;49m \u001b[5;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets East 2025-01 \u001b[3m\u001b[38;5;0;48;5;6m 5,180\u001b[22m\u001b[38;5;15;48;5;237m 7,400 2,220\u001b[23m\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49m 2025-03 \u001b[3m 5,670 8,100 2,430\u001b[23m \u001b[8;17H2025-01 \u001b[3m 7,080 \u001b[8;34H11,80\u001b[8;40H \u001b[8;42H4,720\u001b[23m \u001b[9;2H \u001b[9;12H \u001b[9;17"]
[14.769179, "o", "H2025-03 \u001b[9;27H\u001b[3m7,500 12,500 5,000\u001b[23m \u001b[10;2HSprockets East 2025-02 \u001b[3m 2,940 4,200 1,260\u001b[23m \u001b[11;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[12;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 44,000 15,630\u001b[36;116H\u001b[38;5;0;48;5;6mR\u001b[36;118Hw\u001b[37;10H\u001b[22m\u001b[38;5;0;48;5;5mDate_Month →\u001b[37;23HR\u001b[37;26H \u001b[37;110HDefault \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.870531, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.972288, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.074003, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.175981, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.277102, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.378888, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.480112, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.581487, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.681928, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.784289, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.886179, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.988225, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.08923, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.191357, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.293039, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.395185, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.496391, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.598014, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.699411, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.800941, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.902697, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.004348, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.105735, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.207365, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.30897, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.410872, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.511981, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.613786, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.715365, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.817026, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.918686, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.020006, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.121899, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.223486, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.324746, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.426154, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.527582, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.629511, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.731055, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.832593, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.934443, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.036075, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.137379, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.239173, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.310363, "o", "\u001b[36;89H\u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[22m\u001b[38;5;2;49m [Date_Month Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.412423, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.514646, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.6164, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.718133, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.770074, "o", "\u001b[36;74H\u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[22m\u001b[38;5;2;49m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.872124, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.973223, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.07488, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.129894, "o", "\u001b[36;64H\u001b[1m\u001b[38;5;0;48;5;6m [Date ·] \u001b[22m\u001b[38;5;2;49m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.231558, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.333386, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.434821, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.536254, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.545792, "o", "\u001b[36;48H\u001b[1m\u001b[38;5;0;48;5;6m [Customer Pag] \u001b[22m\u001b[38;5;8;49m [Date ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.647361, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.74837, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.849862, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.912245, "o", "\u001b[3;2H \u001b[1m\u001b[4m\u001b[38;5;3;49m Cost \u001b[4;25H\u001b[22m\u001b[24m\u001b[39;49m \u001b[4;27H\u001b[1m\u001b[4m\u001b[38;5;3;49mStark Enterprises\u001b[24m Soylent Ltd Wonka Industries Cyberdyne Systems Acme Corp Oceanic Airlines\u001b[6;12H\u001b[38;5;6;48;5;237mNor\u001b[6;16Hh 2025-01 \u001b[6;27H\u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[38;5;15;48;5;237m 4,340\u001b[38;5;8;48;5;237m \u001b[7;17H\u001b[23m\u001b[39;49m 2025-02 \u001b[3m\u001b[38;5;8;49m \u001b[39;49m 5,110\u001b[38;5;8;49m \u001b[8;2H\u001b[23m\u001b[39;49m \u001b[8;12H \u001b[8;17H 2025-03 \u001b[3m\u001b[38;5;8;49m \u001b[9;12H\u001b[23m\u001b[39;49mEast\u001b[9;17H 2025-01 \u001b[9;27H\u001b[3m \u001b[9;34H 5,180\u001b[38;5;8;49m \u001b[39;49m 6,230\u001b[38;5;8;49m \u001b[10;2H\u001b[23m"]
[20.912321, "o", "\u001b[39;49m \u001b[10;12H \u001b[10;17H 2025-02 \u001b[3m\u001b[38;5;8;49m \u001b[39;49m 6,440\u001b[38;5;8;49m \u001b[11;2H\u001b[23m\u001b[39;49m 2025-03 \u001b[3m 5,670\u001b[38;5;8;49m \u001b[23m\u001b[39;49m \u001b[12;2H South 2025-01 \u001b[3m\u001b[38;5;8;49m \u001b[39;49m 3,850\u001b[38;5;8;49m \u001b[13;18H\u001b[23m\u001b[39;49m2025-02\u001b[13;26H\u001b[3m\u001b[38;5;8;49m \u001b[14;18H\u001b[23m\u001b[39;49m2025-03\u001b[14;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 4,760\u001b[38;5;8;49m \u001b[15;12H\u001b[23m\u001b[39;49mWest\u001b[15;18H2025-01\u001b[15;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 4,690\u001b[38;5;8;49m \u001b[16;18H\u001b[23m\u001b[39;49m2025-02\u001b[16;26H\u001b[3m"]
[20.912448, "o", "\u001b[38;5;8;49m \u001b[39;49m 4,970\u001b[17;18H\u001b[23m2025-03\u001b[17;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 5,250\u001b[38;5;8;49m \u001b[18;2H\u001b[23m\u001b[39;49mWidgets\u001b[18;12HNorth\u001b[18;18H2025-01\u001b[18;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 7,200\u001b[38;5;8;49m \u001b[19;18H\u001b[23m\u001b[39;49m2025-02\u001b[19;26H\u001b[3m\u001b[38;5;8;49m \u001b[20;18H\u001b[23m\u001b[39;49m2025-03\u001b[20;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 8,100\u001b[38;5;8;49m \u001b[21;12H\u001b[23m\u001b[39;49mEast\u001b[21;18H2025-01\u001b[21;26H\u001b[3m 7,080\u001b[38;5;8;49m \u001b[39;49m 8,520\u001b[38;5;8;49m \u001b[22;18H\u001b[23m\u001b[39;49m2025-02\u001b[22;26H\u001b[3m\u001b[38;5;8;49m "]
[20.912533, "o", " \u001b[39;49m 9,000\u001b[38;5;8;49m \u001b[23;18H\u001b[23m\u001b[39;49m2025-03\u001b[23;26H\u001b[3m 7,500\u001b[38;5;8;49m \u001b[24;12H\u001b[23m\u001b[39;49mSouth\u001b[24;18H2025-01\u001b[24;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 5,880\u001b[38;5;8;49m \u001b[25;18H\u001b[23m\u001b[39;49m2025-02\u001b[25;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 6,120\u001b[38;5;8;49m \u001b[26;18H\u001b[23m\u001b[39;49m2025-03\u001b[26;26H\u001b[3m\u001b[38;5;8;49m \u001b[27;12H\u001b[23m\u001b[39;49mWest\u001b[27;18H2025-01\u001b[27;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 6,300\u001b[28;18H\u001b[23m2025-02\u001b[28;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 6,720\u001b[38;5;8;49m "]
[20.912627, "o", " \u001b[29;18H\u001b[23m\u001b[39;49m2025-03\u001b[29;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 5,880\u001b[38;5;8;49m \u001b[39;49m 7,680\u001b[30;2H\u001b[23mSprockets\u001b[30;12HNorth\u001b[30;18H2025-01\u001b[30;26H\u001b[3m\u001b[38;5;8;49m \u001b[31;18H\u001b[23m\u001b[39;49m2025-02\u001b[31;26H\u001b[3m\u001b[38;5;8;49m \u001b[32;12H\u001b[23m\u001b[39;49mEast\u001b[32;18H2025-02\u001b[32;26H\u001b[3m 2,940\u001b[38;5;8;49m \u001b[33;18H\u001b[23m\u001b[39;49m2025-03\u001b[33;26H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 3,360\u001b[38;5;8;49m \u001b[34;12H\u001b[23m\u001b[39;49mSouth\u001b[34;18H2025-01\u001b[34;26H\u001b[3m\u001b[38;5;8;49m \u001b[36;59H\u001b[23m\u001b[1m\u001b[38;5;0;48;5;6mCol\u001b[37;10H\u001b[22m\u001b[38;5;0;"]
[20.912712, "o", "48;5;5mCustomer →\u001b[37;21HCol \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.021241, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.130594, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.237986, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.347183, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.455154, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.566032, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.672926, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.780079, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.888829, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.996791, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.105822, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.213368, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.320348, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.426688, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.535526, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.643373, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.749869, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.858388, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.965471, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.075854, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.185921, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.294418, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.402404, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.510325, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.620269, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.732998, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.844083, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.949977, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.059804, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.167351, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.273888, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.38234, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.490268, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.598348, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.707559, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.815903, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.925818, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.031393, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.140847, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.247853, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.356972, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.468231, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.578125, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.685815, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.793907, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.906736, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.01559, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.123245, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.231876, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.339574, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.446666, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.554584, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.612204, "o", "\u001b[36;48H\u001b[38;5;4;49m [Customer Col] \u001b[1m\u001b[38;5;0;48;5;6m [Date ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.721803, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.82881, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.937051, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.045557, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.101995, "o", "\u001b[36;64H\u001b[38;5;8;49m [Date ·] \u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.209792, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.318377, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.426665, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.487816, "o", "\u001b[36;74H\u001b[38;5;2;49m [Product Row] \u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.595002, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.703005, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.811798, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.894092, "o", "\u001b[36;89H\u001b[38;5;2;49m [Region Row] \u001b[1m\u001b[38;5;0;48;5;6m [Date_Month Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.002212, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.111067, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.21876, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.327108, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.435327, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.544297, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.588175, "o", "\u001b[3;2H\u001b[38;5;5;49m [Date_Month = 2025-01] \u001b[39;49m \u001b[4;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m \u001b[4;27H Cos\u001b[4;36H \u001b[22m\u001b[24m\u001b[39;49m \u001b[5;2H \u001b[1m\u001b[4m\u001b[38;5;3;49m Stark Enterprises\u001b[24m Soylent Ltd Wonka Industries Cyberdyne Systems Acme Corp Oceanic Airlines\u001b[22m\u001b[39;49m \u001b[6;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[7;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets North \u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[38;5;15;48;5;237m 4,340\u001b[38;5;8;"]
[28.588511, "o", "48;5;237m \u001b[23m\u001b[39;48;5;237m \u001b[8;12H\u001b[39;49mEast\u001b[8;18H\u001b[3m 5,180\u001b[8;48H 6,230\u001b[8;110H\u001b[23m \u001b[9;12HSou\u001b[9;16Hh\u001b[9;18H\u001b[3m\u001b[38;5;8;49m \u001b[9;39H\u001b[39;49m 3,850\u001b[9;56H\u001b[38;5;8;49m \u001b[9;110H\u001b[23m\u001b[39;49m \u001b[10;12HWest\u001b[10;18H\u001b[3m\u001b[38;5;8;49m \u001b[10;56H \u001b[10;68H\u001b[39;49m 4,690\u001b[10;110H\u001b[23m \u001b[11;2HWidgets\u001b[11;12HNorth\u001b[11;18H\u001b[3m\u001b[38;5;8;49m \u001b[11;83H\u001b[39;49m 7,200\u001b[11;110H\u001b[23m \u001b[12;12HEas\u001b[12;16H \u001b[12;18H\u001b[3m 7,080\u001b[12;44H\u001b[38;5;8;49m \u001b[12;51H\u001b[39;49m 8,520\u001b[12;110H\u001b[23m \u001b[13;12HSouth\u001b[13;18H\u001b[3m\u001b[38;5;8;49m \u001b[13;36H\u001b[39;49m 5,880\u001b[13;110H\u001b[23m \u001b[14;12HWest\u001b[14;18H\u001b[3m\u001b[38;5;8;49m \u001b[14;44H \u001b[14;93H\u001b[39;49m 6,300\u001b[23m \u001b[15;2HSprockets\u001b[15;12HNor\u001b[15;16Hh\u001b[15;18H\u001b[3m\u001b[38;5;8;49m \u001b[15;73H \u001b[15;110H\u001b[23m\u001b[39;49m \u001b[16;12HSouth\u001b[16;18H\u001b[3m\u001b[38;5;8;"]
[28.588661, "o", "49m \u001b[16;101H \u001b[23m\u001b[39;49m \u001b[17;12HWest\u001b[17;18H\u001b[3m\u001b[38;5;8;49m \u001b[17;73H \u001b[17;93H\u001b[39;49m 2,240\u001b[23m \u001b[18;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[19;2H\u001b[1m\u001b[38;5;3;49mTotal 12,260 9,730 14,750 4,690 11,540 8,540\u001b[22m\u001b[39;49m \u001b[20;18H \u001b[20;26H \u001b[21;12H \u001b[21;18H \u001b[21;26H \u001b[22;18H \u001b[22;26H "]
[28.588738, "o", " \u001b[23;18H \u001b[23;26H \u001b[24;12H \u001b[24;18H \u001b[24;26H \u001b[25;18H \u001b[25;26H \u001b[26;18H \u001b[26;26H \u001b[27;12H \u001b[27;18H \u001b[27;26H \u001b[28;18H \u001b[28;26H \u001b[29;18H \u001b[29;26H \u001b[30;2H \u001b[30;12H \u001b[30;18H \u001b[30;26H \u001b[31;18H \u001b[31;26H "]
[28.588853, "o", " \u001b[32;12H \u001b[32;18H \u001b[32;26H \u001b[33;18H \u001b[33;26H \u001b[34;12H \u001b[34;18H \u001b[34;26H \u001b[36;116H\u001b[1m\u001b[38;5;0;48;5;6mPag\u001b[37;10H\u001b[22m\u001b[38;5;0;48;5;5mDate_Month\u001b[37;21H→ Page\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.691453, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.796795, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[28.902199, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.006987, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.111806, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.215479, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.319908, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.423309, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.528042, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.632673, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.737576, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.841402, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[29.945086, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.048563, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.152632, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.257558, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.361587, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.466201, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.569187, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.673297, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.776706, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.880876, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[30.984693, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.088614, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.192586, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.29718, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.325158, "o", "\u001b[36;89H\u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[22m\u001b[38;5;5;49m [Date_Month Pag] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.43008, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.533714, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.640572, "o", "\u001b[36;74H\u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[22m\u001b[38;5;2;49m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.745475, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.848524, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[31.930201, "o", "\u001b[36;64H\u001b[1m\u001b[38;5;0;48;5;6m [Date ·] \u001b[22m\u001b[38;5;2;49m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.033502, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.136248, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.239752, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.342356, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.429596, "o", "\u001b[36;48H\u001b[1m\u001b[38;5;0;48;5;6m [Customer Col] \u001b[22m\u001b[38;5;8;49m [Date ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.532883, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.635586, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.738898, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.84424, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.947995, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[32.984771, "o", "\u001b[3;4H\u001b[38;5;5;49mCustomer =\u001b[3;15HStark Enterprises | Date_Month = 2025-01] \u001b[4;15H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost\u001b[24m Revenue Profit\u001b[22m\u001b[39;49m \u001b[5;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets East \u001b[3m\u001b[38;5;0;48;5;6m 5,180\u001b[22m\u001b[38;5;15;48;5;237m 7,400 2,220\u001b[23m\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49mWidgets East \u001b[3m 7,080 11,800 4,720\u001b[23m \u001b[8;2H\u001b[38;5;8;49m──────────────────────"]
[32.984836, "o", "────────────────────────────────────────────────────────────────────────────────────────────────\u001b[9;2H\u001b[1m\u001b[38;5;3;49mTotal 12,260 19,200 6,940\u001b[22m\u001b[39;49m \u001b[10;12H \u001b[10;18H \u001b[11;2H \u001b[11;12H \u001b[11;18H \u001b[12;12H \u001b[12;18H \u001b[13;12H \u001b[13;18H \u001b[14;12H \u001b[14;18H \u001b[15;2H \u001b"]
[32.984988, "o", "[15;12H \u001b[15;18H \u001b[16;12H \u001b[16;18H \u001b[17;12H \u001b[17;18H \u001b[18;2H \u001b[19;2H \u001b[36;59H\u001b[1m\u001b[38;5;0;48;5;6mPag\u001b[37;10H\u001b[22m\u001b[38;5;0;48;5;5mCustomer →\u001b[37;21HPage \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.086487, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.18779, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.289838, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.391186, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.492681, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.593254, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.69439, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.795177, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.896392, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[33.998164, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.103769, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.204562, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.305805, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.407428, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.50897, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.610366, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.71215, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.813785, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[34.915156, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.017084, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.118645, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.220083, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.321694, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.414691, "o", "\u001b[36;48H\u001b[38;5;5;49m [Customer Pag] \u001b[1m\u001b[38;5;0;48;5;6m [Date ·] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.515987, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.617711, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.719335, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.820861, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[35.922469, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.02437, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.126192, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.14992, "o", "\u001b[36;64H\u001b[38;5;8;49m [Date ·] \u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.251377, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.352991, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.455271, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.556932, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.658353, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.759434, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.860885, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[36.961396, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.063528, "o", "\u001b[36;74H\u001b[38;5;2;49m [Product Row] \u001b[1m\u001b[38;5;0;48;5;6m [Region Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.165156, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.2662, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.367788, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.469285, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.571052, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.672135, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.774993, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.876832, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[37.978463, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.079993, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.181443, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.282509, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.38423, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.485301, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.586649, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.689199, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.791226, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.892952, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[38.993685, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.095155, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.19662, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.298101, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.310417, "o", "\u001b[36;89H\u001b[38;5;2;49m [Region Row] \u001b[1m\u001b[38;5;0;48;5;6m [Date_Month Pag] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.41267, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.514667, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.617075, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.718774, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.820583, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[39.922075, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.023689, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.125316, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.226916, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.230596, "o", "\u001b[3;32H\u001b[38;5;5;49m] \u001b[39;49m \u001b[4;15H \u001b[1m\u001b[4m\u001b[38;5;3;49m Cost\u001b[24m Revenue Profit\u001b[6;10H\u001b[38;5;6;48;5;237m East 2025-01 \u001b[3m\u001b[38;5;0;48;5;6m 5,180\u001b[22m\u001b[38;5;15;48;5;237m 7,400 2,220\u001b[7;2H\u001b[23m\u001b[39;49m \u001b[7;10H \u001b[7;15H 2025-03 \u001b[3m 5,670 8,100 2,430\u001b[8;2H\u001b[23mWidgets East 2025-01 \u001b[3m 7,080 11,800 4,720\u001b[23m \u001b[9;2H 2025-03 \u001b[3m 7,500 12,500 5,000\u001b[10;2H\u001b[23mSprockets\u001b[10;12HEast\u001b[10;17H2025-02\u001b[10;25H\u001b[3m 2,940 4,200 1,260\u001b[11;2H\u001b[23m\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[12;2H\u001b[1m\u001b[38;5;3;49mTotal 28,370 44,000 15,630\u001b"]
[40.230708, "o", "[36;116H\u001b[38;5;0;48;5;6mRow\u001b[37;10H\u001b[22m\u001b[38;5;0;48;5;5mDate_Month\u001b[37;21H→ Row\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.332315, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.433228, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.535032, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.636694, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.737953, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.838891, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[40.940545, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.042589, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.144286, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.246074, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.347233, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.448212, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.549993, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.651976, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.753813, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.854633, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[41.957087, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.059085, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.16124, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.262945, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.365201, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.467407, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.482247, "o", "\u001b[36;103H\u001b[38;5;2;49m [Date_Month Row] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL Date_Month → Row Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.583904, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.685078, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.78682, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.888734, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[42.98978, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.091781, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.193421, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.294857, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.395621, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.498973, "o", "\u001b[4;10H\u001b[1m\u001b[4m\u001b[38;5;3;49m Gadgets \u001b[4;26H\u001b[24m Widgets \u001b[4;41H Sprockets\u001b[5;2H\u001b[22m\u001b[39;49m \u001b[1m\u001b[4m\u001b[38;5;3;49m East \u001b[24m East East\u001b[22m\u001b[39;49m \u001b[6;2H \u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-03 2025-01 2025-03 2025-02\u001b[22m\u001b[39;49m \u001b[7;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[8;2H\u001b[1m\u001b[38;5;6;48;5;237mCost \u001b[3m\u001b[38;5;0;48;5;6m 5,180\u001b[22m\u001b[38;5;15;48;5;237m 5,670 7,080 7,500 2,940\u001b[23m\u001b[39;48;5;237m \u001b[9;2H\u001b[39;49mRevenu"]
[43.499016, "o", "e\u001b[9;10H\u001b[3m 7,400 8,100\u001b[9;27H 11,800 12,500 4,200\u001b[10;2H\u001b[23mProfit \u001b[3m 2,220 2,430\u001b[10;27H 4,720\u001b[10;35H 5,\u001b[10;40H00 1,260\u001b[12;12H\u001b[23m\u001b[1m\u001b[38;5;3;49m14,800\u001b[12;20H16,200 2\u001b[12;30H,600 25,\u001b[12;40H00 8,400\u001b[36;32H\u001b[22m\u001b[38;5;2;49m [_Measure Row] \u001b[36;74H\u001b[38;5;4;49m [Product Col] [Region Col] [Date_Month Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.600963, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.703421, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.804551, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[43.907097, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.008979, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.110755, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.212471, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.314251, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.415568, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.517273, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.618742, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.705815, "o", "\u001b[4;10H \u001b[4;26H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost\u001b[24m Revenue\u001b[4;41HProfit\u001b[22m\u001b[39;49m \u001b[5;2H\u001b[38;5;8;49m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[6;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets East 2025-01 \u001b[3m\u001b[38;5;0;48;5;6m 5,180\u001b[22m\u001b[38;5;15;48;5;237m 7,400 2,220\u001b[23m\u001b[39;48;5;237m \u001b[7;2H\u001b[39;49m 2025-03 \u001b[3m 5,670 8,100 2,430\u001b[23m \u001b[8;2HWidgets East 2025-01 \u001b[3m 7,080 11,800 4,720\u001b[23m \u001b[9;2H \u001b[9;10H 2025-03 \u001b[3m \u001b[9;27H7,500 12,500 5,000\u001b[2"]
[44.705935, "o", "3m \u001b[10;2HSprockets East 2025-02 \u001b[3m \u001b[10;27H2,940 \u001b[10;35H4,20\u001b[10;40H 1,260\u001b[23m \u001b[12;12H\u001b[1m\u001b[38;5;3;49m \u001b[12;20H 28,\u001b[12;30H70 44,00\u001b[12;40H 15,630\u001b[22m\u001b[39;49m \u001b[36;32H\u001b[38;5;4;49m [_Measure Col] \u001b[36;74H\u001b[38;5;2;49m [Product Row] [Region Row] [Date_Month Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.807627, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[44.90931, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.010483, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.11209, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.214054, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.315824, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.417061, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.518689, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.62005, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.72157, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.82307, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[45.924911, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.025947, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.128324, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.229411, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.331387, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.432521, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.534601, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.635595, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.737399, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.838296, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[46.939905, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.041873, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.143353, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.245237, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.346678, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.448408, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.550153, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.651708, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.753436, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.854847, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[47.956529, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.05797, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.15988, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.261094, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.362924, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.464278, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.566104, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.667592, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.769687, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.871205, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[48.972328, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.07408, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.175891, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.277195, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.3788, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.442395, "o", "\u001b[37;1H\u001b[1m\u001b[38;5;3;48;5;235m:▌ \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.544319, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.645499, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.747199, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.848828, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.863148, "o", "\u001b[37;2H\u001b[1m\u001b[38;5;3;48;5;235mq▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[49.964715, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.066286, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.167945, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.269492, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.350163, "o", "\u001b[37;3H\u001b[1m\u001b[38;5;3;48;5;235m!▌\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.452616, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.554188, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.655924, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.757431, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.859183, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[50.960929, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.062657, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.163616, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.265278, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.367394, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.468649, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.570276, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.671308, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.773372, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.874565, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[51.97578, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.07771, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.179348, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[52.18419, "o", "\u001b[?1049l"]
[52.18437, "o", "\u001b[?25h"]
[52.186019, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"]
[52.233127, "o", "\u001b]0;~/git_repos/git.fiddlerwoaroof.com/u/edwlan/improvise\u0007"]
[52.387142, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J---\r\n(0) Mac:edwlan--s059 ~gf/u/edwlan/improvise \u001b[30m\u001b[35m\u001b[39mgit\u001b[35m\u001b[33m->\u001b[35m\u001b[32mmain\u001b[35m\u001b[39m\u001b[00m 2026-04-09 15:03:40\r\n16024:% \u001b[K"]
[52.387248, "o", "\u001b[?2004h"]
[53.608892, "o", "\u001b[?2004l\r\r\n"]
{"version": 2, "width": 120, "height": 37, "idle_time_limit": 2.0, "title": "improvise \u2014 pivot"}
[0.00824, "o", "\u001b[?1049h"]
[0.010718, "o", "\u001b[1;1H\u001b[1m\u001b[38;5;0;48;5;4m improvise \u00b7 Acme Sales Demo (demo.improv) ?:help :q quit \u001b[2;1H\u001b[22m\u001b[39;49m\u250c\u001b[2;3HView:\u001b[2;9HDefault\u001b[2;17H\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u001b[38;5;8;49m\u250c Categories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u001b[3;1H\u001b[39;49m\u2502\u001b[38;5;5;49m [Customer = Stark Enterprises] \u001b[3;88H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[3;92H_Index\u001b[3;99H(0)\u001b[3;104H\u001b[38;5;8;49m [None \u2205]\u001b[3;120H\u2502\u001b[4;1H\u001b[39;49m\u2502\u001b[4;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m Cost \u001b[24m Revenue Profit \u001b[4;88H\u001b[22m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[4;92H_Dim\u001b[4;97H(0)\u001b[4;102H\u001b[38;5;8;49m [None \u2205]\u001b[4;120H\u2502\u001b[5;1H\u001b[39;49m\u2502\u001b[5;18H\u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-02 2025-03 2025-01 2025-02 2025-03 2025-01 2025-02\u001b[5;88H\u001b[22m\u001b[39"]
[0.010728, "o", ";49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[5;92H_Measure\u001b[5;101H(3)\u001b[5;106H\u001b[38;5;4;49m [Col \u2194]\u001b[5;120H\u001b[38;5;8;49m\u2502\u001b[6;1H\u001b[39;49m\u2502\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[6;92HCustomer\u001b[6;101H(9)\u001b[6;106H\u001b[38;5;5;49m [Page \u2630\u001b[6;115H]\u001b[6;120H\u001b[38;5;8;49m\u2502\u001b[7;1H\u001b[39;49m\u2502\u001b[1m\u001b[38;5;6;48;5;237mGadgets North \u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[23m\u001b[39;48;5;237m \u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[7;92HDate\u001b[7;97H(40)\u001b[7;103H\u001b[38;5;8;49m [None \u2205]\u001b[7;120H\u2502\u001b[8;1H\u001b[39;49m\u2502\u001b[8;12HEast\u001b[8;18H\u001b[3m 5,180\u001b[38;5;8;49m \u001b[39;49m 5,670 7,400\u001b[38;5;8;49m \u001b[39;49m 8,100 2,220\u001b[38;5;8;49m \u001b[8;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[8;92HP"]
[0.010744, "o", "roduct\u001b[8;100H(3)\u001b[8;105H\u001b[38;5;2;49m [Row \u2195]\u001b[8;120H\u001b[38;5;8;49m\u2502\u001b[9;1H\u001b[39;49m\u2502\u001b[9;12HSouth\u001b[9;18H\u001b[3m\u001b[38;5;8;49m \u001b[9;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[9;92HRegion\u001b[9;99H(4)\u001b[9;104H\u001b[38;5;2;49m [Row \u2195]\u001b[9;120H\u001b[38;5;8;49m\u2502\u001b[10;1H\u001b[39;49m\u2502\u001b[10;12HWest\u001b[10;18H\u001b[3m\u001b[38;5;8;49m \u001b[10;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[39;49m\u25b6\u001b[10;92HDate_Month\u001b[10;103H(3)\u001b[10;108H\u001b[38;5;4;49m [Col \u2194]\u001b[10;120H\u001b[38;5;8;49m\u2502\u001b[11;1H\u001b[39;49m\u2502Widgets\u001b[11;12HNorth\u001b[11;18H\u001b[3m\u001b[38;5;8;49m \u001b[11;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[11;120H\u2502\u001b[12;1H\u001b[39;49m\u2502\u001b[12;12HEast\u001b[12;18H\u001b[3m 7,080\u001b[38;5;8;49m \u001b[39;49m 7,500 11,800\u001b[38;5;8;49m \u001b[39;49m 12,500 4,720\u001b[38;5;8;49m \u001b[12;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[12;120H\u2502\u001b[13;1H\u001b[39;49m\u2502\u001b[13;12HSouth\u001b[13;18H\u001b[3m\u001b[38;5;8;49m "]
[0.010763, "o", " \u001b[13;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[13;120H\u2502\u001b[14;1H\u001b[39;49m\u2502\u001b[14;12HWest\u001b[14;18H\u001b[3m\u001b[38;5;8;49m \u001b[14;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[14;120H\u2502\u001b[15;1H\u001b[39;49m\u2502Sprockets\u001b[15;12HNorth\u001b[15;18H\u001b[3m\u001b[38;5;8;49m \u001b[15;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[15;120H\u2502\u001b[16;1H\u001b[39;49m\u2502\u001b[16;12HEast\u001b[16;18H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 2,940\u001b[38;5;8;49m \u001b[39;49m 4,200\u001b[38;5;8;49m \u001b[39;49m 1,260\u001b[16;88H\u001b[23m\u2502\u001b[38;5;8;49m\u2502\u001b[16;120H\u2502\u001b[17;1H\u001b[39;49m\u2502\u001b[17;12HSouth\u001b[17;18H\u001b[3m\u001b[38;5;8;49m \u001b[17;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[17;120H\u2502\u001b[18;1H\u001b[39;49m\u2502\u001b[18;12HWest\u001b[18;18H\u001b[3m\u001b[38;5;8;49m \u001b[18;88H\u001b[23m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[18;120H\u2502\u001b[19;1H\u001b[39;49m\u2502\u001b[38;5;8;49m"]
[0.010784, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[19;120H\u2502\u001b[20;1H\u001b[39;49m\u2502\u001b[1m\u001b[38;5;3;49mTotal 12,260 2,940 13,170 19,200 4,200 20,600 6,940 1,260\u001b[20;88H\u001b[22m\u001b[39;49m\u2502\u001b[38;5;8;49m\u2502\u001b[20;120H\u2502\u001b[21;1H\u001b[39;49m\u2502\u001b[21;88H\u2502\u001b[38;5;8;49m\u2502\u001b[21;120H\u2502\u001b[22;1H\u001b[39;49m\u2502\u001b[22;88H\u2502\u001b[38;5;8;49m\u2502\u001b[22;120H\u2502\u001b[23;1H\u001b[39;49m\u2502\u001b[23;88H\u2502\u001b[38;5;8;49m\u2502\u001b[23;120H\u2502\u001b[24;1H\u001b[39;49m\u2502\u001b[24;88H\u2502\u001b[38;5;8;49m\u2502\u001b[24;120H\u2502\u001b[25;1H\u001b[39;49m\u2502\u001b[25;88H\u2502\u001b[38;5;8;49m\u2502\u001b[25;120H\u2502\u001b[26;1H\u001b[39;49m\u2502\u001b[26;88H\u2502\u001b[38;5;8;49m\u2502\u001b[26;120H\u2502\u001b[27;1H\u001b[39;49m\u2502\u001b[27;88H\u2502\u001b[38;5;8;49m\u2502\u001b[27;120H\u2502\u001b[28;1H\u001b[39;49m\u2502\u001b[28;88H\u2502\u001b[38;5;8;49m\u2502\u001b[28;120H\u2502\u001b[29;1H\u001b[39;49m\u2502\u001b[29;88H\u2502\u001b[38;5;8;49m\u2502\u001b[29;120H\u2502\u001b[30;1H\u001b[39;49m\u2502\u001b[30;88H\u2502\u001b[38;5;8;49m\u2502\u001b[30;120H\u2502\u001b["]
[0.010803, "o", "31;1H\u001b[39;49m\u2502\u001b[31;88H\u2502\u001b[38;5;8;49m\u2502\u001b[31;120H\u2502\u001b[32;1H\u001b[39;49m\u2502\u001b[32;88H\u2502\u001b[38;5;8;49m\u2502\u001b[32;120H\u2502\u001b[33;1H\u001b[39;49m\u2502\u001b[33;88H\u2502\u001b[38;5;8;49m\u2502\u001b[33;120H\u2502\u001b[34;1H\u001b[39;49m\u2502\u001b[34;88H\u2502\u001b[38;5;8;49m\u2502\u001b[34;120H\u2502\u001b[35;1H\u001b[39;49m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[38;5;8;49m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u001b[36;1H\u001b[38;5;7;49m Tiles: \u001b[36;10H\u001b[38;5;8;49m [_Index \u00b7] [_Dim \u00b7] \u001b[38;5;4;49m [_Measure Col] \u001b[38;5;5;49m [Customer Pag] \u001b[38;5;8;49m [Date \u00b7] \u001b[38;5;2;49m [Product Row] [Region Row] \u001b[38;5;4;49m [Date_Month Col] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL hjkl\u2190\u2191\u2192\u2193:nav ai:edit R:records P:prune F:formulas C:categories V:views T:tiles [:prev-page ]:next-pag\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.113594, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.216309, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.319069, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.421861, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.524574, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.627301, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.730025, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.832574, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[0.934646, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.03742, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.140227, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.242954, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.345694, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.448439, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.551164, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.60679, "o", "\u001b[36;10H\u001b[1m\u001b[38;5;0;48;5;6m [_Index \u00b7] \u001b[37;1H\u001b[22m\u001b[38;5;0;48;5;5m TILES hl\u2190\u2192:select Enter/Space:cycle r:row c:col p:page Esc/Tab:back Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.709471, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.812277, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[1.915005, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.017728, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.120436, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.223184, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.32591, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.42863, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.531316, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.634027, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.736677, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.810908, "o", "\u001b[36;10H\u001b[38;5;8;49m [_Index \u00b7] \u001b[1m\u001b[38;5;0;48;5;6m [_Dim \u00b7] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.913611, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.016275, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.118914, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.17099, "o", "\u001b[36;22H\u001b[38;5;8;49m [_Dim \u00b7] \u001b[1m\u001b[38;5;0;48;5;6m [_Measure Col] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.274291, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.377983, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.48162, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.531951, "o", "\u001b[36;32H\u001b[38;5;4;49m [_Measure Col] \u001b[1m\u001b[38;5;0;48;5;6m [Customer Pag] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.635296, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.738948, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.842562, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.882724, "o", "\u001b[36;48H\u001b[38;5;5;49m [Customer Pag] \u001b[1m\u001b[38;5;0;48;5;6m [Date \u00b7] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.985398, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.089296, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.192493, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.242332, "o", "\u001b[36;64H\u001b[38;5;8;49m [Date \u00b7] \u001b[1m\u001b[38;5;0;48;5;6m [Product Row] \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.346109, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.448809, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.552424, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.599678, "o", "\u001b[1;47H\u001b[1m\u001b[38;5;0;48;5;4m[+]\u001b[4;8H\u001b[4m\u001b[38;5;3;49m Cost \u001b[4;22H \u001b[4;42H \u001b[5;8H Gadgets \u001b[5;19H \u001b[24m Widgets \u001b[5;43H \u001b[5;51H Sprockets\u001b[5;67H \u001b[5;75H \u001b[6;2H\u001b[22m\u001b[39;49m \u001b[1m\u001b[4m\u001b[38;5;3;49m 2025-01\u001b[24m 2025-02 2025-03 2025-01 2025-02 2025-03 2025-01 2025-02 2025-03\u001b[22m\u001b[39;49m \u001b[7;2H\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[8;2H\u001b[1m\u001b[38;5;6;48;5;237mNorth \u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[23m\u001b[39;48;5;237m \u001b[8;105H\u001b[38;5;4;49m [Col \u2194]\u001b[9;2H\u001b[39;49mEast\u001b[9;8H\u001b[3m 5,180\u001b[38;5;8;49m \u001b[9;24H\u001b[39;49m 5,670 7,080\u001b[9;48H 7,500\u001b[9;66H 2,940\u001b[9;82H\u001b[38;5;8;49m \u001b[10;2H\u001b[23m\u001b[39;49m"]
[4.59972, "o", "South\u001b[10;8H\u001b[3m\u001b[38;5;8;49m \u001b[10;82H \u001b[11;3H\u001b[23m\u001b[39;49mest \u001b[3m\u001b[38;5;8;49m \u001b[11;82H \u001b[12;2H\u001b[23m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[13;2H\u001b[1m\u001b[38;5;3;49mTotal 5,180 0 5,670 7,080 0 7,500 0 2,940 0\u001b[14;12H\u001b[22m\u001b[39;49m \u001b[14;18H \u001b[15;2H \u001b[15;12H \u001b[15;18H \u001b[16;12H \u001b[16;18H \u001b[17;12H \u001b[17;18H \u001b[18;12H \u001b[18;18H \u001b[19;2H "]
[4.599766, "o", " \u001b[20;2H \u001b[36;84H\u001b[1m\u001b[38;5;0;48;5;6mC\u001b[36;86Hl\u001b[37;10H\u001b[22m\u001b[38;5;0;48;5;5mProduct \u2192 Col \u001b[37;42H \u001b[37;49H \u001b[37;56H \u001b[37;64H \u001b[37;108H Default\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.70338, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.805285, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.908241, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.011837, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.114534, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.217208, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.320236, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.423831, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.527439, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.630676, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.734425, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.838082, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.940768, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.043457, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.146139, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.248828, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.351066, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.453816, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.556508, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.659257, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.761965, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.800917, "o", "\u001b[4;8H \u001b[4;22H\u001b[1m\u001b[4m\u001b[38;5;3;49mCost\u001b[4;42H\u001b[24m Revenue Profit \u001b[22m\u001b[39;49m \u001b[5;8H \u001b[5;19H\u001b[1m\u001b[4m\u001b[38;5;3;49m2025-01\u001b[24m 2025-02 2025-03\u001b[5;43H2025-01\u001b[5;51H2025-02 2025-03\u001b[5;67H2025-01\u001b[5;75H2025-02\u001b[22m\u001b[39;49m \u001b[6;2H\u001b[38;5;8;49m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[7;2H\u001b[1m\u001b[38;5;6;48;5;237mGadgets North \u001b[3m\u001b[38;5;0;48;5;6m \u001b[22m\u001b[38;5;8;48;5;237m \u001b[23m\u001b[39;48;5;237m \u001b[8;2H\u001b[39;49m East \u001b[3m 5,180\u001b[38;5;8;49m \u001b[39;49m 5,670 7,400\u001b[38;5;8;49m \u001b[39;49m 8,100 2,220\u001b[38;5;8;49m \u001b[23m\u001b[39;49m \u001b[8;105H\u001b[38;5;2;49m [Row \u2195]\u001b[9;2H\u001b[39;49m \u001b[9;8H South \u001b[9;24H\u001b[3m\u001b[38;5;8;49m \u001b[9;48H \u001b[9;66H "]
[6.800947, "o", "\u001b[9;82H\u001b[23m\u001b[39;49m \u001b[10;2H \u001b[10;8H West \u001b[10;82H \u001b[11;3Hidgets North \u001b[11;82H \u001b[12;2H East \u001b[3m 7,080\u001b[38;5;8;49m \u001b[39;49m 7,500 11,800\u001b[38;5;8;49m \u001b[39;49m 12,500 4,720\u001b[38;5;8;49m \u001b[23m\u001b[39;49m \u001b[13;2H South \u001b[3m\u001b[38;5;8;49m \u001b[23m\u001b[39;49m \u001b[14;12HWest\u001b[14;18H\u001b[3m\u001b[38;5;8;49m \u001b[15;2H\u001b[23m\u001b[39;49mSprockets\u001b[15;12HNorth\u001b[15;18H\u001b[3m\u001b[38;5;8;49m \u001b[16;12H\u001b[23m\u001b[39;49mEast\u001b[16;18H\u001b[3m\u001b[38;5;8;49m \u001b[39;49m 2,940\u001b[38;5;8;49m \u001b[39;49m 4,200\u001b[38;5;8;49m \u001b[39;49m 1,260\u001b[17;12H\u001b[23mSouth\u001b[17;18H\u001b[3m\u001b[38;5;8;49m \u001b[18;12H\u001b[23m\u001b[39;49mWest\u001b[18;18H\u001b[3m\u001b[38;5;8;49m \u001b[19;2H\u001b[23m\u2500\u2500\u2500\u2500\u2500\u2500\u2500"]
[6.800971, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u001b[20;2H\u001b[1m\u001b[38;5;3;49mTotal 12,260 2,940 13,170 19,200 4,200 20,600 6,940 1,260\u001b[36;84H\u001b[38;5;0;48;5;6mR\u001b[36;86Hw\u001b[37;20H\u001b[22m\u001b[38;5;0;48;5;5mR\u001b[37;22Hw\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.903724, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.006438, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.107813, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.21049, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.313243, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.415929, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.518618, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.62134, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.72403, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.826802, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.929516, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.03224, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.134921, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.237609, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.340279, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.442985, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.54568, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.648387, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.74982, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.852504, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.955191, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.003008, "o", "\u001b[36;74H\u001b[38;5;2;49m [Product Row] \u001b[37;1H\u001b[38;5;0;48;5;8m NORMAL Product \u2192 Row Default \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.105732, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.208518, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.311187, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.413891, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.516585, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.619324, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.721926, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.824649, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.927433, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.030203, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.132942, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.235699, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.338384, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.441134, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.543863, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.646616, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.748527, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.851224, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.953268, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
+14 -16
View File
@@ -9,8 +9,8 @@
content="Terminal pivot-table modeling in the spirit of Lotus Improv: multidimensional cells, formulas over dimensions instead of cell addresses, vim-style keybindings."
/>
<!-- improvise-e61: the site root, linking to the app under ./app/.
Why cols/rows are 120x37 and not the 100x30 the tickets name: see the
close reasons on improvise-e61 and improvise-kh8. -->
120x37 is an INPUT now, not whatever the recorder had: the casts are
made by docs/record-casts.py, which sets it (improvise-2oxt). -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/asciinema-player@3.10.0/dist/bundle/asciinema-player.min.css"
@@ -67,15 +67,6 @@
margin: 0 0 1rem;
max-width: 72ch;
}
/* Same size as the body text and set off, not a grey footnote: a
visitor who reads the section reads this (improvise-2oxt). */
p.dated {
margin: 0 0 1rem;
padding: 0.6rem 0.9rem;
max-width: 72ch;
border-left: 3px solid var(--accent);
background: var(--panel);
}
a {
color: var(--accent);
}
@@ -104,6 +95,18 @@
border-radius: 6px;
overflow: hidden;
}
/* A 120-column cast cannot fit a phone: the player DERIVES its font size
from the width it is given, so 390px gave 4.83px (improvise-o31u). */
@media (max-width: 900px) {
.player {
overflow-x: auto;
}
/* The overflow must stay INSIDE .player. A page that scrolls sideways
breaks the whole document rather than one section of it. */
.player > * {
min-width: 880px;
}
}
pre {
background: var(--panel);
border: 1px solid var(--rule);
@@ -156,11 +159,6 @@
<p class="note">
An aggregated cell opens onto the records behind it, editable in place.
</p>
<p class="dated">
Recorded 2026-04-09. The records grid has changed since: it now has a
column per measure plus columns for adding a category and a measure,
where this recording shows a single <code>Value</code> column.
</p>
<div class="player" id="cast-drill"></div>
<h2>Formulas over dimensions</h2>
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""Record the four asciicasts under docs/casts/ from a driven improvise session.
Not `asciinema rec`: that records a SHELL, and the shell it records is the
recorder's own — every previous cast opens and closes on a real hostname and
home-directory path on what is now the site root (improvise-2oxt). Here there is
no shell at all. `improvise` is the pty's only child, so there is nothing for a
prompt to leak into, and the geometry, the pacing and the keys are inputs rather
than whatever the recording machine happened to have.
Usage: python3 docs/record-casts.py [--out docs/casts] [--bin target/release/improvise]
The output is asciicast v2, which is what `asciinema-player` on docs/index.html
reads: one JSON header line, then `[time, "o", data]` per chunk.
"""
import argparse
import fcntl
import json
import os
import pty
import select
import signal
import struct
import subprocess
import sys
import termios
import time
COLS, ROWS = 120, 37
# Long enough that a reader can follow, short enough that a loop is bearable.
BEAT = 0.9
SETTLE = 0.35
def keys(*items):
"""A step list from (text, pause) pairs, with the pause defaulted."""
return [(k, BEAT) if isinstance(k, str) else k for k in items]
ESC = "\x1b"
def scenarios(root):
"""The four the landing page embeds, each named for the section over it."""
demo = os.path.join(root, "examples/demo.improv")
csv = os.path.join(root, "examples/demo.csv")
return {
# "Reassign an axis": the tile bar, a category moved, the grid redrawn.
"pivot": (
[demo],
keys(
("", 1.6),
("T", 1.2),
*[("\x1b[C", 0.35)] * 5,
("c", 2.2),
("r", 2.2),
(ESC, 1.6),
),
),
# "Drill into an aggregate": the records grid behind one cell, which is
# the cast 2oxt says is materially wrong — it predates the +cat and +
# columns entirely.
"drill": (
[demo],
keys(
("", 1.6),
# Customer off the page axis first, so a cell aggregates over
# all nine of them and the drill has more than one record to
# show. Drilling the shipped layout shows exactly one row,
# which is true and demonstrates nothing.
("T", 1.0),
*[("\x1b[C", 0.35)] * 3,
(" ", 1.8),
(ESC, 1.2),
# Gadgets/East at Cost 2025-01, which has records behind it.
# The origin cell is blank, and a drill into nothing draws an
# empty records grid — a true frame about the wrong thing.
("j", 1.0),
(">", 2.2),
*[("\x1b[C", 0.45)] * 6,
("", 1.4),
(ESC, 1.6),
),
),
# "Formulas over dimensions": the panel, then the formula that is
# already in the document, read as text rather than as a cell address.
"formulas": (
[demo],
keys(
("", 1.6),
# The panel first: the formula is TEXT over categories and
# items, which is the claim the section makes.
("F", 2.4),
(ESC, 1.0),
# Then move a category and let the Profit column follow. A
# formula naming `B3` could not survive this, which is the
# whole point and is only visible if the grid really moves.
("T", 1.0),
*[("\x1b[C", 0.35)] * 7,
("r", 2.6),
(ESC, 2.0),
),
),
# "Import a CSV": the wizard's own steps, stepped through.
"import": (
["import", csv, "-o", "/dev/null"],
keys(
("", 1.4),
("\r", 1.2),
("\r", 1.2),
("\r", 1.2),
("\r", 1.2),
("\r", 1.4),
(ESC, 1.0),
),
),
}
def record(binary, argv, steps, cwd):
"""Drive one session on a pty and return its asciicast event list."""
master, slave = pty.openpty()
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", ROWS, COLS, 0, 0))
env = {
"TERM": "xterm-256color",
"PATH": "/usr/bin:/bin",
"LANG": "en_US.UTF-8",
"HOME": "/tmp",
# `improvise` is the only child, so nothing else can print a path.
"COLUMNS": str(COLS),
"LINES": str(ROWS),
}
child = subprocess.Popen(
[binary, *argv],
stdin=slave,
stdout=slave,
stderr=slave,
env=env,
cwd=cwd,
close_fds=True,
)
os.close(slave)
started = time.time()
events = []
def drain(until):
"""Everything the child says before `until`, timestamped as it arrives."""
while True:
budget = until - time.time()
if budget <= 0:
return
ready, _, _ = select.select([master], [], [], min(budget, 0.05))
if not ready:
continue
try:
chunk = os.read(master, 65536)
except OSError:
return
if not chunk:
return
events.append(
[round(time.time() - started, 6), "o", chunk.decode("utf-8", "replace")]
)
for text, pause in steps:
if text:
os.write(master, text.encode())
drain(time.time() + pause)
# No `:q` typed at the end: the player loops, so the last frame is also the
# frame after the last one, and a minibuffer reading `:q` is what it would
# show. The process is signalled instead and the recording ends on the UI.
drain(time.time() + SETTLE)
if child.poll() is None:
child.send_signal(signal.SIGTERM)
try:
child.wait(timeout=3)
except subprocess.TimeoutExpired:
child.kill()
os.close(master)
return events
def main():
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=os.path.join(root, "docs/casts"))
ap.add_argument("--bin", default=os.path.join(root, "target/release/improvise"))
ap.add_argument("--only", default=None)
args = ap.parse_args()
if not os.path.exists(args.bin):
sys.exit(f"no binary at {args.bin} — cargo build --release --bin improvise")
for name, (argv, steps) in scenarios(root).items():
if args.only and name != args.only:
continue
events = record(args.bin, argv, steps, root)
header = {
"version": 2,
"width": COLS,
"height": ROWS,
# No `timestamp` and no `env`: the first carries the recording
# machine's clock and the second carried $SHELL (improvise-2oxt).
"idle_time_limit": 2.0,
"title": f"improvise — {name}",
}
path = os.path.join(args.out, f"{name}.cast")
with open(path, "w") as out:
out.write(json.dumps(header) + "\n")
for event in events:
out.write(json.dumps(event) + "\n")
print(f"{path}: {len(events)} chunks, {events[-1][0] if events else 0:.1f}s")
if __name__ == "__main__":
main()
+114
View File
@@ -0,0 +1,114 @@
v2025-04-09
# Harness tall fixture
Initial View: Tall
## View: Default
_Index: none
_Dim: none
_Measure: page
Region: row
format: ,.0
## View: Tall
_Index: none
_Dim: none
_Measure: column
Region: row
format: ,.0
## Category: _Measure
- Revenue, Cost
## Category: Region
- R01[Alpha]
- R02[Alpha]
- R03[Alpha]
- R04[Alpha]
- R05[Alpha]
- R06[Alpha]
- R07[Alpha]
- R08[Alpha]
- R09[Alpha]
- R10[Alpha]
- R11[Beta]
- R12[Beta]
- R13[Beta]
- R14[Beta]
- R15[Beta]
- R16[Beta]
- R17[Beta]
- R18[Beta]
- R19[Beta]
- R20[Beta]
- R21[Gamma]
- R22[Gamma]
- R23[Gamma]
- R24[Gamma]
- R25[Gamma]
- R26[Gamma]
- R27[Gamma]
- R28[Gamma]
- R29[Gamma]
- R30[Gamma]
## Data
Region=R01, _Measure=Cost = 50
Region=R01, _Measure=Revenue = 100
Region=R02, _Measure=Cost = 51
Region=R02, _Measure=Revenue = 101
Region=R03, _Measure=Cost = 52
Region=R03, _Measure=Revenue = 102
Region=R04, _Measure=Cost = 53
Region=R04, _Measure=Revenue = 103
Region=R05, _Measure=Cost = 54
Region=R05, _Measure=Revenue = 104
Region=R06, _Measure=Cost = 55
Region=R06, _Measure=Revenue = 105
Region=R07, _Measure=Cost = 56
Region=R07, _Measure=Revenue = 106
Region=R08, _Measure=Cost = 57
Region=R08, _Measure=Revenue = 107
Region=R09, _Measure=Cost = 58
Region=R09, _Measure=Revenue = 108
Region=R10, _Measure=Cost = 59
Region=R10, _Measure=Revenue = 109
Region=R11, _Measure=Cost = 60
Region=R11, _Measure=Revenue = 110
Region=R12, _Measure=Cost = 61
Region=R12, _Measure=Revenue = 111
Region=R13, _Measure=Cost = 62
Region=R13, _Measure=Revenue = 112
Region=R14, _Measure=Cost = 63
Region=R14, _Measure=Revenue = 113
Region=R15, _Measure=Cost = 64
Region=R15, _Measure=Revenue = 114
Region=R16, _Measure=Cost = 65
Region=R16, _Measure=Revenue = 115
Region=R17, _Measure=Cost = 66
Region=R17, _Measure=Revenue = 116
Region=R18, _Measure=Cost = 67
Region=R18, _Measure=Revenue = 117
Region=R19, _Measure=Cost = 68
Region=R19, _Measure=Revenue = 118
Region=R20, _Measure=Cost = 69
Region=R20, _Measure=Revenue = 119
Region=R21, _Measure=Cost = 70
Region=R21, _Measure=Revenue = 120
Region=R22, _Measure=Cost = 71
Region=R22, _Measure=Revenue = 121
Region=R23, _Measure=Cost = 72
Region=R23, _Measure=Revenue = 122
Region=R24, _Measure=Cost = 73
Region=R24, _Measure=Revenue = 123
Region=R25, _Measure=Cost = 74
Region=R25, _Measure=Revenue = 124
Region=R26, _Measure=Cost = 75
Region=R26, _Measure=Revenue = 125
Region=R27, _Measure=Cost = 76
Region=R27, _Measure=Revenue = 126
Region=R28, _Measure=Cost = 77
Region=R28, _Measure=Revenue = 127
Region=R29, _Measure=Cost = 78
Region=R29, _Measure=Revenue = 128
Region=R30, _Measure=Cost = 79
Region=R30, _Measure=Revenue = 129
+78
View File
@@ -26,6 +26,15 @@ test.describe("every field the render model projects reaches the screen", () =>
await expect(page.locator("#grid tbody th").first()).toHaveText("Espresso");
await expect(page.locator("#grid tbody td").first()).not.toHaveText("");
// `doc_name`, `doc_stored_as`: the title bar. improvise-z754 projected
// these and nothing painted them for a wave, which `render_surface`'s
// substring search cannot see — only a loaded page can (improvise-5jna).
// The dirty marker's own spelling is `web/tests/shell.test.js`: this
// deployment persists on every accepted batch, so `[+]` is transient.
await expect(page.locator("#doc-name")).toHaveText(
"Corner Coffee — 2025 (demo.improv)",
);
// `mode`, `hint`, `view_name`, `status`: the header's four facts.
await expect(page.locator("#mode")).toHaveText("NORMAL");
await expect(page.locator("#hint")).not.toHaveText("");
@@ -212,4 +221,73 @@ test.describe("a pointer event reaches the client and its answer is painted", ()
await expect(page.locator("#mode")).toHaveText("FORMULAS");
await expect(page.locator("#formulas li.selected")).toHaveCount(1);
});
// improvise-0x9u. The neighbouring state the assertion above cannot see: it
// presses Escape first, so it only ever clicks from a NON-editing page.
test("a click moves the selection from an editing state too", async ({ page }) => {
await openApp(page);
const cells = page.locator("#grid tbody td");
await cells.nth(0).click();
await expect(cells.nth(0).locator("input")).toHaveCount(1);
// No Escape between the two clicks — that is the whole test. The blur used
// to repaint and `replaceChildren` destroyed the <td> the mousedown landed
// on, so no click event was ever dispatched and the pointer was dead.
await cells.nth(4).click();
await expect(cells.nth(4).locator("input")).toHaveCount(1);
await expect(cells.nth(0).locator("input")).toHaveCount(0);
// The third edit of the same repair: `close` restores the DRAWN text, not
// the seed. `repaint` early-returns while `editing` is set, so a `18400`
// put back here stays on screen (improvise-0x9u).
await expect(cells.nth(0)).toHaveText("18,400");
await page.keyboard.press("Escape");
await expect(cells.nth(4)).toHaveClass(/selected/);
await expect(page.locator("#grid tbody td.selected")).toHaveCount(1);
});
});
test.describe("the selection is on screen", () => {
// improvise-jq8l: `web/app.js` called `scrollIntoView` NOWHERE, so a selection
// moved past the bottom of `#grid` simply stayed there, invisible, with no way
// back but scrolling by hand. Only a browser can see this: it is a fact about
// `#grid`'s own scroll box, not about the projection, which had already sent
// the row — the page renders more rows than fit, for the reason below.
test("a selection driven below the fold scrolls itself into view", async ({ page }) => {
// `harness/fixtures/tall.improv`: 30 rows in three groups, which is more
// than the box can show because `ROW_PX` (26) under-estimates the real
// 28.6px row and inline group rows are deliberately not charged for.
await page.goto("/app/index.html?doc=tall-scroll.improv&seed=tall.improv");
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
await expect(page.locator("#view-name")).toHaveText("Tall");
await expect(page.locator("#grid tbody th[scope=row]").first()).toHaveText("R01");
await page.locator("#grid").focus();
// Not vacuous by construction: if `#grid` does not overflow there is
// nothing to scroll and every assertion below passes on a broken page.
const overflow = await page
.locator("#grid")
.evaluate((g) => g.scrollHeight - g.clientHeight);
expect(overflow).toBeGreaterThan(0);
for (let i = 0; i < 25; i++) await page.keyboard.press("ArrowDown");
// The selected cell must be inside the box the user can see. Measured
// against `#grid`'s own rect rather than the window's, because `#grid` is
// the scroll container — the page itself does not scroll.
await expect
.poll(async () =>
page.locator("#grid").evaluate((g) => {
const sel = g.querySelector("td.selected");
if (!sel) return "no selected cell";
const cell = sel.getBoundingClientRect();
const box = g.getBoundingClientRect();
return cell.bottom <= box.bottom + 1 && cell.top >= box.top - 1;
}),
)
.toBe(true);
// And it really did have to move: a page that never scrolls leaves this 0.
expect(await page.locator("#grid").evaluate((g) => g.scrollTop)).toBeGreaterThan(0);
});
});
+21 -11
View File
@@ -10,6 +10,15 @@ import { test, expect } from "@playwright/test";
/// answers a missing seed with an EMPTY model -- so everything below asserts
/// labels the fixture really carries. "A grid is drawn" would pass on the
/// empty document a typo in the filename produces.
/// The first row that carries DATA. Not `tbody tr` first: since improvise-0o6x
/// a group header first on its axis is drawn, and `Coastal` is exactly that in
/// `grouped.improv` -- so the first row is a header alone, with no `td` at all.
const firstDataRow = (page) =>
page
.locator("#grid tbody tr")
.filter({ hasNot: page.locator("th.group") })
.first();
async function openGrouped(page, doc) {
await page.goto(`/app/index.html?doc=${doc}&seed=grouped.improv`);
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
@@ -20,9 +29,7 @@ async function openGrouped(page, doc) {
// half-drawn, and a one-shot read cannot wait.
await expect(page.locator("#grid thead tr")).toHaveCount(3);
await expect(page.locator("#grid thead th.group").first()).toHaveText("▼ Q1");
await expect(page.locator("#grid tbody tr").first().locator("th").last()).toHaveText(
"Main",
);
await expect(firstDataRow(page).locator("th").last()).toHaveText("Main");
await page.locator("#grid").focus();
}
@@ -58,7 +65,7 @@ test.describe("a grouped, multi-level document", () => {
// Two row categories, so the corner spans two — and the row headers below
// must agree with it, or the table is ragged.
expect(lines.every((l) => l.cornerSpan === 2)).toBe(true);
await expect(page.locator("#grid tbody tr").first().locator("th")).toHaveCount(2);
await expect(firstDataRow(page).locator("th")).toHaveCount(2);
// The level lines. The blanks are repeat suppression, decided in Rust by
// `AxisFrame::header_labels` — NOT an absent label, and not the page's
@@ -111,7 +118,7 @@ test.describe("a grouped, multi-level document", () => {
// that were merely "big enough" passes the pair above and fails here.
const columns = lines[2].cells.length;
expect(groups.reduce((n, c) => n + c.span, 0)).toBe(columns);
await expect(page.locator("#grid tbody tr").first().locator("td")).toHaveCount(columns);
await expect(firstDataRow(page).locator("td")).toHaveCount(columns);
});
// `RenderRow::Group` carries no cells, so the page CANNOT draw one with
@@ -125,12 +132,15 @@ test.describe("a grouped, multi-level document", () => {
const groupRows = page.locator("#grid tbody tr", {
has: page.locator("th.group"),
});
// Two, not three: `Coastal` is the frame's FIRST entry and a leading group
// header is dropped before it reaches the page — improvise-0o6x, open. If
// this count becomes 3, that bug is fixed and this line is the record of
// when. It is not an endorsement.
await expect(groupRows).toHaveCount(2);
await expect(groupRows).toHaveText(["▼ Inland", "▶ Upland"]);
// Three, not two: this WAS two, because `Coastal` is the frame's FIRST
// entry and a leading group header was dropped on the way to the page.
// improvise-0o6x fixed it, and this line is the record of when.
await expect(groupRows).toHaveCount(3);
await expect(groupRows).toHaveText([
"▼ Coastal",
"▼ Inland",
"▶ Upland",
]);
// The glyph is `indicator(collapsed)`: a page that hard-coded either one
// passes on one of these rows and fails on the other.
expect(await groupRows.locator("td").count()).toBe(0);
+96
View File
@@ -65,4 +65,100 @@ test.describe("the landing page", () => {
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
await expect(page.locator("#status")).toContainText("in-page worker");
});
// improvise-o31u. Since improvise-kh8 this page is the SITE ROOT, so the
// first thing a phone visitor sees is four panels of terminal. The player
// derives its font size from the container width, and 120 columns into the
// ~356px inside the page's gutters gave 4.83px — measured, not eyeballed.
//
// The instrument matters: `getBoundingClientRect` reports a height for a
// `content-visibility`-hidden child and heading text overflows its box
// without widening it, so neither "the element is there" nor a box
// measurement can see this. The COMPUTED FONT SIZE is what a reader sees.
test("the casts are legible on a phone, and the page still does not scroll sideways", async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/index.html");
await expect(page.locator(".ap-player")).toHaveCount(CASTS.length);
for (const name of CASTS) {
const player = page.locator(`#cast-${name}`);
await player.locator(".ap-play-button").click();
await expect(player.locator(".ap-line").first()).toBeVisible({ timeout: 20_000 });
const px = await player
.locator(".ap-terminal")
.evaluate((el) => parseFloat(getComputedStyle(el).fontSize));
expect(px, `${name}.cast renders at ${px}px on a 390px viewport`).toBeGreaterThanOrEqual(
10,
);
}
// The trade this fix must NOT make: a page that scrolls sideways breaks
// the whole document on a phone rather than one section of it. The
// overflow has to be contained inside `.player`.
const scrolls = await page.evaluate(() => ({
page: document.documentElement.scrollWidth - document.documentElement.clientWidth,
player: document.querySelector("#cast-pivot").scrollWidth >
document.querySelector("#cast-pivot").clientWidth,
}));
expect(scrolls.page).toBe(0);
expect(scrolls.player, "the cast must be the thing that scrolls").toBe(true);
});
// The other half: desktop is not collateral. 13.11px at 1280 was fine and
// nothing about this fix is allowed to touch it.
test("a desktop viewport is unchanged by the phone fix", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto("/index.html");
const player = page.locator("#cast-pivot");
await player.locator(".ap-play-button").click();
await expect(player.locator(".ap-line").first()).toBeVisible({ timeout: 20_000 });
const px = await player
.locator(".ap-terminal")
.evaluate((el) => parseFloat(getComputedStyle(el).fontSize));
expect(px).toBeGreaterThan(12);
expect(
await page.evaluate(
() =>
document.querySelector("#cast-pivot").scrollWidth <=
document.querySelector("#cast-pivot").clientWidth,
),
"a desktop player has nothing to scroll",
).toBe(true);
});
// improvise-2oxt. The four casts used to be `asciinema rec` of a SHELL, and
// the shell was the recorder's own: every one of them opened and closed on
// `(0) Mac:<hostname> ~gf/u/edwlan/improvise`, on what improvise-kh8 made the
// site root. `docs/record-casts.py` drives `improvise` as the pty's only
// child, so there is no shell for a prompt to leak from.
//
// Asserted over the FILE rather than the rendering, because that is where the
// leak lives: a player draws a prompt as ordinary text and a screenshot
// assertion would have to know what the hostname was.
test("no cast carries the recording machine's shell, clock or geometry", async ({
request,
}) => {
for (const name of CASTS) {
const lines = (await (await request.get(`/casts/${name}.cast`)).text()).trim().split("\n");
const header = JSON.parse(lines[0]);
// `env` carried $SHELL and $TERM off the recording machine; `timestamp`
// carried its clock. Neither is anything a visitor is owed.
expect(header.env, `${name}.cast carries the recorder's environment`).toBeUndefined();
expect(header.timestamp, `${name}.cast carries a recording clock`).toBeUndefined();
// The geometry is an INPUT now, and docs/index.html spells the same
// numbers: a 30-row player over a 37-row cast clips the tile bar and the
// status line, which is exactly where the interesting output is.
expect([header.width, header.height], `${name}.cast geometry`).toEqual([120, 37]);
// The OSC window-title sequence is the leak's signature: `improvise`
// emits none and a shell emits one per prompt, carrying the cwd. Checked
// over EVERY chunk, not just the first — the old casts leaked at the end
// as well, when the session exited back to the prompt.
const body = lines.slice(1).join("\n");
expect(body.includes("\\u001b]"), `${name}.cast sets a window title`).toBe(false);
}
});
});
+5
View File
@@ -231,6 +231,11 @@ doctrine(an_observation_correct_about_what_changed_can_be_wrong_about_when_it_is
doctrine(a_siblings_unmerged_pr_is_invisible_to_bd_search,'AGENTS.md requires a bd search before filing. In a parallel wave that is not sufficient: a fix living in a sibling agent UNMERGED PR is in no tracker state at all, so the search correctly returns nothing and the duplicate is filed anyway. Observed 2026-09-20 -- the tooling agent filed improvise-v5pc to restore a deleted help assertion while the harness agent was landing exactly that in PR #72. Neither agent erred. Check OPEN PRS as well as the tracker, and have the orchestrator reconcile filings against merged work at wave end rather than trusting each agent to have seen the others.').
doctrine(a_hard_reset_discards_knowledge_written_to_the_working_branch,'The orchestrator wrote doctrines to kb.pl on local main, pushed each as a branch, then ran git reset --hard gh/main to resync -- twice -- and silently discarded six clauses that existed only in the unmerged local commits. Nothing failed: kb_consistent passed on the reduced file, the counts simply read lower, and the loss was found only by grepping for clause names remembered from the session. Two rules follow. Write a fact ONCE, on the branch that will carry it to main, not on a local main that is going to be resynced. And after any reset or force-move of a branch holding knowledge, re-count the predicates and diff clause NAMES against what you believe you wrote -- a total that merely looks plausible is the same non-evidence a passing test count is.').
doctrine(a_checker_that_reads_headings_cannot_see_a_contradicting_body,'check-evidence validates that a heading names a test that exists. It says nothing about the PROSE under the heading, which is what a human actually reads. Demonstrated on this repo 2026-09-20: renaming a retired entry to the test that had been restored made the checker green while the body three paragraphs down still said \"The test no longer exists\" and \"nothing replaced the assertion\" -- one entry making mutually exclusive claims, and the gate could not see it because the gate was never about that. Found by review, not by the checker. The general form is that a mechanical check licenses exactly the claim it makes and no neighbouring one, so a rename is a read of the whole entry rather than an edit of its first line -- a_fact_stated_twice_is_changed_by_grep applied WITHIN a document rather than across them.').
doctrine(a_cost_note_names_the_half_the_author_was_thinking_about,'Twice now a reviewer has been asked to check a claim of the form \"X is now O(n) where it was O(1)\", and both times the interesting answer was not whether the claim was true but WHICH SIDE OF IT A READER WOULD ACT ON. On PR #76 the author wrote that the cost is \"O(cells) per CellData OR LayoutGeometry effect, where it was per LayoutGeometry effect\" -- which reads as though CellData is the newly burdened path. Measured at 18,000 cells it is the reverse: CellData was ALREADY O(cells) through record_fingerprints, so widening it is a constant factor (+27%), while LayoutGeometry went from O(config) to O(cells + config) and got 33x slower, 78us to 2.63ms, on the held-key-repeat operations. Not dishonest -- the author was thinking about WHICH EFFECTS NEWLY QUALIFY rather than WHICH PATH NEWLY CHANGED COMPLEXITY CLASS, and those are different questions with different answers. A cost note is worth a measurement precisely when the author can say confidently which half is worse, because that confidence is what stops anyone measuring.').
doctrine(name_the_test_that_fails_when_only_this_edit_is_reverted,'A repair is usually N edits, and the failure mode is that N-1 are guarded while the last is explained in a COMMENT instead. Three independent reviews in one wave found the same shape: improvise-mai9 (the helper was tested, the caller that actually rendered was not), improvise-0x9u (preventDefault and the close/dismiss split were both tested, the drawn-vs-seed restore was not, and reverting it shows 18400 where 18,400 belongs on the shipped demo), improvise-o31u (the phone arm was measured, the desktop arm was justified by a mechanism measurement contradicts). The unguarded edit is reliably the one the author understood BEST -- which is why it carries the most careful comment, and why it felt covered: the comment is doing the work the test should, and it reads as evidence because it is the most thoughtful thing on the screen. The check is per-edit, not per-fix: for each distinct edit, name the test that fails when ONLY that edit is reverted. A line with a comment and no answer is the line to inject.').
doctrine(a_stale_list_of_what_is_missing_is_worse_than_no_list,'repo-map gotcha 24 named TransposeAxes and CycleAxis as the only effects calling reset_cursor, and named SetAxis, HideItem, ShowItem, ToggleGroup, TogglePruneEmpty, AddCategory, RemoveCategory, AddItem and RemoveItem as renumbering the grid WITHOUT resetting. improvise-9mo9 had closed that the day before: all of them reset, thirteen in total. So the paragraph named as ABSENT the exact set that was by then PRESENT, and read as a survey of outstanding work rather than as history -- strictly worse than saying nothing, because it invites an agent to re-fix what is fixed. A list of what is missing decays in the one direction that looks like a to-do. Where the set is derivable, publish the DERIVATION beside it: awk over impl Effect blocks for reset_cursor, brace-matched rather than grepped.').
doctrine(two_counts_from_two_derivations_are_not_one_fact,'Thirteen effects call reset_cursor and thirteen declare Model(LayoutGeometry). The counts are equal and the SETS ARE NOT: CreateView, DeleteView and SetNumberFormat declare the kind and never reset; SetPageSelection resets only conditionally. Equal length invites reconciling one list to the other, which silently changes what one of them means. Same shape as the wasm bundle being seven crates while its CI gate covers eight -- neither number is the other one typo.').
doctrine(verify_the_claim_you_are_about_to_publish_as_the_correction,'While correcting repo-map gotcha 24 for being stale, the orchestrator asserted from a reviewer report that CreateView, DeleteView and SetNumberFormat declare LayoutGeometry and never reset the cursor. Measuring it before merging found the claim TRUE but UNDERSTATED: the sets differ in BOTH directions, three each way -- AddItem, AddItemInGroup and RemoveItem reset while declaring CellData. And the orchestrator naive awk reported fourteen LayoutGeometry effects, picking up HelpPageSet, which is the exact artefact the same document already warns about two paragraphs away. A correction is a claim like any other and decays the same way; the person writing the fix is the one least likely to check it, because they have just read the evidence and feel current.').
work_done('improvise-8tk',bug_fix,'ZZ/:wq quit even when the save failed, silently discarding the model. Save now aborts the effect batch on failure; save_to_current_path/1 returns Result<String,String>; three tests in ui/effect.rs.').
work_done('improvise-907',bug_fix,'Import wizard step machine redesign: validation_error() gate at ReviewProposals (no-category and over-limit), history-stack previous() bound to Left on every step, and Done made a real confirmation step holding the built workbook. Superseded improvise-gfq, improvise-o1c, improvise-c6j.').
work_done('improvise-6sq',wont_do,'Merging EditOrDrill and EnterEditAtCursorCmd into one EnterEdit{mode} is not safe: they differ in WHEN the cell value is read, not in a mode parameter. Added ui/app.rs::tests::i_and_o_diverge_on_an_empty_records_grid as executable evidence.').
+124 -11
View File
@@ -193,17 +193,28 @@ fn draw_popup_frame(f: &mut Frame, popup: Rect, title: &str, border_color: Color
inner
}
/// Terminal styling for the mode indicator. Its wildcard arm is a known
/// defect (improvise-zx8t) — see `context/code/tui.md`.
/// Terminal styling for the mode indicator. Exhaustive on purpose, so a new
/// `AppMode` fails the build here (improvise-zx8t) — `context/code/tui.md`.
fn mode_style(mode: &AppMode) -> Style {
match mode {
AppMode::Editing { .. } | AppMode::RecordsEditing { .. } => {
Style::default().fg(Color::Black).bg(Color::Green)
}
AppMode::CommandMode { .. } => Style::default().fg(Color::Black).bg(Color::Yellow),
AppMode::TileSelect => Style::default().fg(Color::Black).bg(Color::Magenta),
_ => Style::default().fg(Color::Black).bg(Color::DarkGray),
}
let bg = match mode {
AppMode::Editing { .. } | AppMode::RecordsEditing { .. } => Color::Green,
AppMode::CommandMode { .. } => Color::Yellow,
AppMode::TileSelect => Color::Magenta,
AppMode::Normal
| AppMode::RecordsNormal
| AppMode::FormulaEdit { .. }
| AppMode::FormulaPanel
| AppMode::CategoryPanel
| AppMode::CategoryAdd { .. }
| AppMode::ItemAdd { .. }
| AppMode::ViewPanel
| AppMode::ImportWizard
| AppMode::ExportPrompt { .. }
| AppMode::SavePrompt { .. }
| AppMode::Help
| AppMode::Quit => Color::DarkGray,
};
Style::default().fg(Color::Black).bg(bg)
}
fn draw(f: &mut Frame, app: &App) {
@@ -377,6 +388,17 @@ fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) {
);
}
/// The minibuffer row, with any pending status message riding on its right.
/// The minibuffer owns the bottom row and used to hide it — tui.md §3.
fn minibuffer_line(prompt: &str, buf: &str, status_msg: &str, width: u16) -> String {
let left = format!("{prompt}{buf}▌");
let right = match status_msg.is_empty() {
true => String::new(),
false => format!(" {status_msg} "),
};
fill_line(left, &right, width)
}
fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
if let Some(mb) = app.view_state.mode.minibuffer() {
let buf = app
@@ -385,7 +407,7 @@ fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
.get(&mb.buffer_key)
.map(|s| s.as_str())
.unwrap_or("");
let text = format!("{}{}▌", mb.prompt, buf);
let text = minibuffer_line(&mb.prompt, buf, &app.view_state.status_msg, area.width);
f.render_widget(
Paragraph::new(text).style(
Style::default()
@@ -516,6 +538,97 @@ fn draw_welcome(f: &mut Frame, area: Rect) {
}
}
#[cfg(test)]
mod bottom_bar_tests {
use super::{draw_bottom_bar, minibuffer_line};
use crate::ui::app::{App, AppMode};
use crate::workbook::Workbook;
use ratatui::{Terminal, backend::TestBackend, layout::Rect};
/// The bottom row as the terminal really receives it, so the assertion is
/// about `draw_bottom_bar`'s wiring and not about a helper it may not call.
fn bottom_row(app: &App, width: u16) -> String {
let mut terminal = Terminal::new(TestBackend::new(width, 1)).expect("test backend");
terminal
.draw(|f| draw_bottom_bar(f, Rect::new(0, 0, width, 1), app))
.expect("draw");
terminal
.backend()
.buffer()
.content()
.iter()
.map(|c| c.symbol())
.collect()
}
fn app_with_minibuffer_and_message() -> App {
let mut app = App::new(Workbook::new("T"), None);
app.view_state.mode = AppMode::category_add();
let key = app
.view_state
.mode
.minibuffer()
.expect("CategoryAdd carries a MinibufferConfig")
.buffer_key
.clone();
app.view_state.buffers.insert(key, "North".to_string());
app.view_state.status_msg = "category already exists".to_string();
app
}
/// improvise-mai9, and the ONE assertion that can fail for the real defect
/// — see context/test-evidence/tui.md.
#[test]
fn a_status_message_reaches_the_bottom_row_while_a_minibuffer_is_open() {
let app = app_with_minibuffer_and_message();
let row = bottom_row(&app, 80);
assert!(
row.contains("North"),
"the minibuffer must still own the left of the row: {row:?}"
);
assert!(
row.contains("category already exists"),
"the status message is never drawn — the bug is in draw_bottom_bar, \
which used to render the minibuffer INSTEAD of the status line, so \
a test that calls the composing helper directly cannot see it: {row:?}"
);
}
/// The minibuffer is still the thing that owns the row: a fix that dropped
/// to `draw_status` would pass the assertion above on the message alone.
#[test]
fn the_minibuffer_prompt_is_not_displaced_by_the_message() {
let app = app_with_minibuffer_and_message();
let row = bottom_row(&app, 80);
let prompt_at = row.find("North").expect("the buffer is drawn");
let message_at = row.find("category already exists").expect("the message");
assert!(
prompt_at < message_at,
"the prompt leads and the message follows it: {row:?}"
);
}
/// The guard's ONLY observable effect, in the narrow band where the two
/// arms differ at all. Why the obvious negative arm cannot: tui.md.
#[test]
fn an_empty_message_never_pushes_a_row_that_fits_past_its_width() {
// Spelled here, NOT taken from `minibuffer_line`: a bound read off the
// function under test moves with the fault and skips the failing band.
let fits_from = "Category: North\u{258c}".chars().count() as u16;
for width in fits_from..fits_from + 24 {
let line = minibuffer_line("Category: ", "North", "", width);
assert!(
line.chars().count() <= width as usize,
"width {width}: an empty status still composed {} columns \
from {line:?} — an unconditional append pads for a message \
that is not there",
line.chars().count()
);
}
}
}
#[cfg(test)]
mod key_conversion_tests {
use super::*;
+16 -8
View File
@@ -140,20 +140,23 @@ impl<'a> GridWidget<'a> {
format!("{:<width$}", "", width = row_header_width as usize),
Style::default(),
);
let mut prev_group: Option<String> = None;
// The run rule's one home, plus the TUI's one divergence from
// it at a scrolled offset — `context/code/protocol.md` §3a.
let run_heads = cols.group_run_heads();
for ci in visible_col_range.clone() {
let x = col_x_at(ci);
if x >= area.x + area.width {
break;
}
let cw = col_w_at(ci) as usize;
let col_group = col_groups.get(ci).copied().flatten();
let group_name = col_group.map(|g| g.name.clone());
let label = match (group_name != prev_group, col_group) {
(true, Some(g)) => format!("{} {}", indicator(g.collapsed), g.name),
_ => String::new(),
let head = match ci == visible_col_range.start {
true => col_groups.get(ci).copied().flatten(),
false => run_heads.get(ci).copied().flatten(),
};
let label = match head {
Some(g) => format!("{} {}", indicator(g.collapsed), g.name),
None => String::new(),
};
prev_group = group_name;
buf.set_string(
x,
y,
@@ -231,6 +234,9 @@ impl<'a> GridWidget<'a> {
break;
}
match entry {
// A boundary, not a row: nothing is painted and no line is
// consumed (improvise-gd83).
FrameEntry::GroupEnd => continue,
FrameEntry::Group(group) => {
let label = format!("{} {}", indicator(group.collapsed), group.name);
buf.set_string(
@@ -895,12 +901,15 @@ mod tests {
assert_eq!(text, GROUPED_FRAME, "\n--- got ---\n{text}\n--- end ---");
}
/// Re-pinned for improvise-0o6x: the leading `▼ Co…` is the ONE line that
/// moved, and its absence was the bug. context/test-evidence/tui.md.
const GROUPED_FRAME: &str = concat!(
"┌ View: Default ───────────────────────────┐\n",
"│ [Payer = Alice] │\n",
"│ ▼ Q1 ▼ Q2 │\n",
"│ Jan Apr │\n",
"│──────────────────────────────────────────│\n",
"│▼ Co…────────── │\n",
"│East 100 100 │\n",
"│▼ In…────────── │\n",
"│West 50 50 │\n",
@@ -910,7 +919,6 @@ mod tests {
"│ │\n",
"│ │\n",
"│ │\n",
"│ │\n",
"└──────────────────────────────────────────┘",
);
}
+82 -23
View File
@@ -7,7 +7,7 @@ use improvise::ui::grid::GridWidget;
use improvise::view::{Axis, GridLayout};
use improvise::workbook::Workbook;
use improvise_command::ui::project;
use improvise_protocol::{GridCache, Snapshot, ViewState};
use improvise_protocol::{FrameEntry, GridCache, Snapshot, ViewState};
use improvise_wasm_client::render::{RenderModel, RenderRow};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
@@ -278,37 +278,96 @@ fn the_two_front_ends_draw_the_same_column_header() {
.iter()
.map(|g| (g.label.as_deref(), g.span))
.collect();
// improvise-gd83: MEMBERSHIP, not nearest-preceding. Sep is ungrouped, so
// the run ends before it and resumes under the next measure's Q1 header.
assert_eq!(
named,
vec![(Some("Q1"), 6)],
"Q1 covers all six columns, INCLUDING the two under the ungrouped \
Sep — `expand_category` emits a header on entering a group and no \
boundary on leaving one, so the nearest-preceding walk keeps \
answering Q1 (see `AxisFrame::group_of`). That is `GridLayout`'s \
rule and the TUI paints the same span; it is reproduced here rather \
than corrected, because correcting it changes what the TUI draws. \
See `THE_MEMBERSHIP_CORRECT_SPANS` for what improvise-gd83 makes \
this line say."
vec![(Some("Q1"), 2), (None, 1), (Some("Q1"), 2), (None, 1)],
"the two Q1 runs are separated by the ungrouped Sep; a run that \
swallowed it would be `expand_category` leaking a group across its \
own boundary (see `AxisFrame::group_of`)"
);
// A DELIBERATELY WRONG constant and a live tripwire — do not remove it.
// Derived, not guessed: see `context/test-evidence/tui.md`.
const THE_MEMBERSHIP_CORRECT_SPANS: [(Option<&str>, usize); 4] =
[(Some("Q1"), 2), (None, 1), (Some("Q1"), 2), (None, 1)];
assert_ne!(
named.as_slice(),
THE_MEMBERSHIP_CORRECT_SPANS.as_slice(),
"when this stops differing, improvise-gd83 is fixed and the \
assertion above should become the constant"
assert!(
named.iter().any(|(label, _)| label.is_none()),
"precondition: the fixture must contain an UNGROUPED column, or this \
test cannot see a leaked group at all"
);
// And the group line the TUI paints names the same group.
// improvise-8qlr: the TUI's group line, compared EXACTLY. A `contains`
// passes while the TUI labels every column, which is a different grid.
let group_line = &lines[rule - levels - 1];
assert!(
group_line.contains("▼ Q1"),
"the TUI paints the same group above the same columns: {group_line:?}"
let expected: Vec<String> = model
.col_groups
.iter()
.flat_map(|g| {
let head = match &g.label {
Some(name) => format!("{} {name}", indicator(g.collapsed)),
None => String::new(),
};
std::iter::once(head).chain(std::iter::repeat_n(String::new(), g.span - 1))
})
.collect();
assert_eq!(
&tui_header_cells(group_line, &cache),
&expected,
"the TUI's group line and the browser's runs disagree — one of them \
coalesces the runs itself instead of asking `group_runs`"
);
}
/// improvise-0o6x: a group FIRST on its axis. Separate from `two_level_rows`,
/// which orders Carol first to dodge this case on purpose.
fn leading_group_rows() -> Workbook {
let mut m = Workbook::new("Test");
m.add_category("Recipient").unwrap();
m.add_item_in_group("Recipient", "Alice", "Fam").unwrap();
m.add_item_in_group("Recipient", "Bob", "Fam").unwrap();
m.add_item("Recipient", "Carol").unwrap();
m.add_item("_Measure", "Revenue").unwrap();
m.active_view_mut().set_axis("Recipient", Axis::Row);
m.active_view_mut().set_axis("_Measure", Axis::Column);
for r in ["Alice", "Bob", "Carol"] {
m.model.set_cell(
coord(&[("Recipient", r), ("_Measure", "Revenue")]),
CellValue::Number(1.0),
);
}
m
}
/// A collapsed group at position zero is otherwise a trap: its rows are hidden
/// and its header is the only affordance. context/test-evidence/tui.md.
#[test]
fn a_group_header_first_on_its_axis_reaches_both_front_ends() {
for collapse in [false, true] {
let mut wb = leading_group_rows();
if collapse {
wb.active_view_mut()
.toggle_group_collapse("Recipient", "Fam");
}
let (_app, cache) = whole_grid(&mut wb);
assert!(
matches!(cache.row_frame.entries.first(), Some(FrameEntry::Group(g)) if g.name == "Fam"),
"precondition (collapse={collapse}): the frame must LEAD with the \
header, or body_from has nothing at position zero to drop"
);
let browser = browser_rows(&cache);
assert!(
matches!(browser.first(), Some(RenderRow::Group { label, .. }) if label == "Fam"),
"the browser dropped the leading group header: {browser:?}"
);
let painted = tui_row_headers(&wb, &cache);
assert_eq!(
painted.first().map(|r| r.join("").trim().to_string()),
Some(format!("{} Fam", indicator(collapse))),
"the TUI dropped the leading group header: {painted:?}"
);
}
}
/// The bug's own words, separate because the agreement above would also hold
/// if *both* ends started slash-joining.
#[test]
+42 -8
View File
@@ -7,6 +7,7 @@ import {
HANDSHAKE_TIMEOUT_MS,
ROW_PX,
createShell,
documentTitle,
extentOf,
frameCostOf,
gridPlan,
@@ -35,6 +36,7 @@ const el = {
help: document.getElementById("help"),
status: document.getElementById("status"),
viewName: document.getElementById("view-name"),
docName: document.getElementById("doc-name"),
};
await init();
@@ -60,8 +62,8 @@ function sendViewport() {
const box = el.grid.getBoundingClientRect();
const metrics = { rowPx: ROW_PX, colPx: COL_PX };
const { rows, cols } = extentOf(box, frameCost, metrics);
// `client.set_viewport`, NOT the associated `Client.viewport`: the same
// frame, but it also RECORDS the window. Why that matters: clients.md.
// `set_viewport` RECORDS the window as well as framing it; the frame-only
// twin beside it is deleted, so there is nothing to call by mistake (seik).
transport.send(client.set_viewport(rows, cols));
}
@@ -162,6 +164,13 @@ function repaint() {
paintMinibuffer(model);
shell.setModel(model.status);
el.viewName.textContent = model.view_name;
// The title bar, off the cache `src/draw.rs::draw_title` folds — one
// derivation, both front ends (improvise-z754, improvise-5jna).
el.docName.textContent = documentTitle(
model.doc_name,
model.doc_dirty,
model.doc_stored_as,
);
el.grid.replaceChildren(gridTable(model));
// The flag is the client's answer, not the page's: `C` toggles it and the
// TUI draws the panel on exactly the same condition (improvise-wsaw).
@@ -173,6 +182,13 @@ function repaint() {
client.click_formula(i),
);
// The selection must be on screen. `#grid` scrolls on its own — ROW_PX and
// COL_PX under-estimate and group rows are uncharged — so a selection the
// projection did send can still sit below the fold (improvise-jq8l).
el.grid
.querySelector("td.selected")
?.scrollIntoView({ block: "nearest", inline: "nearest" });
// What the frame costs, so the NEXT viewport announcement is honest.
// Inline group ROWS are deliberately NOT counted — context/code/clients.md.
const next = frameCostOf(model);
@@ -299,7 +315,12 @@ function gridTable(model) {
// Every cell moves the selection; an editable one also opens its editor,
// which is the pointer's `i`. Not repainted in that branch: `edit` has
// set `editing`, and a repaint would take the <input> away.
td.addEventListener("click", () => {
// On MOUSEDOWN, and defaulted away: from an editing state the input's
// blur repainted first and this <td> was gone before `click` could fire
// (improvise-0x9u). See context/code/clients.md.
td.addEventListener("mousedown", (event) => {
event.preventDefault();
if (editing) editing.close(false);
const frame = client.click_cell(cell.row, cell.col);
if (frame) transport.send(frame);
if (cell.editable) edit(td, cell.row, cell.col, cell.editText);
@@ -314,10 +335,12 @@ function gridTable(model) {
* back is the server's, never the string typed here. */
function edit(td, row, col, seed) {
if (editing) return;
editing = { row, col };
// `seed` is `RenderCell::edit_text` -- the STORED value, not the drawn one:
// `td.textContent` has been through the number format.
const original = seed;
// What was on the screen, kept so closing can put it back: `close` no longer
// always repaints, so restoring `seed` would leave `1200` where `1,200` was.
const drawn = td.textContent;
const input = document.createElement("input");
input.value = original;
td.replaceChildren(input);
@@ -327,6 +350,8 @@ function edit(td, row, col, seed) {
// Enter removes the focused <input>, which fires `blur`, which calls
// `close` again. Harmless before, but only by accident.
let closed = false;
// `close` does NOT repaint: a caller about to open another editor on a live
// <td> must not have the table rebuilt under it (improvise-0x9u).
const close = (commit) => {
if (closed) return;
closed = true;
@@ -337,19 +362,28 @@ function edit(td, row, col, seed) {
// follows restores whatever the server now says is there.
if (frame) transport.send(frame);
}
td.replaceChildren(original);
td.replaceChildren(drawn);
painted = -1;
};
const dismiss = (commit) => {
close(commit);
repaint();
};
editing = { row, col, close };
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") close(true);
else if (e.key === "Escape") close(false);
if (e.key === "Enter") dismiss(true);
else if (e.key === "Escape") dismiss(false);
// The editor owns the keyboard while it is open; the page-level handler
// below must not also see these.
e.stopPropagation();
});
input.addEventListener("blur", () => close(false));
// Removing the focused <input> fires `blur` SYNCHRONOUSLY, so a caller that
// already closed this editor must not have the table rebuilt from inside its
// own `close` — that is what detached the <td> in improvise-0x9u's repair.
input.addEventListener("blur", () => {
if (!closed) dismiss(false);
});
}
/** A side panel. An empty row list is what says it is closed — the page asks
+1
View File
@@ -34,6 +34,7 @@
-->
<header>
<h1>improvise</h1>
<span id="doc-name"></span>
<span id="view-name"></span>
<span id="mode"></span>
<span id="hint"></span>
+6
View File
@@ -16,6 +16,12 @@ export function composeStatus(shell, model) {
return [shell, model].filter(Boolean).join(" · ");
}
/** The title bar's one line, in the order and spelling `src/draw.rs::draw_title`
* uses. The three facts are the host's; only the joining is here (clients.md). */
export function documentTitle(name, dirty, storedAs) {
return `${name}${storedAs ? ` (${storedAs})` : ""}${dirty ? " [+]" : ""}`;
}
/** The collapse glyph. `collapsed` is the fact, decided by the view and
* carried on the frame; only this spelling is the page's (gotcha 26). */
export function indicator(collapsed) {
+4
View File
@@ -49,6 +49,10 @@ header h1 {
color: var(--accent);
}
#doc-name {
opacity: 0.85;
}
#mode {
color: var(--accent);
}
+25
View File
@@ -86,6 +86,31 @@ test("the page still reaches the client through a handful of calls", () => {
);
});
/// Methods on `Client` the page does not call, each with the reason it is
/// still exported. A door with no caller and no entry here is improvise-seik:
/// the dangerous kind, because calling the wrong one of a pair is silent.
// What this CANNOT see, exactly as UNPAINTED cannot: `callsOn` is a source
// scan, so a `client.foo()` inside a function nothing calls reads as called.
const UNCALLED = {
free: "wasm-bindgen's own destructor, not ours to remove",
initSync: "wasm-bindgen's own module loader, beside the default export",
revision: "on_frame returns it; a page that stores it can skip a repaint",
};
test("the bundle exports no client door the page has no reason to hold", () => {
const declared = declarations("pkg/improvise_wasm_client.d.ts");
const called = new Set(callsOn("app.js", "client").map((c) => c.name));
// `constructor` is `new Client()`, which `callsOn` cannot see as a call.
const spare = [...declared.keys()].filter(
(name) => name !== "constructor" && !called.has(name),
);
assert.deepEqual(
spare.sort(),
Object.keys(UNCALLED).sort(),
"an exported method nothing calls — delete it, or add it to UNCALLED with the reason",
);
});
test("every worker call web/worker.js makes is declared, with that arity", () => {
const declared = declarations("worker-pkg/improvise_worker_server.d.ts");
const calls = callsOn("worker.js", "server");
+4 -8
View File
@@ -17,14 +17,10 @@ const code = (rel) =>
/// Fields the page deliberately does not read, each with the reason. A field
/// belongs here only with an argument; the list is the debt, written down.
// improvise-z754 put the title bar's facts in one shared cache and taught
// `src/draw.rs::draw_title` to fold it; this page never learned to. That is
// improvise-5jna, open, and this list is what stops it being forgotten.
const UNPAINTED = {
doc_name: "improvise-5jna: the title bar is drawn by the TUI and not here",
doc_dirty: "improvise-5jna",
doc_stored_as: "improvise-5jna",
};
// Empty since improvise-5jna painted the title bar. Note what this check CANNOT
// see, and why the harness asserts the painted text too: it is a substring
// search, so a field named by a function nothing calls reads as painted.
const UNPAINTED = {};
/// The `pub` fields of `RenderModel`, read off the Rust that defines it.
function renderModelFields() {
+15
View File
@@ -7,6 +7,7 @@ import assert from "node:assert/strict";
import {
composeStatus,
createShell,
documentTitle,
extentOf,
frameCostOf,
indicator,
@@ -160,3 +161,17 @@ test("the collapse glyph points right when collapsed", () => {
assert.equal(indicator(true), "▶");
assert.equal(indicator(false), "▼");
});
test("the title bar spells the document the way the TUI's own title does", () => {
// `src/draw.rs::draw_title`: name, then what it is stored as in parens, then
// the dirty marker. The harness cannot pin `[+]` — the standalone deployment
// persists on every accepted batch, so the marker is transient there.
assert.equal(
documentTitle("Corner Coffee", true, "demo.improv"),
"Corner Coffee (demo.improv) [+]",
);
assert.equal(documentTitle("Corner Coffee", false, "demo.improv"), "Corner Coffee (demo.improv)");
// A host with nowhere to write it: `stored_as` is `None` over the wire, which
// arrives as `null`, and an empty pair of parens would be a lie about storage.
assert.equal(documentTitle("Untitled", true, null), "Untitled [+]");
});