Skip to content

fix(dashboards): Pipeline by Stage is a horizontal bar chart, not a funnel (#2073 item 2) - #2182

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-2073-item-2-pipeline-stage-chart
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-2073-item-2-pipeline-stage-chart

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #2073 (item 2)
Clause-②: no

Item 2 only; the card stays open for items 3 and 5–8.

What this does

The Pipeline by Stage tile (zh-CN 阶段管道分布) on Executive Overview, Sales Performance and CRM Overview drew open opportunity value per stage as a funnel. Open amount by stage is not monotonic, so the shape pinched and then flared into a diamond. The card's acceptance allows a bar chart for amount by stage, or a count-based funnel that is monotonic by construction. Measured below, only the bar is reachable, so the tile is now a horizontal bar chart.

  • src/sales/dashboards/shared-widgets.ts holds the one factory behind all three tiles. type: 'funnel' becomes type: 'horizontal-bar', and the factory is renamed pipelineByStageFunnelWidget → pipelineByStageWidget. Its doc comment says why: a bar, not a funnel; horizontal, not vertical; no sortBy. Binding, filter, id, title and description are unchanged, so the locale packs are not touched.
  • crm.dashboard.ts, sales.dashboard.ts and executive.dashboard.ts change only the import and the call, for the rename. Two comments that called the tile a funnel now say what it is.
  • The new test/pipeline-by-stage-chart.test.ts (8 cases) pins four facts on the three registered dashboards. The tile sits once on each. It is type: 'horizontal-bar', with the same binding and open-stage filter. It has no options.sortBy. And crm_opportunity.stage lists its open options in sales order, which is the order the bars are drawn in.
  • In test/docs-analytics-vocabulary.test.ts and test/docs-quick-tour-navigation.test.ts, the factory takes its new name, and two test titles that said funnel no longer do.
  • Docs in all three locales: analytics/dashboards (three tile bullets), analytics/index, analytics/reports and getting-started/quick-tour. Tile names stay English under the English-title rule. zh-Hans says 条形图 / 横条 and zh-Hant 橫條圖 / 橫條, so they stay distinct from the 柱状图 / 長條圖 those pages already use for the vertical bar chart on the Opportunities by Stage report.
  • .changeset/2073-pipeline-by-stage-bar-chart.md (patch).

Measured

Setup: a fresh boot of this worktree (objectstack dev, @objectstack/* 18.0.0-next.3, shipped seed, 23 opportunities), Chromium at /opt/pw-browsers, 1440×1000, signed in as the dev admin.

Assumption 1 (one factory): holds. An uncapped grep over src/ finds the factory defined once and called once each in crm (:129), sales (:160) and executive (:191). No other dashboard widget is a funnel. The report opportunity_funnel_owner_stage is a funnel too, but it is a report, not this widget (see Acceptance notes).

Assumption 2 (amounts not monotonic): holds. POST /api/v1/analytics/dataset/query on opportunity_metrics, dimensions: ['stage'], open stages only, answered 200:

stage, in sales order total_amount opp_count
prospecting 139,500 2
qualification 174,000 2
needs_analysis 173,000 2
proposal 237,000 2
negotiation 1,294,000 2

The tiles' own queries carry the dashboard's default this_quarter window (close_date 2026-10-01 to 2026-12-31). They returned the same rows, except that Prospecting is 86,500. The server answers in stored-value order: needs_analysis, negotiation, proposal, prospecting, qualification.

Assumption 3 (a count funnel is not monotonic either): holds, and a cumulative count funnel cannot be authored. The seed happens to put 2 open deals in every stage, and one more deal in a later stage makes the counts rise. A "reached stage S or later" count needs either one deal in several buckets, which a group-by cannot produce, or one filtered count measure per stage drawn as a dimensionless multi-measure funnel. The spec refuses the second: DASHBOARD_WIDGET_MULTI_MEASURE_TYPES does not include funnel. That leaves the bar chart.

Assumption 4 (sales order): holds. A DOM probe ran on each tile after hydration:

shape marks in the tile order drawn
before, funnel, all 3 dashboards 5 .recharts-funnel-trapezoid, widths 67 / 67 / 92 / 500 / 500 px Prospecting to Negotiation: narrow on top, flaring into a diamond
vertical bar (tried, not kept) 5 .recharts-rectangle Prospecting, Qualificati…, Needs Analy…, Proposal, Negotiation
after, horizontal-bar, all 3 dashboards 5 .recharts-rectangle top to bottom: Prospecting, Qualification, Needs Analysis, Proposal, Negotiation
after, zh-CN browser locale 5 .recharts-rectangle 寻找客户, 资格审查, 需求分析, 提案, 谈判, under the title 阶段管道分布

The console draws a select dimension in the field's declared option order when the widget has no sortBy, and it did so here although the dataset answered alphabetically. A sortBy: 'stage' would not give sales order. order: { stage: 'asc' } on the same query sorts by label: in en, Needs Analysis, Negotiation, Proposal, Prospecting, Qualification; in zh-CN, 寻找客户, 提案, 谈判, 资格审查, 需求分析. The bars take the stage options' own colours.

Why horizontal. A vertical bar rotates its category labels, and the console cuts a rotated label to 12 characters by design (ROTATED_X_LABEL_MAX_CHARS, objectui#7247 and objectui#11796). So two of the five stage names were cut. A horizontal bar's category axis sizes itself to the longest label and draws all of them, and it keeps the funnel's top-to-bottom reading order. This is the chart choice for long category names, not a tile-by-tile workaround: no other tile changes.

Tests and gates (head 2eb98ff)

pnpm verify ran step by step through the shared verify lock. Each exit below is read from the lock's VERDICT command-exit line.

step exit · reading
pnpm validate 0 · "Validation passed"; no line names pipeline_by_stage
pnpm typecheck 0
pnpm lint 0
pnpm lint:i18n-gate 0 · "0 i18n/missing-* issues"
pnpm hygiene 0 · "source hygiene clean"
pnpm hygiene:tokens 0 · src/sales business semantics ~57,962 tokens (ceiling ~88,500)
pnpm build 0 · dist/objectstack.json carries pipeline_by_stage 3 times, all horizontal-bar, none with a sortBy
vitest run --shard=1/3 0 · 66 files, 1096 passed
vitest run --shard=2/3 0 · 66 files, 1245 passed, 1 skipped (the existing it.skipIf in test/script-main-guard.test.ts)
vitest run --shard=3/3 0 · 65 files, 1579 passed

The three shards hold 197 test files, the same count find test -name '*.test.ts' gives.

Ablation (run once, not kept). On 2eb98ff, scripts/ablation-replace.mjs replaced type: 'horizontal-bar', with type: 'funnel', in shared-widgets.ts (anchor count 1 → 0, marker count 0 → 1, blob 70ad10c4 → ae646f65), under a trap that restores from HEAD. test/pipeline-by-stage-chart.test.ts then gave "Tests 3 failed | 5 passed (8)". Those were the three predicted cases, one per dashboard (expected 'funnel' to be 'horizontal-bar'), and the sortBy and option-order cases stayed green. After the restore, the blob was 70ad10c4 again, equal to HEAD:, and git diff HEAD was empty. No build step was needed, because the test reads src/ through the composed stack, not dist/.

Acceptance notes

  • Outside this item, reported to the seat: the Opportunity Funnel by Owner → Stage report draws the same shape. Its source is src/sales/reports/opportunity.report.ts, chart.type: 'funnel' over total_amount by stage. Opened from its Reports entry on this boot, it draws 6 trapezoids, 139 / 139 / 189 / 1034 / 1034 / 1031 px wide from Prospecting to Closed Won, and the Negotiation label is not visible. It is not touched here because it is outside the claim.
  • The README's assets/screenshots/hotcrm/sales-dashboard/ pair, captured 2026-09-09, shows the old funnel at its bottom edge. It is outside the claim; recapturing it belongs with the screenshot set.
  • test/i18n-shared-widget-parity.test.ts says "The pipeline funnel used to appear word-for-word". That sentence is history, so it is left as is.
  • The 12-character cap on rotated labels is deliberate platform behaviour with a stated reason. It is noted here and not filed.

Generated by Claude Code

…orizontal bar chart, not a funnel (#2073 item 2)

A funnel draws each stage as wide as its value, so it reads as one only when
the values shrink stage by stage. Open amount by stage does not (seed:
139,500 / 174,000 / 173,000 / 237,000 / 1,294,000), and the tile pinched and
flared into a diamond on Executive Overview, Sales Performance and CRM
Overview. The shared factory, renamed pipelineByStageWidget, now declares
type horizontal-bar with the same binding and no sortBy, so the console draws
the stages top to bottom in the stage field's option order. The docs pages
that called the tile a funnel are updated in all three locales.

Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r
Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Oct 19, 2026 2:05pm UTC

Request Review

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT (#2073 item 2) · PR #2182 at 2eb98ff0b · repo:hotcrm seat 2 · os-support-ai · session session_01KqkTQNLcxVc588eZpJmp1r · 2026-10-19T13:54Z · report on hotcrm#2073

  1. Shape:
    • Draft, base main, opening with Part of #2073 (item 2) and Clause-②: no. No closing keyword sits next to a card number. Assignee os-support-ai.
    • 20 files, +149 / −44: the shared widget factory, the three dashboards' import and call, docs in three locales, two docs pins renamed, one new pin, one changeset. No governed path (AGENTS.md, CLAUDE.md, .claude/**, .github/workflows/**).
    • One commit, ending with the model-free Claude-Session: / Co-authored-by: Claude pair: 0 model-identifier lines.
    • main cc0608c is an ancestor of the head, and main has not moved since.
  2. Premise, re-measured: one factory, pipelineByStageFunnelWidget with type: 'funnel', called by crm.dashboard.ts, sales.dashboard.ts and executive.dashboard.ts (the seat's grep on cc0608c agrees). The dev's dataset query shows open amounts by stage that rise and fall (139,500 / 174,000 / 173,000 / 237,000 / 1,294,000).
  3. The diff, read:
    • type: 'funnel' → type: 'horizontal-bar', and the factory is renamed pipelineByStageWidget. The id, title, description, filter and binding are unchanged, so no locale pack moves.
    • No options.sortBy, on measurement: the console draws a select dimension in its declared option order, which is crm_opportunity.stage's sales order. A label sort would not be.
    • The route deviation (horizontal, not the suggested vertical bar) is accepted. A vertical bar's rotated labels are cut at 12 characters by design, which was measured on two of the five stage names.
    • The card's acceptance offered a bar chart or a monotonic count funnel. The dev showed the count funnel cannot be authored (DASHBOARD_WIDGET_MULTI_MEASURE_TYPES excludes funnel, and a group-by cannot put one deal in several buckets).
  4. Changeset and docs, read sentence by sentence: the changeset names the tile, its three dashboards, the old shape and the new one, and says the numbers and the title are unchanged. The four docs pages in three locales now say horizontal bar chart (条形图 / 橫條圖), with tile names kept in English. No sentence claims more than the diff does.
  5. Gates:
    • The dev's verify on 2eb98ff0 passes through the lock: each build-half step exits 0, and the three shards cover 197 files, each exiting 0.
    • The new pin test/pipeline-by-stage-chart.test.ts (8 cases) asserts the type, the binding, the absent sortBy and the stage option order. Ablation back to funnel turns exactly the 3 type cases red, and the restore is proven blob-equal.
    • Browser, before and after, at 1440: 5 .recharts-funnel-trapezoid → 5 .recharts-rectangle per tile, Prospecting to Negotiation top to bottom, in en and zh-CN.
    • CI on 2eb98ff0b at this review: 9 of 10 checks success, with Build and Test (22.x) running. Landing waits for it.
  6. Out of scope:
    • The Opportunity Funnel by Owner → Stage report draws the same pinch-and-flare. The seat read src/sales/reports/opportunity.report.ts:56: chart: { type: 'funnel', … xAxis: 'stage', yAxis: 'total_amount' }. Folded into hotcrm#2073 as item 10, the same family.
    • The README screenshot pair under assets/screenshots/hotcrm/sales-dashboard/ shows the old funnel. That is polish with no carrier, kept in the PR's Acceptance notes.

Landing: once every check on the head is green, this seat flips the PR ready and arms auto-merge through the relay; the merge queue merges it. On merge, #2073 goes back to pm:queue for items 3, 5–8 and 10.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 14:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit f8eed90 Oct 19, 2026
11 checks passed
os-support-ai pushed a commit that referenced this pull request Oct 20, 2026
Brings in #2182 and #2183. Neither touches the seed, the escalation
flows or the tests this branch edits.

Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r
Co-authored-by: Claude <noreply@anthropic.com>
os-support-ai pushed a commit that referenced this pull request Oct 20, 2026
quote.object.ts merged without a conflict: #2183 narrowed crm_contact's
requiredWhen, this branch declares expiration_date's default; both kept.

Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI plumbing and the verification pipeline documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants