Skip to content

Commit c26d643

Browse files
feat(spec): a dataset measure declares its polarity, so a lower-is-better KPI draws a rising delta as bad news (#26739)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBSoRovYVnrLPiM3xeEDVWqq)_ Fixes #26602 Clause-②: yes (widening) Metadata-protocol: added Skills-docs: updated here — skills/objectstack-ui/rules/dashboards.md (the metric-widget `compareTo` bullet and the measure presentation line), skills/objectstack-ui/evals/analytics-inline-vs-dataset.json (eval 8 now asks for the polarity and asserts `polarity: 'lower'`), content/docs/ui/dashboards.mdx ## 维护者速读 - 改了什么:数据集度量新增可选键 `polarity`(`higher` / `lower` / `neutral`),分析服务把它带到答案的列描述上,仪表盘 KPI 卡片和交叉表单元格按它给环比变化上色。 - 为什么改:「平均审批周转」这类越低越好的指标,周转变慢 23% 却显示绿色上升箭头,把坏消息画成了好消息。 - 风险与回滚:未声明 `polarity` 的度量颜色与现在完全一致(上升绿、下降红),纯新增;回滚即还原本 PR。 - 先看:`packages/spec/src/ui/dataset.zod.ts`(新键与取值)、`ui/packages/plugin-dashboard/src/DatasetWidget.tsx` 的 `deltaToneClass`、`packages/services/service-analytics/src/analytics-service.ts` 的 `enrichResultColumns` 一行转发。 - 两分钟验证:在 `ui/` 目录执行 `pnpm exec vitest run packages/plugin-dashboard/src/__tests__/DatasetWidget.polarity-26602.test.tsx`,预期 9 个用例全部通过。 - 需要你做的:本 PR 触及 `skills/**`(Tier H),合并需要你的批准;另请确认 eval 文件 token 上限按裁决 A 抬高 13。 ## What changes **Where the key lives, and why.** The card offered two seats: the widget's `compareTo` or the dataset measure. Measured at `origin/main` 4373103, the widget's `compareTo` cannot carry it: - `DatasetWidget` forwards `widget.compareTo` verbatim into the dataset selection. - The wire `DatasetCompareToSchema` (`packages/spec/src/api/analytics.zod.ts`) is a `strictObject`, so a new member there would be refused on every query. - `packages/spec/src/ui/dashboard-compareto.test.ts` pins "a parsed widget `compareTo` is assignable to `DatasetCompareTo` with no mapping". The widget's slot is a thin projection of the executor's own directive, by design. So the key is declared on the **dataset measure only**, the semantic layer, where every widget over the measure agrees. A count whose meaning flips with a filter (overdue vs resolved records) is two measures, each with its own measure `filter` and its own polarity. A widget-level override would be a later, pure widening if a consumer needs one. **The values.** `DatasetMeasurePolarity = z.enum(['higher', 'lower', 'neutral'])`. They name the better direction rather than a sign or a boolean, so `neutral` (no good/bad colour, for a measure where neither direction is news) is a peer value and not a third state of a flag. Undeclared keeps today's colours exactly (a rise green, a fall red). Nothing infers a polarity from the measure's name or unit. - **Spec** (`packages/spec/src/ui/dataset.zod.ts`): `DatasetMeasureSchema.polarity`, optional, no default materialized. Near-miss keys `betterWhen` (the card's own proposal), `direction`, `goodDirection`, `trendDirection` are pointed at `polarity`; the boolean spellings `higherIsBetter` / `lowerIsBetter` are answered with the three values. - **Wire** (`packages/spec/src/api/analytics.zod.ts`, `packages/spec/src/contracts/analytics-service.ts`): the column descriptor declares `fields[].polarity` with the same enum, mirrored in `AnalyticsResult` and `AnalyticsResultResponseSchema` (the existing compile-time drift guard binds the two). - **Producer** (`packages/services/service-analytics/src/analytics-service.ts#enrichResultColumns`): one line relays the authored value, on the live query and the draft preview alike, the `__compare` column included. Undeclared measures and dimension columns carry no key, so an answer over a dataset that declares none is byte-identical. - **Renderer** (`ui/packages/plugin-dashboard/src/DatasetWidget.tsx`): one helper, `deltaToneClass(direction, polarity)`, colours both surfaces that colour a delta, the metric tile and the cross-tab cell. `lower` draws a rise red and a fall green; `neutral` draws neither (the tile's muted text colour); `higher` and undeclared draw a rise green and a fall red. The arrow keeps the number's direction under every polarity, and a flat delta stays muted. - **Metadata form** (`packages/spec/src/ui/dataset.form.ts`): the measures repeater offers `polarity`, labelled in `zh-CN` / `ja-JP` / `es-ES` (`packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts`). - **Liveness** (`packages/spec/liveness/dataset.json`): `measures.polarity` live, with the renderer as evidence and `enrichResultColumns` as producer. - **Skills and docs**: the metric-widget bullet in `skills/objectstack-ui/rules/dashboards.md` teaches the key (paid for inside the file, and the file's ceiling is lowered 6239 → 6221); eval 8 asks for it; `content/docs/ui/dashboards.mdx` gains a presentation-home row and a sentence on the `compareTo` row. - **Changesets**: `.changeset/26602-measure-polarity.md` (`@objectstack/spec` minor, `@objectstack/service-analytics` minor, `@objectstack/platform-objects` minor for the four regenerated `metadata-forms` translation catalogs it publishes under `./apps`), `.changeset/26602-delta-polarity.md` (`@object-ui/plugin-dashboard` minor; ui changesets live in the root directory since #25745). ## Why A KPI's colour is read before its number. hotclm's average approval turnaround tile showed "↑ 23% vs previous period" in green for a turnaround that got 23% longer. ## Tests All at head `aef918b95`, on `origin/main` 4373103 (main has not moved since the branch was cut). - `packages/spec/src/ui/dataset-measure-polarity.test.ts` (17): the three values parse and keep their value; the closed vocabulary; an absent key materializes nothing; `up` / `down` / `good` / `inverse` / `true` refused at the key (`invalid_value` at `['polarity']`); `betterWhen` and `direction` pointed at `polarity`; the boolean spellings answered with the values; the wire descriptor accepts exactly the measure's vocabulary and stays optional. - `packages/services/service-analytics/src/__tests__/measure-column-polarity.test.ts` (3), each on the live path and the draft preview: each declared value relayed as written; no key on an undeclared measure or a dimension column; the `__compare` column carries its measure's polarity. - `ui/packages/plugin-dashboard/src/__tests__/DatasetWidget.polarity-26602.test.tsx` (9): the metric tile for `lower` (rise bad, fall good, arrows kept), `neutral` (no tone, muted), `higher`, undeclared (class string byte-identical to today's), and a flat delta muted under every polarity; the cross-tab cell for `lower`, `neutral` and undeclared. - Reverse verification, through `scripts/ablation-replace.mjs`, each restored to `HEAD` by blob hash: deleting the relay line in `enrichResultColumns` turns 2 of 3 service tests red (the two positive ones; the absence test stays green); replacing `deltaToneClass` with the old direction-only colouring turns 4 of 9 renderer tests red (both tile `lower` cases, the cell `lower` and the cell `neutral` cases). - Full suites: `@objectstack/spec` Test Files 732 passed (732), Tests 21938 passed, 1 todo. `@objectstack/service-analytics` Test Files 192 passed (192), Tests 4650 passed, 282 skipped. `@objectstack/platform-objects` Test Files 96 passed (96), Tests 1361 passed. `@object-ui/plugin-dashboard` Test Files 190 passed (190), Tests 1864 passed, 6 skipped. - Typecheck: `@objectstack/spec`, `@objectstack/service-analytics`, `@objectstack/platform-objects` exit 0. `@object-ui/plugin-dashboard` type-check NOT MEASURED locally: it exits 2 with 133 errors, every one a `Cannot find module '@object-ui/*'` or an implicit `any` downstream of one, because the sibling `@object-ui/*` packages have no built declarations in this container. CI's `Type Check UI` is the reading. ## Acceptance notes - **Skills token ceiling raised once, under a standing ruling.** `skills/objectstack-ui/evals/analytics-inline-vs-dataset.json` grows +29 tokens for the polarity in eval 8, part-funded by trimming two parentheticals, eval 2's note that restated its own expected output and eval 8's "(a rise draws red)". The file is at 5443 bytes. The balance (+13, 1348 → 1361) is raised under the maintainer ruling recorded beside that row in `scripts/check-skills-token-ratchet.mjs`, quoted verbatim: 「A:先削重复,余额按实测提高」. Seat: please confirm that ruling covers this eval; the alternative is dropping the eval change and declaring the skills half `follow-up owed`. - Skills readings (lines): `rules/dashboards.md` 464 → 462; `evals/analytics-inline-vs-dataset.json` 87 → 87 (5389 → 5462 bytes); all `SKILL.md` 4600 → 4600; whole `skills/` tree 19192 → 19190. - On the metric tile, `cn` is tailwind-merge, so the tile's explicit muted class for `neutral` would win over a tone class even without the helper's `neutral` branch. The cross-tab cell has no such class, which is why its `neutral` pin is the one that goes red when the helper's branch is removed. - Not carried (noted, not filed): the inline `ObjectMetricWidget` / `MetricWidget` trend path reads no dataset measure, so it has no polarity to read; `DatasetReportRenderer` draws no comparison delta. ## Verification - `pnpm --filter @objectstack/spec check:generated --fix` regenerated the 5 stale artifacts (api-surface, export-origins, declaration-map, references docs, liveness counts) from a rebuilt dist and re-checked them green; the other 9 were already current. - `pnpm --filter @objectstack/spec check:empty-state`: all classified (no new permissive empty-state statement). - `pnpm check:i18n`: OK, 10 packages in sync. The dataset form's measures repeater offers `polarity` (the spec's form/Zod reconciliation pin required it), labelled 极性 / 極性 / Polaridad; the two pinned populations move by one (`object-lifecycle-panel-echo-decisions` positive control 689 → 690 on today's `main`, `report-form-echo-decisions` dataset leaves 39 → 40). - `node scripts/check-skills-token-ratchet.mjs` and `--self-test`: OK (79 cases). - eslint on every changed source file: 0 errors. - The derived gate families (`node scripts/pm/dispatch-gates.mjs --commands`, 138 at this head) are running locally; their verdicts go in the os-dev report on #26602, and the PR's check-runs are the CI reading. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (eslint on the changed files; the repo-wide run is CI's). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzfjMN88NYp5BcCRyGR --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzfjMN88NYp5BcCRyGR)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2205863 commit c26d643

33 files changed

Lines changed: 624 additions & 38 deletions

‎.changeset/26602-delta-polarity.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@object-ui/plugin-dashboard': minor
3+
---
4+
5+
Clause-②: yes (widening)
6+
Metadata-protocol: added
7+
8+
A dataset-bound KPI tile and a cross-tab cell colour their period-over-period delta by the measure's declared `polarity` (`fields[].polarity` on the dataset answer, objectstack#26602). `'lower'` draws a rise red and a fall green, so a slower approval turnaround no longer reads as good news. `'neutral'` draws the delta in the muted text colour, with no green and no red. `'higher'`, and an undeclared polarity, keep the colours every delta had before: a rise green, a fall red. The arrow still shows which way the number moved under every polarity.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-analytics': minor
4+
'@objectstack/platform-objects': minor
5+
---
6+
7+
feat(spec): a dataset measure declares its `polarity`, so a lower-is-better KPI draws a rising delta as bad news
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: added
11+
12+
`DatasetMeasureSchema` declares one optional key, `polarity`, with three values (`DatasetMeasurePolarity`): `'higher'` (a rise is good), `'lower'` (a fall is good: turnaround days, overdue counts, cost) and `'neutral'` (neither direction is good or bad). It is declared on the measure, the semantic layer, so every widget over the measure agrees, and not on a widget's `compareTo`, which is the executor's closed comparison directive. An undeclared polarity keeps today's colours: a rise green, a fall red. Nothing infers a polarity from the measure's name. The near-miss keys `betterWhen`, `direction`, `goodDirection` and `trendDirection` are pointed at `polarity`, and the boolean spellings `higherIsBetter` / `lowerIsBetter` are answered with the three values.
13+
14+
The dataset answer's column descriptor declares the same vocabulary as `fields[].polarity` (`AnalyticsResult` and `AnalyticsResultResponseSchema`), and the analytics service relays the measure's declared value onto it, on the live query and the draft-data preview alike, the measure's `__compare` column included. An undeclared measure and every dimension column carry no `polarity` key, so an answer over a dataset that declares none is unchanged.
15+
16+
The dataset form's measures repeater offers `polarity`, labelled in every shipped locale (`@objectstack/platform-objects` metadata-form catalogs).

‎content/docs/api/data-api.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,8 @@ is executed. `POST /analytics/sql` refuses the same keys. To order by a member,
474474
<Callout type="info">
475475
**`fields[]` is the resolved presentation surface — read it first.** Each entry carries
476476
`name` and `type` and, when the producer declares them, `label`, `format`, `currency`,
477-
`percentScale`, `builtinAggregate`, `aggregate` and `nullLabel`: the optional members
477+
`percentScale`, `builtinAggregate`, `aggregate`, `nullLabel` and `polarity` (which direction
478+
of a measure's change is good news, relayed from the dataset measure): the optional members
478479
`AnalyticsResultResponseSchema` declares (`packages/spec/src/api/analytics.zod.ts`),
479480
mirrored member for member by `IAnalyticsService.query`'s `AnalyticsResult` and bound to
480481
it at compile time. Optional means a given column may omit them — read them defensively,

‎content/docs/data-modeling/analytics.mdx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ measure values by hand:
260260
result `fields`, so a KPI or chart legend reads "Total Spent / $616,000"
261261
rather than "spent_sum / 616000". The renderer applies the `format` at display
262262
time (it can't be baked into the numeric row value charts plot).
263+
A measure that declares `polarity` (`higher`, `lower` or `neutral`: which
264+
direction of change is good news) carries it on its column too, so a
265+
period-over-period delta on a lower-is-better KPI draws a rise as bad news;
266+
undeclared keeps a rise green and a fall red.
263267

264268
## Migrating from inline queries
265269

‎content/docs/references/index.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Protocol reference — every schema by module
33
navTitle: Protocol Reference
4-
description: Every schema published by @objectstack/spec — 1558 schemas across 14 protocol modules
4+
description: Every schema published by @objectstack/spec — 1559 schemas across 14 protocol modules
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -33,8 +33,8 @@ counts are sums of the rows they head. Regenerate with
3333
| [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
3434
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3535
| [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
36-
| [UI Protocol](/docs/references/ui) | 16 | 171 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
37-
| **Total** | **202** | **1558** | 14 protocol modules |
36+
| [UI Protocol](/docs/references/ui) | 16 | 172 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
37+
| **Total** | **202** | **1559** | 14 protocol modules |
3838

3939
---
4040

@@ -369,7 +369,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a
369369

370370
## UI Protocol
371371

372-
**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 171 schemas**
372+
**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 172 schemas**
373373

374374
Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer.
375375

@@ -382,7 +382,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay
382382
| [`chart.zod.ts`](/docs/references/ui/chart) | `ChartAggregate`, `ChartAggregateFunction`, `ChartAnnotation`, `ChartAxis`, `ChartConfig`, `ChartDrillDown`, `ChartGroupBy`, `ChartInteraction`, `ChartSeries`, `ChartType` |
383383
| [`component.zod.ts`](/docs/references/ui/component) | `ActionButtonProps`, `ActionGroupProps`, `ActionIconProps`, `ActionMenuProps`, `ElementButtonProps`, `ElementDefinitionListProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementRepeaterProps`, `ElementTextInputProps`, `ElementTextProps`, `ObjectCalendarProps`, `ObjectFormProps`, `ObjectGanttProps`, `ObjectGridProps`, `ObjectKanbanProps`, `ObjectMapProps`, `ObjectMasterDetailFormProps`, `ObjectMetricProps`, `ObjectTimelineProps`, `ObjectTreeProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordAlertAction`, `RecordAlertProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordHistoryProps`, `RecordLineItemsProps`, `RecordPathProps`, `RecordQuickActionsProps`, `RecordReferenceRailProps`, `RecordRelatedListProps`, `ReferenceRailEntry` |
384384
| [`dashboard.zod.ts`](/docs/references/ui/dashboard) | `Dashboard`, `DashboardHeader`, `DashboardHeaderAction`, `DashboardWidget`, `DashboardWidgetChartConfig`, `DashboardWidgetOptions`, `GlobalFilter`, `GlobalFilterOptionsFrom`, `WidgetActionType`, `WidgetColorVariant` |
385-
| [`dataset.zod.ts`](/docs/references/ui/dataset) | `Dataset`, `DatasetDimension`, `DatasetDurationUnit`, `DatasetMeasure`, `DatasetMeasureDuration`, `DerivedMeasureOp` |
385+
| [`dataset.zod.ts`](/docs/references/ui/dataset) | `Dataset`, `DatasetDimension`, `DatasetDurationUnit`, `DatasetMeasure`, `DatasetMeasureDuration`, `DatasetMeasurePolarity`, `DerivedMeasureOp` |
386386
| [`expression-bindable-text-keys.zod.ts`](/docs/references/ui/expression-bindable-text-keys) | `ExpressionBindableTextKey` |
387387
| [`i18n.zod.ts`](/docs/references/ui/i18n) | `AriaProps`, `I18nLabel`, `InlineLocaleMap` |
388388
| [`notification.zod.ts`](/docs/references/ui/notification) | `NotificationPosition`, `NotificationSeverity`, `NotificationType` |

‎content/docs/references/ui/dataset.mdx‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Dataset schema — UI Protocol reference
33
navTitle: Dataset
4-
description: "Analytics Dataset — the one semantic layer. Reference for Dataset, DatasetDimension, DatasetDurationUnit and 3 more: every property with its type and default."
4+
description: "Analytics Dataset — the one semantic layer. Reference for Dataset, DatasetDimension, DatasetDurationUnit and 4 more: every property with its type and default."
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -34,8 +34,8 @@ layer is absorbed/retired in a later phase (D-A).
3434
## TypeScript Usage
3535

3636
```typescript
37-
import { DatasetSchema, DatasetDimensionSchema, DatasetDurationUnit, DatasetMeasureSchema, DatasetMeasureDurationSchema, DerivedMeasureOp } from '@objectstack/spec/ui';
38-
import type { Dataset, DatasetDimension, DatasetDurationUnit, DatasetMeasure, DatasetMeasureDuration } from '@objectstack/spec/ui';
37+
import { DatasetSchema, DatasetDimensionSchema, DatasetDurationUnit, DatasetMeasureSchema, DatasetMeasureDurationSchema, DatasetMeasurePolarity, DerivedMeasureOp } from '@objectstack/spec/ui';
38+
import type { Dataset, DatasetDimension, DatasetDurationUnit, DatasetMeasure, DatasetMeasureDuration, DatasetMeasurePolarity } from '@objectstack/spec/ui';
3939

4040
// Validate data
4141
const result = DatasetSchema.parse(data);
@@ -88,6 +88,7 @@ const result = DatasetSchema.parse(data);
8888
| **filter** | `any` | optional | |
8989
| **format** | `string` | optional | Numeral pattern for a NUMERIC measure — grouping, decimals, percent; e.g. "0,0.00", "0.0%". An amount takes its symbol from `currency`, not from a "$" in the pattern. A DATE-valued measure never reads a date pattern: `"YYYY-MM-DD"` renders that arm's default face. A date or datetime value reads `format` as a display style — `short` or `relative`, honoured on both. |
9090
| **currency** | `string` | optional | Display currency code (ISO 4217) |
91+
| **polarity** | `Enum<'higher' \| 'lower' \| 'neutral'>` | optional | Which direction of change is good news: higher (rise green, fall red), lower (fall green, rise red) or neutral (no good/bad colour). Colours a period-over-period delta; undeclared keeps rise green, fall red. |
9192
| **derived** | `{ op: Enum<'ratio' \| 'sum' \| 'difference' \| 'product'>; of: string[] }` | optional | |
9293
| **duration** | `{ from: string; to: string; unit: Enum<'seconds' \| 'minutes' \| 'hours' \| 'days'> }` | optional | Time between two datetime columns of each record (`{ from, to, unit }`), reduced by `aggregate` (avg / min / max / sum). Records with either instant null are excluded. Replaces `field`. |
9394

@@ -143,6 +144,7 @@ const result = DatasetSchema.parse(data);
143144
| **filter** | `any` | optional | |
144145
| **format** | `string` | optional | Numeral pattern for a NUMERIC measure — grouping, decimals, percent; e.g. "0,0.00", "0.0%". An amount takes its symbol from `currency`, not from a "$" in the pattern. A DATE-valued measure never reads a date pattern: `"YYYY-MM-DD"` renders that arm's default face. A date or datetime value reads `format` as a display style — `short` or `relative`, honoured on both. |
145146
| **currency** | `string` | optional | Display currency code (ISO 4217) |
147+
| **polarity** | `Enum<'higher' \| 'lower' \| 'neutral'>` | optional | Which direction of change is good news: higher (rise green, fall red), lower (fall green, rise red) or neutral (no good/bad colour). Colours a period-over-period delta; undeclared keeps rise green, fall red. |
146148
| **derived** | `{ op: Enum<'ratio' \| 'sum' \| 'difference' \| 'product'>; of: string[] }` | optional | |
147149
| **duration** | `{ from: string; to: string; unit: Enum<'seconds' \| 'minutes' \| 'hours' \| 'days'> }` | optional | Time between two datetime columns of each record (`{ from, to, unit }`), reduced by `aggregate` (avg / min / max / sum). Records with either instant null are excluded. Replaces `field`. |
148150

@@ -168,6 +170,17 @@ const result = DatasetSchema.parse(data);
168170
| **unit** | `Enum<'seconds' \| 'minutes' \| 'hours' \| 'days'>` | ✅ | Unit of the result: the measure answers a number of these (1.5 with "days" is a day and a half). |
169171

170172

173+
---
174+
175+
## DatasetMeasurePolarity
176+
177+
### Allowed Values
178+
179+
* `higher`
180+
* `lower`
181+
* `neutral`
182+
183+
171184
---
172185

173186
## DerivedMeasureOp

‎content/docs/ui/dashboards.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ selects `dimensions` (X / group / split) and `values` (the measures to plot):
103103
| `layout` | `object` | optional | Grid position and size (auto-flowed into the grid when omitted) |
104104
| `chartConfig` | `object` | optional | Chart **appearance**: `title`, `subtitle`, `description`, `colors`, `height`, `showLegend`, `showDataLabels`, `annotations`, `interaction`. The bound dataset decides which series exist and which column each one reads, so `type`, `xAxis`, `yAxis` and `series` are refused here — see below |
105105
| `colorVariant` | `enum` | optional | KPI/card accent color |
106-
| `compareTo` | `object` | optional | Period-over-period comparison: `{ kind: 'previousPeriod' \| 'previousYear', dimension? }`. Omit `dimension` when the selection dates exactly one time dimension — the runtime resolves it, and errors naming the candidates rather than guessing when it cannot. |
106+
| `compareTo` | `object` | optional | Period-over-period comparison: `{ kind: 'previousPeriod' \| 'previousYear', dimension? }`. Omit `dimension` when the selection dates exactly one time dimension — the runtime resolves it, and errors naming the candidates rather than guessing when it cannot. The delta is coloured by the measure's `polarity`: `'lower'` draws a rise red and a fall green, `'neutral'` draws neither; undeclared, a rise is green. |
107107
| `options` | `object` | optional | The query keys below. Presentation does not go here — see below |
108108

109109
### What `chartConfig` does **not** carry
@@ -147,6 +147,7 @@ Presentation has declared homes instead:
147147
| You want | Write it here |
148148
| :--- | :--- |
149149
| A number's format or currency | The dataset **measure**'s `format` and `currency` |
150+
| A KPI delta's good/bad colour (lower is better for turnaround, cost) | The dataset **measure**'s `polarity`: `'higher'`, `'lower'` or `'neutral'` |
150151
| A KPI tile's accent color | The widget's `colorVariant` |
151152
| A chart's look (colors, legend, data labels, height) | The widget's `chartConfig` |
152153

‎packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2095,6 +2095,9 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
20952095
"measures.currency": {
20962096
label: "Currency"
20972097
},
2098+
"measures.polarity": {
2099+
label: "Polarity"
2100+
},
20982101
"measures.derived": {
20992102
label: "Derived From"
21002103
},

‎packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2095,6 +2095,9 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
20952095
"measures.currency": {
20962096
label: "Moneda"
20972097
},
2098+
"measures.polarity": {
2099+
label: "Polaridad"
2100+
},
20982101
"measures.derived": {
20992102
label: "Derivada de"
21002103
},

‎packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2095,6 +2095,9 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
20952095
"measures.currency": {
20962096
label: "通貨"
20972097
},
2098+
"measures.polarity": {
2099+
label: "極性"
2100+
},
20982101
"measures.derived": {
20992102
label: "派生元"
21002103
},

0 commit comments

Comments
 (0)