Cost Breakdown
blockfinanceThree-panel spend dashboard. Stacked weekly bar chart over time on top, plus two side-by-side categorical pies below (share by lane / share by carrier). Theme-aware via registry tokens.
Installation
$pnpm dlx uipkge-ng@latest add cost-breakdown$npx uipkge-ng@latest add cost-breakdown$yarn dlx uipkge-ng@latest add cost-breakdown$bunx uipkge-ng@latest add cost-breakdownManifest:
https://uipkge.dev/r/angular/cost-breakdown.jsonInstalls to:src/app/components/blocks/cost-breakdown/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
CostWeekRowinterface CostWeekRow {
week: string
detentionOrigin: number
combinedDndOrigin: number
demurrageOrigin: number
detentionDest: number
demurrageDest: number
combinedDndDest: number
}Files installed (2)
src/app/components/blocks/cost-breakdown/cost-breakdown.component.ts8.7 kB
// Three-panel cost breakdown: a stacked-by-week bar over time on top, and two // side-by-side categorical pie charts below (cost share by lane + by carrier). // Port of the React block 1:1 — swap `weeklySeries`, `byLane`, and `byCarrier` // to repoint. Pies surface inline value labels so the panels read at a glance. import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { cn } from '@/lib/utils' import { UiBarChartComponent } from '@/ui/charts/bar-chart/bar-chart.component' import { UiPieChartComponent } from '@/ui/charts/pie-chart/pie-chart.component' export interface CostWeekRow { week: string detentionOrigin: number combinedDndOrigin: number demurrageOrigin: number detentionDest: number demurrageDest: number combinedDndDest: number } // 17 weeks of cost data. Heavy spike in mid-Feb mirrors a closing // quarter cleanup or a port-disruption incident. export const weeklySeries: CostWeekRow[] = [ { week: 'Jan 26 - Feb 1', detentionOrigin: 2800, combinedDndOrigin: 0, demurrageOrigin: 0, detentionDest: 0, demurrageDest: 0, combinedDndDest: 0, }, { week: 'Feb 2 - Feb 8', detentionOrigin: 1100, combinedDndOrigin: 0, demurrageOrigin: 0, detentionDest: 0, demurrageDest: 0, combinedDndDest: 0, }, { week: 'Feb 9 - Feb 15', detentionOrigin: 1800, combinedDndOrigin: 3100, demurrageOrigin: 800, detentionDest: 1800, demurrageDest: 4300, combinedDndDest: 1100, }, { week: 'Feb 16 - Feb 22', detentionOrigin: 0, combinedDndOrigin: 22000, demurrageOrigin: 12500, detentionDest: 0, demurrageDest: 1900, combinedDndDest: 35500, }, { week: 'Feb 23 - Mar 1', detentionOrigin: 0, combinedDndOrigin: 0, demurrageOrigin: 3300, detentionDest: 800, demurrageDest: 6800, combinedDndDest: 4400, }, { week: 'Mar 2 - Mar 8', detentionOrigin: 2400, combinedDndOrigin: 0, demurrageOrigin: 6400, detentionDest: 1200, demurrageDest: 2800, combinedDndDest: 6600, }, { week: 'Mar 9 - Mar 15', detentionOrigin: 0, combinedDndOrigin: 0, demurrageOrigin: 600, detentionDest: 1300, demurrageDest: 5300, combinedDndDest: 5400, }, { week: 'Mar 16 - Mar 22', detentionOrigin: 5400, combinedDndOrigin: 0, demurrageOrigin: 6000, detentionDest: 600, demurrageDest: 4200, combinedDndDest: 1400, }, { week: 'Mar 23 - Mar 29', detentionOrigin: 0, combinedDndOrigin: 11000, demurrageOrigin: 4900, detentionDest: 0, demurrageDest: 4300, combinedDndDest: 2200, }, { week: 'Mar 30 - Apr 5', detentionOrigin: 7700, combinedDndOrigin: 14000, demurrageOrigin: 5400, detentionDest: 1700, demurrageDest: 6300, combinedDndDest: 7200, }, { week: 'Apr 6 - Apr 12', detentionOrigin: 1200, combinedDndOrigin: 3100, demurrageOrigin: 1500, detentionDest: 0, demurrageDest: 4800, combinedDndDest: 6300, }, { week: 'Apr 13 - Apr 19', detentionOrigin: 0, combinedDndOrigin: 3300, demurrageOrigin: 3200, detentionDest: 3500, demurrageDest: 5000, combinedDndDest: 5700, }, { week: 'Apr 20 - Apr 26', detentionOrigin: 1700, combinedDndOrigin: 5300, demurrageOrigin: 2500, detentionDest: 600, demurrageDest: 3800, combinedDndDest: 4800, }, { week: 'Apr 27 - May 3', detentionOrigin: 6000, combinedDndOrigin: 15000, demurrageOrigin: 5500, detentionDest: 600, demurrageDest: 800, combinedDndDest: 2400, }, { week: 'May 4 - May 10', detentionOrigin: 1900, combinedDndOrigin: 2200, demurrageOrigin: 3300, detentionDest: 3500, demurrageDest: 3700, combinedDndDest: 1100, }, { week: 'May 11 - May 17', detentionOrigin: 1900, combinedDndOrigin: 1200, demurrageOrigin: 800, detentionDest: 700, demurrageDest: 600, combinedDndDest: 1200, }, ] export const costStackKeys = [ { key: 'detentionOrigin', label: 'Detention at origin' }, { key: 'combinedDndOrigin', label: 'Combined at origin' }, { key: 'demurrageOrigin', label: 'Demurrage at origin' }, { key: 'detentionDest', label: 'Detention at destination' }, { key: 'demurrageDest', label: 'Demurrage at destination' }, { key: 'combinedDndDest', label: 'Combined at destination' }, ] as const export const byLane = [ { name: 'LANE-01', value: 119800 }, { name: 'LANE-02', value: 39000 }, { name: 'LANE-03', value: 53100 }, { name: 'LANE-04', value: 32000 }, { name: 'LANE-05', value: 24700 }, { name: 'LANE-06', value: 23500 }, { name: 'LANE-07', value: 20300 }, { name: 'LANE-08', value: 18300 }, { name: 'LANE-09', value: 14700 }, { name: 'OTHER', value: 8100 }, ] export const byCarrier = [ { name: 'C-ALPHA', value: 144200 }, { name: 'C-BETA', value: 69800 }, { name: 'C-GAMMA', value: 32700 }, { name: 'C-DELTA', value: 31800 }, { name: 'C-EPSILON', value: 27800 }, { name: 'C-ZETA', value: 22600 }, { name: 'OTHER', value: 10100 }, ] export function totalCostAll(): number { return weeklySeries.reduce( (acc, w) => acc + w.detentionOrigin + w.combinedDndOrigin + w.demurrageOrigin + w.detentionDest + w.demurrageDest + w.combinedDndDest, 0, ) } export function fmtUsd(n: number): string { return `$${(n / 1000).toFixed(1)}k` } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-cost-breakdown, [ui-cost-breakdown]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [UiBarChartComponent, UiPieChartComponent], template: ` <div data-slot="cost-breakdown" [class]="rootClass"> <!-- Weekly trend (top) --> <section class="bg-card border-border rounded-lg border p-5"> <div class="mb-3 flex items-baseline justify-between gap-4"> <div> <h3 class="text-sm font-semibold tracking-tight">Cost per day</h3> <p class="text-muted-foreground mt-0.5 text-xs">17-week rollup. Hover a stack to inspect the breakdown.</p> </div> <div class="text-right"> <div class="text-muted-foreground font-mono text-xs tracking-wider uppercase">Total</div> <div class="text-lg font-semibold tabular-nums">{{ fmtUsd(totalAll) }}</div> </div> </div> <ui-bar-chart [data]="weeklySeries" xField="week" [yField]="stackFields" [option]="stackedOption" height="320" /> </section> <!-- Dual pie breakdown (bottom) --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2"> <section class="bg-card border-border rounded-lg border p-4 sm:p-5"> <h3 class="text-sm font-semibold tracking-tight">Cost share by lane</h3> <p class="text-muted-foreground mt-0.5 mb-4 text-xs">Top 9 lanes + a grouped 'Other' bucket.</p> <ui-pie-chart [data]="byLane" [option]="pieOption" height="320" /> </section> <section class="bg-card border-border rounded-lg border p-4 sm:p-5"> <h3 class="text-sm font-semibold tracking-tight">Cost share by carrier</h3> <p class="text-muted-foreground mt-0.5 mb-4 text-xs">Top 6 carriers + a grouped 'Other' bucket.</p> <ui-pie-chart [data]="byCarrier" [option]="pieOption" height="320" /> </section> </div> </div> `, }) export class UiCostBreakdownComponent { protected readonly weeklySeries = weeklySeries protected readonly byLane = byLane protected readonly byCarrier = byCarrier protected readonly totalAll = totalCostAll() protected readonly stackFields = costStackKeys.map((s) => s.key) protected readonly stackedOption = { series: costStackKeys.map(() => ({ stack: 'total' as const })), yAxis: { type: 'value' as const, axisLabel: { formatter: (v: number) => (v >= 1000 ? `$${Math.round(v / 1000)}K` : `$${v}`) }, }, } // Inline slice labels mirror the React `pieOption`. The label colour is left to // the pie wrapper default (registry chart text token) so the block does not take // a direct `@/ui/charts/use-chart-theme` dependency. protected readonly pieOption = { legend: { show: false }, series: [ { radius: ['0%', '70%'], label: { show: true, formatter: (p: { value: number; percent: number }) => `$${(p.value / 1000).toFixed(1)}k\n(${p.percent}%)`, fontSize: 10, }, labelLine: { show: false }, }, ], } @Input('class') className?: string get rootClass(): string { return cn('space-y-6', this.className) } fmtUsd(n: number): string { return fmtUsd(n) } }src/app/components/blocks/cost-breakdown/index.ts0.2 kB
export { UiCostBreakdownComponent, weeklySeries, costStackKeys, byLane, byCarrier, totalCostAll, fmtUsd, type CostWeekRow, } from './cost-breakdown.component'
Raw manifest:https://uipkge.dev/r/angular/cost-breakdown.json