Repository navigation
fix(dashboards): Pipeline by Stage is a horizontal bar chart, not a funnel (#2073 item 2) - #2182
Merged
objectstack-fleet[bot] merged 2 commits intoOct 19, 2026
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
Author
席位核对ACCEPT (#2073 item 2) · PR #2182 at
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 Generated by Claude Code |
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>
This was referenced Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tsholds the one factory behind all three tiles.type: 'funnel'becomestype: 'horizontal-bar', and the factory is renamedpipelineByStageFunnelWidget→pipelineByStageWidget. Its doc comment says why: a bar, not a funnel; horizontal, not vertical; nosortBy. Binding, filter, id, title and description are unchanged, so the locale packs are not touched.crm.dashboard.ts,sales.dashboard.tsandexecutive.dashboard.tschange only the import and the call, for the rename. Two comments that called the tile a funnel now say what it is.test/pipeline-by-stage-chart.test.ts(8 cases) pins four facts on the three registered dashboards. The tile sits once on each. It istype: 'horizontal-bar', with the same binding and open-stage filter. It has nooptions.sortBy. Andcrm_opportunity.stagelists its open options in sales order, which is the order the bars are drawn in.test/docs-analytics-vocabulary.test.tsandtest/docs-quick-tour-navigation.test.ts, the factory takes its new name, and two test titles that said funnel no longer do.analytics/dashboards(three tile bullets),analytics/index,analytics/reportsandgetting-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 reportopportunity_funnel_owner_stageis 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/queryonopportunity_metrics,dimensions: ['stage'], open stages only, answered 200:The tiles' own queries carry the dashboard's default
this_quarterwindow (close_date2026-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_TYPESdoes not includefunnel. That leaves the bar chart.Assumption 4 (sales order): holds. A DOM probe ran on each tile after hydration:
funnel, all 3 dashboards.recharts-funnel-trapezoid, widths 67 / 67 / 92 / 500 / 500 pxbar(tried, not kept).recharts-rectanglehorizontal-bar, all 3 dashboards.recharts-rectangle.recharts-rectangleThe 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. AsortBy: '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
barrotates 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 verifyran step by step through the shared verify lock. Each exit below is read from the lock'sVERDICT command-exitline.pnpm validatepipeline_by_stagepnpm typecheckpnpm lintpnpm lint:i18n-gatei18n/missing-*issues"pnpm hygienepnpm hygiene:tokenspnpm builddist/objectstack.jsoncarriespipeline_by_stage3 times, allhorizontal-bar, none with asortByvitest run --shard=1/3vitest run --shard=2/3it.skipIfintest/script-main-guard.test.ts)vitest run --shard=3/3The 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.mjsreplacedtype: 'horizontal-bar',withtype: 'funnel',inshared-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.tsthen 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 toHEAD:, andgit diff HEADwas empty. No build step was needed, because the test readssrc/through the composed stack, notdist/.Acceptance notes
src/sales/reports/opportunity.report.ts,chart.type: 'funnel'overtotal_amountby 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.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.tssays "The pipeline funnel used to appear word-for-word". That sentence is history, so it is left as is.Generated by Claude Code