diff --git a/package-lock.json b/package-lock.json index b4eea9c1..c50f9db9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "igniteui-angular-charts": "^20.0.0", "igniteui-angular-core": "^20.0.0", "igniteui-angular-maps": "^20.0.0", + "igniteui-grid-lite": "^0.7.1", "igniteui-theming": "^24.0.1", "igniteui-webcomponents": "^6.3.6", "minireset.css": "~0.0.4", @@ -2634,12 +2635,12 @@ } }, "node_modules/@lit/context": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.4.tgz", - "integrity": "sha512-0b8wOWNUPcXjGHFtVP+odwhrZBZw+PpCjKWn8IGO10iHT95Xd6FcUAxe1aE7PUtOBvHZoxvrfdBzweYnDW7VNQ==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.6.tgz", + "integrity": "sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A==", "license": "BSD-3-Clause", "dependencies": { - "@lit/reactive-element": "^1.6.2 || ^2.0.0" + "@lit/reactive-element": "^1.6.2 || ^2.1.0" } }, "node_modules/@lit/reactive-element": { @@ -7206,6 +7207,75 @@ "igniteui-angular-core": "20.0.0" } }, + "node_modules/igniteui-grid-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/igniteui-grid-lite/-/igniteui-grid-lite-0.7.1.tgz", + "integrity": "sha512-CUKa9/U5Tn1DDxPMoHPP5RRbTlA/KqiSaHGDaMrrGNRBFbLQAZJnKNR3mlTgHeIk0rWjL6Wyc2bZgjpTWRQxIQ==", + "license": "MIT", + "dependencies": { + "@lit-labs/virtualizer": "~2.1.0", + "@lit/context": "~1.1.5", + "igniteui-webcomponents": "~7.1.0", + "lit": "^3.3.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/igniteui-grid-lite/node_modules/igniteui-webcomponents": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/igniteui-webcomponents/-/igniteui-webcomponents-7.1.3.tgz", + "integrity": "sha512-iG4xBzooIOxtnAG8VIg39zKVWoo9jy2iEK3CDLGZYb9hU1LZx+4SAOPFkB9FSp15dgR4ql6kGKYf93T8oK0sdw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.0", + "@lit-labs/virtualizer": "^2.1.0", + "@lit/context": "^1.1.0", + "igniteui-i18n-core": "^1.0.1", + "lit": "^3.3.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "dompurify": "^3.3.0", + "igniteui-i18n-resources": "^1.0.1", + "marked": "^17.0.0", + "marked-shiki": "^1.2.0", + "shiki": "^3.20.0" + }, + "peerDependenciesMeta": { + "dompurify": { + "optional": true + }, + "igniteui-i18n-resources": { + "optional": true + }, + "marked": { + "optional": true + }, + "marked-shiki": { + "optional": true + }, + "shiki": { + "optional": true + } + } + }, + "node_modules/igniteui-i18n-core": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/igniteui-i18n-core/-/igniteui-i18n-core-1.0.5.tgz", + "integrity": "sha512-CO5Rqo5uqgyAfIHbFVuBEmo+fYXgLQ7zxRShqAu+ng8kDS5uwWjKeSGWVZjKMBgxmc6ZWMMbdcWIpGZUSOkPsw==", + "license": "MIT", + "peerDependencies": { + "igniteui-i18n-resources": "1.0.5" + }, + "peerDependenciesMeta": { + "igniteui-i18n-resources": { + "optional": true + } + } + }, "node_modules/igniteui-theming": { "version": "24.0.1", "resolved": "https://registry.npmjs.org/igniteui-theming/-/igniteui-theming-24.0.1.tgz", diff --git a/package.json b/package.json index 11fde8dd..8dc34d18 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "igniteui-angular-charts": "^20.0.0", "igniteui-angular-core": "^20.0.0", "igniteui-angular-maps": "^20.0.0", + "igniteui-grid-lite": "^0.7.1", "igniteui-theming": "^24.0.1", "igniteui-webcomponents": "^6.3.6", "minireset.css": "~0.0.4", diff --git a/projects/finance-grid/src/app/app.component.scss b/projects/finance-grid/src/app/app.component.scss index b07b6d03..9e886c47 100644 --- a/projects/finance-grid/src/app/app.component.scss +++ b/projects/finance-grid/src/app/app.component.scss @@ -1,17 +1,5 @@ -@use "igniteui-angular/theming" as *; - -@include core(); -@include typography( - $font-family: $bootstrap-typeface, - $type-scale: $bootstrap-type-scale -); -@include theme( - $palette: $light-bootstrap-palette, - $schema: $light-bootstrap-schema -); - -html { +:host { + display: block; height: 100%; - max-height: 1000px; } diff --git a/projects/finance-grid/src/app/finance-grid/finance-grid.component.html b/projects/finance-grid/src/app/finance-grid/finance-grid.component.html index a6e752cc..a57830e1 100644 --- a/projects/finance-grid/src/app/finance-grid/finance-grid.component.html +++ b/projects/finance-grid/src/app/finance-grid/finance-grid.component.html @@ -1,84 +1,75 @@ - - - Financial Portfolio - - - - - - - - - - - - -
- - {{ val }} +
+
+
+

IG Financial Sample

+

A grid showcasing Ignite UI Grid.

+
+
+ + + + Financial Portfolio + - - - - - -
-
- {{ val | percent : "1.2-2" }} + + + + + + + + + +
+ +
+ {{ cell.row.data.ticker }} + {{ cell.row.data.company }} +
+
+
- @if (val >= 0) { - trending_up - } @else { - trending_down - } -
- - - - - -
-
- {{ val | currency : 'USD' : 'symbol' : '1.2-2' }} + + + + + + + + +
+ {{ vm.formatCurrency(cell.row.data.lastPrice) }} +
+
+
- @if (val >= 0) { - trending_up - } @else { - trending_down - } -
- - - - -
-
- {{ val | percent : "1.2-2" }} + + + {{ vm.formatCurrency(cell.value) }} + + + + + +
+ +
+
+
- @if (val >= 0) { - trending_up - } @else { - trending_down - } -
- - - - -
-
- {{ val | percent : "1.2-2" }} + + +
+ {{ vm.formatPercent(cell.value) }} + + +
- -
- - - - - - {{ val }} days - - + + + +
diff --git a/projects/finance-grid/src/app/finance-grid/finance-grid.component.scss b/projects/finance-grid/src/app/finance-grid/finance-grid.component.scss index 0ac1593c..bbf0803b 100644 --- a/projects/finance-grid/src/app/finance-grid/finance-grid.component.scss +++ b/projects/finance-grid/src/app/finance-grid/finance-grid.component.scss @@ -1,39 +1,322 @@ -:host { +app-finance-grid { + --white: #ffffff; + --line: #d8dde6; + --line-soft: #c8cfda; + --text: #263446; + --text-dark: #1d1f20; + --subtle-muted: #5a5d61; + --font-family-condensed: 'Barlow Condensed', 'Arial Narrow', sans-serif; + --font-family-base: 'Barlow', 'Segoe UI', sans-serif; + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-3xl: 2rem; + --top-link-active: #5f46cf; + --pill-positive-text: #136045; + --pill-positive-bg: #d6f2e7; + --pill-negative-text: #9e3f74; + --pill-negative-bg: #f8d8e9; + --pill-negotiation-text: #5238be; + --pill-negotiation-bg: #e6ddfb; + display: block; height: 100%; width: 100%; + min-height: 0; min-width: 1100px; + color: var(--text); + font-family: var(--font-family-base); +} + +app-finance-grid .finance-shell { + padding: 1.5rem; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + flex: 1 1 auto; + min-height: 0; + gap: 1.5rem; + overflow-x: auto; + background: #f0f3f7; +} + +app-finance-grid .finance-header { + display: flex; + align-items: center; + justify-content: space-between; } -::ng-deep { - :root { - font-family: var(--ig-font-family); - } - .profitCondition { - color: var(--ig-success-500); - } +app-finance-grid .finance-header h1 { + margin: 0; + font-family: var(--font-family-condensed); + font-weight: 600; + font-size: var(--font-size-3xl); + line-height: 1.1; + color: #1d1f20; +} - .lossCondition { - color: var(--ig-error-500); - } +app-finance-grid .subtle { + margin: 0.2rem 0 0; + color: var(--subtle-muted); + font-size: var(--font-size-base); } -.grid-sizing { - --ig-size: var(--ig-size-small); +app-finance-grid .finance-shell .finance-grid { + --ig-size: 1; + width: 100%; + height: 100%; + min-height: 0; + font-family: var(--font-family-base); + font-size: var(--font-size-sm); + border: 1px solid #d8dde6; + border-radius: 0.3rem; + overflow: hidden; + background: #ffffff; + + --background: #ffffff; + --header-background: #ffffff; + --header-text-color: #516073; + --header-border-color: #dde3ec; + --content-background: #ffffff; + --content-text-color: #223247; + --row-odd-background: #ffffff; + --row-even-background: #ffffff; + --row-hover-background: #eef3f8; + --row-border-color: #e5e9f0; + --filtering-row-background: #f9fbfd; + --filtering-row-text-color: #506176; + --cell-active-border-color: #ff4d92; } -.assets-container { +app-finance-grid .finance-grid .igx-grid__th, +app-finance-grid .finance-grid .igx-grid__td, +app-finance-grid .finance-grid .igx-grid__td-inner, +app-finance-grid .finance-grid .igx-grid__drop-area, +app-finance-grid .finance-grid .numeric-cell, +app-finance-grid .finance-grid .text-cell { + font-size: var(--font-size-base) !important; +} + +app-finance-grid .finance-grid .igx-grid__th { + height: 50px; + border-bottom: 1px solid #cfd8e6; + border-right: 1px solid #d7dfea; +} + +app-finance-grid .finance-grid .igx-grid-th__title { + font-size: var(--font-size-sm) !important; + font-weight: 600; +} + +app-finance-grid .finance-grid .igx-grid__td { + height: 50px; + border-bottom: 1px solid #d7dfea; +} + +app-finance-grid .finance-grid igx-grid-toolbar { + border-top: 1px solid #eef1f4; + border-bottom: 1px solid #dde3ec; + background: #ffffff; + min-height: 56px; + padding: 0 16px; + gap: 12px; +} + +app-finance-grid .finance-grid igx-grid-toolbar-title { + color: var(--text-dark); + font-family: var(--font-family-condensed); + font-size: 1.125rem; + font-weight: 600; + flex: 0 0 auto !important; + margin-right: 8px; +} + +/* Override Bootstrap theme pink primary on toolbar buttons */ +app-finance-grid igx-grid-toolbar .igx-button--outlined { + border: 1px solid var(--line-soft) !important; + color: var(--text-dark) !important; + background: var(--white) !important; + box-shadow: none !important; + height: 36px !important; + padding: 0 14px !important; + font-size: var(--font-size-sm) !important; + font-family: var(--font-family-base) !important; +} + +app-finance-grid igx-grid-toolbar .igx-button--outlined:hover { + background: #f0f3f7 !important; + border-color: #99a4b4 !important; +} + +app-finance-grid igx-grid-toolbar .igx-button--outlined igx-icon { + color: var(--text-dark) !important; + font-size: 1.1rem !important; +} + +app-finance-grid .finance-toolbar-search { display: flex; align-items: center; - gap: 0.5rem; - & igx-avatar { - --igx-avatar-size: 24px; - } + flex: 1; + max-width: 280px; } -.progress-container { - display: grid; - grid-template-columns: 0.5fr 1fr; +app-finance-grid .finance-search-input { + width: 100%; + padding: 0.375rem 0.75rem; + border: 1px solid var(--line); + border-radius: 4px; + font-family: var(--font-family-base); + font-size: var(--font-size-sm); + color: var(--text); + background: var(--white); + outline: none; + transition: border-color 0.15s; + height: 38px; +} + +app-finance-grid .finance-search-input::placeholder { + color: var(--subtle-muted); +} + +app-finance-grid .finance-search-input:focus { + border-color: var(--top-link-active, #7b62e8); +} + +app-finance-grid .ticker-cell, +app-finance-grid .price-combined-cell, +app-finance-grid .net-profit-combined-cell, +app-finance-grid .allocation-cell { + display: flex; + align-items: center; width: 100%; +} + +app-finance-grid .ticker-cell { + gap: 0.6rem; +} + +app-finance-grid .ticker-logo { + flex: 0 0 auto; +} + +/* Force avatar background — Bootstrap theme doesn't resolve --ig-avatar-background on .igx-avatar--circle */ +app-finance-grid .ticker-logo.igx-avatar--circle { + background: var(--ig-avatar-background) !important; +} + +app-finance-grid .ticker-logo.igx-avatar--circle span { + color: var(--ig-avatar-color, #fff) !important; + font-size: 0.82rem !important; + font-weight: 700 !important; + letter-spacing: 0.01em; +} + +app-finance-grid .ticker-meta { + display: grid; + gap: 0.15rem; + min-width: 0; +} + +app-finance-grid .ticker-symbol { + font-weight: 700; + color: var(--text); +} + +app-finance-grid .ticker-name { + color: var(--subtle-muted); + font-size: 0.94rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +app-finance-grid .price-trend-spark { + display: block; +} + +app-finance-grid .price-combined-cell, +app-finance-grid .net-profit-combined-cell { + gap: 0.6rem; + flex-wrap: wrap; +} + +app-finance-grid .price-combined-price { + font-weight: 600; +} + +app-finance-grid .finance-delta-badge, +app-finance-grid .finance-value-badge { + --ig-size: 1; + background: transparent !important; + border-radius: 999px !important; + padding: 0 !important; +} + +app-finance-grid .finance-delta-badge .igx-badge__value, +app-finance-grid .finance-value-badge .igx-badge__value { + display: inline-flex; + align-items: center; + min-height: 1.35rem; + padding: 0.08rem 0.52rem; + border-radius: 999px; + font-size: var(--font-size-xs); + font-weight: 600; + letter-spacing: 0; + border: none; + box-shadow: none; +} + +app-finance-grid .finance-delta-badge.igx-badge--success .igx-badge__value, +app-finance-grid .finance-value-badge.igx-badge--success .igx-badge__value { + color: var(--pill-positive-text); + background: var(--pill-positive-bg); +} + +app-finance-grid .finance-delta-badge.igx-badge--error .igx-badge__value, +app-finance-grid .finance-value-badge.igx-badge--error .igx-badge__value { + color: var(--pill-negative-text); + background: var(--pill-negative-bg); +} + +app-finance-grid .finance-delta-badge.igx-badge--info .igx-badge__value, +app-finance-grid .finance-value-badge.igx-badge--info .igx-badge__value { + color: var(--pill-negotiation-text); + background: var(--pill-negotiation-bg); +} + +app-finance-grid .allocation-cell { gap: 0.5rem; } + +app-finance-grid .allocation-value { + min-width: 3.5rem; + font-variant-numeric: tabular-nums; +} + +app-finance-grid .allocation-track { + position: relative; + display: flex; + flex: 1 1 0; + min-width: 50px; + height: 0.5rem; + border-radius: 999px; + background: #dde3ec; + overflow: hidden; +} + +app-finance-grid .allocation-fill { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, #7f4cf6 0%, #5f46cf 100%); +} + +app-finance-grid .finance-grid .numeric-cell, +app-finance-grid .allocation-value, +app-finance-grid .ticker-symbol, +app-finance-grid .price-combined-price { + font-variant-numeric: tabular-nums; +} + +app-finance-grid .finance-col-sep { + border-left: 1px solid #eef1f4; +} diff --git a/projects/finance-grid/src/app/finance-grid/finance-grid.component.ts b/projects/finance-grid/src/app/finance-grid/finance-grid.component.ts index 227d9bd4..98fcabac 100644 --- a/projects/finance-grid/src/app/finance-grid/finance-grid.component.ts +++ b/projects/finance-grid/src/app/finance-grid/finance-grid.component.ts @@ -1,130 +1,218 @@ -import { - afterNextRender, - Component, - OnDestroy, - OnInit, - ViewChild -} from '@angular/core'; -import { FilteringExpressionsTree, FilteringLogic, IColumnPipeArgs, IgxStringFilteringOperand, THEME_TOKEN, ThemeToken } from 'igniteui-angular/core'; -import { IgxAvatarComponent } from 'igniteui-angular/avatar'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewEncapsulation, computed, inject, signal } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { PLATFORM_ID } from '@angular/core'; +import { IgxAvatarModule } from 'igniteui-angular/avatar'; +import { IgxBadgeModule } from 'igniteui-angular/badge'; import { IgxCellTemplateDirective, IgxColumnComponent, IgxGridToolbarActionsComponent, IgxGridToolbarComponent, IgxGridToolbarExporterComponent, IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent, IgxGridToolbarTitleComponent } from 'igniteui-angular/grids/core'; import { IgxGridComponent } from 'igniteui-angular/grids/grid'; -import { IgxIconComponent } from 'igniteui-angular/icon'; -import { IgxInputDirective, IgxInputGroupComponent } from 'igniteui-angular/input-group'; -import { IgxLinearProgressBarComponent } from 'igniteui-angular/progressbar'; -import { CurrencyPipe, PercentPipe, AsyncPipe } from '@angular/common'; -import { FinancialService } from '../services/financial.service'; -import { BehaviorSubject } from 'rxjs'; -import { FormsModule } from '@angular/forms'; +import { IgxSparklineModule } from 'igniteui-angular-charts'; +import { IgxCsvExporterService, IgxExcelExporterService, IgxPdfExporterService, type IColumnExportingEventArgs } from 'igniteui-angular/grids/core'; + +type FinanceTone = 'blue' | 'orange' | 'green' | 'slate' | 'gold' | 'red'; + +interface PriceTrendPoint { + value: number; +} + +interface FinanceSeedRow { + ticker: string; + company: string; + lastPrice: number; + changePct: number; + marketValue: number; + netProfit: number; + netProfitPct: number; + allocationPct: number; + averageCost: number; + position: number; + holdingPeriodDays: number; + tone: FinanceTone; +} + +interface FinanceRow extends FinanceSeedRow { + priceTrend: PriceTrendPoint[]; +} @Component({ standalone: true, selector: 'app-finance-grid', - providers: [ - { - provide: THEME_TOKEN, - useFactory: () => { - return new ThemeToken('bootstrap'); - }, - }, - ], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, imports: [ - FormsModule, - CurrencyPipe, - PercentPipe, - AsyncPipe, - IgxLinearProgressBarComponent, - IgxAvatarComponent, + IgxAvatarModule, + IgxBadgeModule, + IgxSparklineModule, IgxGridComponent, IgxColumnComponent, IgxCellTemplateDirective, - IgxInputGroupComponent, - IgxInputDirective, - IgxIconComponent, - IgxIconComponent, IgxGridToolbarComponent, IgxGridToolbarTitleComponent, IgxGridToolbarActionsComponent, IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent, IgxGridToolbarExporterComponent -], + ], templateUrl: './finance-grid.component.html', styleUrl: './finance-grid.component.scss', }) export class FinanceGridComponent implements OnInit, OnDestroy { - @ViewChild(IgxGridComponent, { static: true }) public grid!: IgxGridComponent; - public data$: BehaviorSubject = new BehaviorSubject([]); - public isLoading = true; - public currencyDigitsFormat: IColumnPipeArgs = { - digitsInfo: '1.2-2' - }; - - public profitLossValueClasses = { - profitCondition: this.profitConditionHandler, - lossCondition: this.lossConditionHandler, - }; - private _timer!: ReturnType; - private readonly updateTimerInMs = 3000; - constructor(private financialService: FinancialService) { - afterNextRender({ - write: () => { - this._timer = setInterval(() => { - this.financialService.updateAllPrices(this.grid.data); - this.grid.markForCheck(); - }, this.updateTimerInMs); - }, - }); - } + protected readonly vm = this; + + private readonly excelExporter = inject(IgxExcelExporterService); + private readonly csvExporter = inject(IgxCsvExporterService); + private readonly pdfExporter = inject(IgxPdfExporterService); + private readonly platformId = inject(PLATFORM_ID); + private readonly currencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }); + private readonly percentFormatter = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + private readonly financeNonExportableFields = new Set(['priceTrend']); + private readonly viewportWidth = signal(isPlatformBrowser(this.platformId) ? window.innerWidth : 1280); + private readonly resizeHandler = (): void => this.viewportWidth.set(window.innerWidth); + + private readonly financeSeedRows: FinanceSeedRow[] = [ + { ticker: 'AMD', company: 'Advanced Micro Devices Inc.', lastPrice: 130.36, changePct: 0, marketValue: 3128.64, netProfit: 1315.68, netProfitPct: 72.57, allocationPct: 4.38, averageCost: 75.54, position: 24, holdingPeriodDays: 157, tone: 'blue' }, + { ticker: 'ETH', company: 'Ethereum', lastPrice: 3567.93, changePct: -0.2, marketValue: 10073.79, netProfit: 3074.79, netProfitPct: 40.3, allocationPct: 18.43, averageCost: 2543, position: 3, holdingPeriodDays: 120, tone: 'slate' }, + { ticker: 'ABNB', company: 'Airbnb Inc.', lastPrice: 130.65, changePct: -0.77, marketValue: 3004.95, netProfit: 1709.59, netProfitPct: 131.98, allocationPct: 3.13, averageCost: 56.32, position: 23, holdingPeriodDays: 540, tone: 'red' }, + { ticker: 'BABA', company: 'Alibaba Group Holding Limited', lastPrice: 81.81, changePct: 1.22, marketValue: 981.72, netProfit: -310.92, netProfitPct: -24.05, allocationPct: 3.12, averageCost: 107.72, position: 12, holdingPeriodDays: 91, tone: 'orange' }, + { ticker: 'BTC', company: 'Bitcoin', lastPrice: 95300.86, changePct: 0.72, marketValue: 19060.17, netProfit: 7013.97, netProfitPct: 58.23, allocationPct: 29.1, averageCost: 60231, position: 0.2, holdingPeriodDays: 323, tone: 'gold' }, + { ticker: 'AAPL', company: 'Apple Inc.', lastPrice: 223.36, changePct: 0.45, marketValue: 245.7, netProfit: 76.07, netProfitPct: 44.84, allocationPct: 0.41, averageCost: 154.21, position: 1.1, holdingPeriodDays: 632, tone: 'slate' }, + { ticker: 'MSFT', company: 'Microsoft Corp.', lastPrice: 401.22, changePct: 0.5, marketValue: 280.85, netProfit: -20.15, netProfitPct: -6.69, allocationPct: 0.73, averageCost: 430, position: 0.7, holdingPeriodDays: 342, tone: 'blue' }, + { ticker: 'GOOGL', company: 'Alphabet Inc.', lastPrice: 160.02, changePct: 1.25, marketValue: 208.03, netProfit: -18.17, netProfitPct: -8.03, allocationPct: 0.55, averageCost: 174, position: 1.3, holdingPeriodDays: 376, tone: 'green' }, + { ticker: 'AMZN', company: 'Amazon.com Inc.', lastPrice: 205.87, changePct: 0.49, marketValue: 885.24, netProfit: -285.82, netProfitPct: -24.41, allocationPct: 2.83, averageCost: 272.34, position: 4.3, holdingPeriodDays: 352, tone: 'gold' }, + { ticker: 'TSLA', company: 'Tesla Inc.', lastPrice: 332.92, changePct: 0.9, marketValue: 1598.02, netProfit: 157.01, netProfitPct: 10.9, allocationPct: 3.48, averageCost: 300.21, position: 4.8, holdingPeriodDays: 452, tone: 'red' } + ]; + + protected readonly hideMostColumns = computed(() => this.viewportWidth() < 700); + protected readonly financeRows: FinanceRow[] = this.financeSeedRows.map((row) => ({ ...row, priceTrend: this.buildPriceTrend(row.ticker, row.changePct) })); + protected readonly maxAllocationPct = Math.max(...this.financeRows.map((row) => Math.abs(row.allocationPct)), 0); + protected readonly financeAssetFilter = signal(''); + protected readonly filteredFinanceRows = computed(() => { + const query = this.financeAssetFilter().trim().toLowerCase(); + if (!query) { + return this.financeRows; + } + + return this.financeRows.filter((row) => row.ticker.toLowerCase().includes(query) || row.company.toLowerCase().includes(query)); + }); public ngOnInit(): void { - this.financialService.getData(); - this.data$ = this.financialService.records; - this.data$.subscribe((data) => { - if (data.length !== 0) { - this.isLoading = false; - } - }); + this.configureExportExclusions(); + if (isPlatformBrowser(this.platformId)) { + window.addEventListener('resize', this.resizeHandler); + } } public ngOnDestroy(): void { - if (this._timer) { - clearInterval(this._timer); + if (isPlatformBrowser(this.platformId)) { + window.removeEventListener('resize', this.resizeHandler); } } - protected filter(event: Event) { - const value = (event.target as HTMLInputElement).value; - const expressionTree = new FilteringExpressionsTree(FilteringLogic.Or); - const tickerExpression = { - condition: IgxStringFilteringOperand.instance().condition('contains'), - fieldName: 'id', - searchVal: value, - ignoreCase: true - }; - const assetExpression = { - condition: IgxStringFilteringOperand.instance().condition('contains'), - fieldName: 'holdingName', - searchVal: value, - ignoreCase: true + protected onFinanceAssetSearch(event: Event): void { + const target = event.target; + if (!(target instanceof HTMLInputElement)) { + return; + } + + this.financeAssetFilter.set(target.value); + } + + protected financeAvatarInitials(ticker: string): string { + const normalized = ticker.replace(/[^A-Z0-9]/gi, '').toUpperCase(); + return normalized.slice(0, 3) || 'EQ'; + } + + protected financeAvatarColor(tone: FinanceTone): string { + const colors: Record = { + blue: '#2f74c0', + orange: '#d97b23', + green: '#3ea35b', + slate: '#5c7399', + gold: '#c7a43b', + red: '#bf4a4a' }; - expressionTree.filteringOperands.push(tickerExpression,assetExpression); - if (value) { - this.grid.filteringExpressionsTree = expressionTree; - } else { - this.grid.clearFilter(); + + return colors[tone]; + } + + protected formatCurrency(value: number): string { + return this.currencyFormatter.format(value); + } + + protected formatPercent(value: number): string { + return `${this.percentFormatter.format(Math.abs(value))}%`; + } + + protected badgeType(value: number): string { + if (value > 0) return 'success'; + if (value < 0) return 'error'; + return 'info'; + } + + protected badgeLabel(value: number): string { + const arrow = value > 0 ? '↑ ' : value < 0 ? '↓ ' : ''; + return `${arrow}${this.formatPercent(value)}`; + } + + protected allocationWidth(value: number): string { + const magnitude = Math.abs(value); + if (magnitude === 0 || this.maxAllocationPct <= 0) { + return '0%'; } + + const scaled = (magnitude / this.maxAllocationPct) * 100; + return `${Math.min(100, Math.max(6, scaled))}%`; } - protected getPathToImage(companyName: string) { - return `companies/${companyName.split(' ')[0]}.png`; + protected sparklineBrush(changePct: number): string { + return changePct < 0 ? '#e34b4b' : '#1dbf63'; } - private profitConditionHandler(rowData: any, columnKey: string) { - return rowData[columnKey] >= 0; + protected sparklineNegativeBrush(changePct: number): string { + return changePct < 0 ? '#ff7a7a' : '#e34b4b'; } - private lossConditionHandler(rowData: any, columnKey: string) { - return rowData[columnKey] < 0; + private configureExportExclusions(): void { + const skipChartColumns = (args: IColumnExportingEventArgs): void => { + if (this.financeNonExportableFields.has(args.field)) { + args.cancel = true; + } + }; + + this.excelExporter.columnExporting.subscribe(skipChartColumns); + this.csvExporter.columnExporting.subscribe(skipChartColumns); + this.pdfExporter.columnExporting.subscribe(skipChartColumns); + } + + private buildPriceTrend(ticker: string, changePct: number): PriceTrendPoint[] { + const points = 30; + const target = Number(changePct.toFixed(2)); + const volatility = Math.max(0.2, Math.min(1.15, Math.abs(changePct) * 0.55 + 0.25)); + const hashSeed = ticker.split('').reduce((acc, ch) => ((acc * 31) + ch.charCodeAt(0)) >>> 0, 2166136261); + let seed = hashSeed; + let value = 0; + const series: PriceTrendPoint[] = []; + + const nextRandom = (): number => { + seed ^= seed << 13; + seed ^= seed >>> 17; + seed ^= seed << 5; + return (seed >>> 0) / 4294967296; + }; + + for (let index = 0; index < points; index++) { + const t = index / (points - 1); + const directionalTarget = target * t; + const randomStep = (nextRandom() - 0.5) * volatility; + const pull = (directionalTarget - value) * 0.22; + const microWave = Math.sin((t * Math.PI * 9) + (hashSeed % 11)) * 0.06; + value += randomStep + pull + microWave; + series.push({ value: Number(value.toFixed(2)) }); + } + + if (series.length > 0) { + series[series.length - 1] = { value: target }; + } + + return series; } } diff --git a/projects/finance-grid/src/styles.scss b/projects/finance-grid/src/styles.scss index 9e6ad246..4a9b818e 100644 --- a/projects/finance-grid/src/styles.scss +++ b/projects/finance-grid/src/styles.scss @@ -1,8 +1,21 @@ +@use "igniteui-angular/theming" as *; + +@include core(); +@include typography( + $font-family: $bootstrap-typeface, + $type-scale: $bootstrap-type-scale +); +@include theme( + $palette: $light-bootstrap-palette, + $schema: $light-bootstrap-schema +); + html { margin: 0; padding: 0; height: 100%; } + body { margin: 0; padding: 8px; diff --git a/projects/fleet-management-grid/public/assets/car_images.json b/projects/fleet-management-grid/public/assets/car_images.json new file mode 100644 index 00000000..14dc46fc --- /dev/null +++ b/projects/fleet-management-grid/public/assets/car_images.json @@ -0,0 +1,257 @@ +{ + "Ford F-150 blue": [ + "Ford-F150-caleb-white-XGJBSkoqX_I-unsplash.jpg", + "Ford-F150-caleb-white-XGJBSkoqX_I-unsplash1.jpg", + "Ford-F150-caleb-white-XGJBSkoqX_I-unsplash2.jpg" + ], + "Ford F-150 red": [ + "Ford-f150-7572360_1920.jpg", + "Ford-f150-7572360_19201.jpg", + "Ford-f150-7572360_19202.jpg" + ], + "Ford Focus blue": [ + "ford-focus-5167838_1280.jpg", + "ford-focus-5167838_12801.jpg", + "ford-focus-5167838_12802.jpg" + ], + "Ford Focus dark gray": [ + "1-1-Ford-Focus-car-3300587_1280.jpg", + "1-2-Ford-Focus-car-3300588_1280.jpg", + "1-3-Ford-Focus-car-3300587_1280.jpg" + ], + "Ford Mustang blue": [ + "Ford-Mustang-automobile-1450573_1920.jpg", + "Ford-Mustang-automobile-1450573_19201.jpg", + "Ford-Mustang-automobile-1450573_19202.jpg" + ], + "Ford Mustang red": [ + "Ford-Mustang-convertible-1630448_1920.jpg", + "Ford-Mustang-convertible-1630448_19201.jpg", + "Ford-Mustang-convertible-1630448_19202.jpg" + ], + "Honda Civic": [ + "honda-civic-4967605_1920.jpg", + "honda-civic-4967605_19201.jpg", + "honda-civic-4967605_19202.jpg" + ], + "Hyundai ix35": [ + "Hyundai-ix35-5505817_1920.jpg", + "Hyundai-ix35-5505817_19201.jpg", + "Hyundai-ix35-5505817_19202.jpg" + ], + "Hyundai Kona": [ + "Hyundai-Kona-electric-4964667_1920.jpg", + "Hyundai-Kona-electric-4964667_19201.jpg", + "Hyundai-Kona-electric-4964667_19202.jpg" + ], + "Kia Ceed red": [ + "Kia-Ceed-car-3305685_1280.jpg", + "Kia-ceed-car-3305686_1920.jpg", + "Kia-Ceed-car-3305699_1280.jpg" + ], + "Kia Ceed white": [ + "Kia-Ceed-car-6097887_1920.jpg", + "Kia-Ceed-car-6097887_19201.jpg", + "Kia-Ceed-car-6097887_19202.jpg" + ], + "Kia EV6 gray": [ + "KIA-EV6-hyundai-motor-group-gKODwMitO-c-unsplash.jpg", + "KIA-EV6-hyundai-motor-group-gKODwMitO-c-unsplash1.jpg", + "KIA-EV6-hyundai-motor-group-gKODwMitO-c-unsplash2.jpg" + ], + "Kia EV6 red": [ + "KIA-EV6-hyundai-motor-group-jdjzmbw4GPg-unsplash.jpg", + "KIA-EV6-hyundai-motor-group-jdjzmbw4GPg-unsplash1.jpg", + "KIA-EV6-hyundai-motor-group-jdjzmbw4GPg-unsplash2.jpg" + ], + "Kia EV6 GT": [ + "KIA-EV6-GT-line edited hyundai-motor-group-ErBFxMkJpKk-unsplash-detail1.jpg", + "KIA-EV6-GT-line edited hyundai-motor-group-ErBFxMkJpKk-unsplash.jpg", + "KIA-EV6-GT-line edited hyundai-motor-group-ErBFxMkJpKk-unsplash2.jpg" + ], + "Kia Soul": [ + "Kia-Soul-car-2441815_1920.jpg", + "Kia-Soul-car-2441815_19201.jpg", + "Kia-Soul-car-2441815_19202.jpg" + ], + "Kia Sportage": [ + "Kia-Sportage-2382086_1920.jpg", + "Kia-Sportage-2382086_19201.jpg", + "Kia-Sportage-2382086_19202.jpg" + ], + "Mazda 3": [ + "mazda-3-7498005_1920.jpg", + "mazda-3-7498005_19201.jpg", + "mazda-3-7498005_19202.jpg" + ], + "Mazda 6": [ + "3-1-Mazda-6-car-6122178_1920.jpg", + "3-2-Mazda-6-car-6122177_1920.jpg", + "3-3-Mazda-6-car-6122177_1920.jpg" + ], + "Mazda MX-5": [ + "2-1-Mazda-MX-5-cabriolet-3627312_1920.jpg", + "2-2-Mazda-MX-5-cabriolet-3708152_1920.jpg", + "2-3-Mazda-MX-5-cabriolet-3708152_1920.jpg" + ], + "Tesla 3 red": [ + "Tesla-3-car-8607713_1920.jpg", + "Tesla-3-car-8607713_19201.jpg", + "Tesla-3-car-8607713_19202.jpg" + ], + "Tesla 3 white 1": [ + "tesla-3-5937063_1920.jpg", + "tesla-3-5937063_19201.jpg", + "tesla-3-5937063_19202.jpg" + ], + "Tesla 3 white 2": [ + "Tesla-3-charlie-deets-AkgALppFIwo-unsplash.jpg", + "Tesla-3-charlie-deets-AkgALppFIwo-unsplash1.jpg", + "Tesla-3-charlie-deets-AkgALppFIwo-unsplash2.jpg" + ], + "Tesla 3 white 3": [ + "Tesla-3-i-m-zion-A-JEMot0hWs-unsplash.jpg", + "Tesla-3-i-m-zion-A-JEMot0hWs-unsplash1.jpg", + "Tesla-3-i-m-zion-A-JEMot0hWs-unsplash2.jpg" + ], + "Toyota Corolla": [ + "toyota-corolla-347288_1920.jpg", + "toyota-corolla-347288_19201.jpg", + "toyota-corolla-347288_19202.jpg" + ], + "Toyota RAV4 1": [ + "Toyota-RAV4-gino-marcelo-hernandez-sanchez-MN0-x2hDNrc-unsplash.jpg", + "Toyota-RAV4-gino-marcelo-hernandez-sanchez-MN0-x2hDNrc-unsplash1.jpg", + "Toyota-RAV4-gino-marcelo-hernandez-sanchez-MN0-x2hDNrc-unsplash2.jpg" + ], + "Toyota RAV4 2": [ + "Toyota-RAV4-krish-parmar-PmSwFm4Lw1c-unsplash.jpg", + "Toyota-RAV4-krish-parmar-PmSwFm4Lw1c-unsplash1.jpg", + "Toyota-RAV4-krish-parmar-PmSwFm4Lw1c-unsplash2.jpg" + ], + "Toyota RAV4 3": [ + "Toyota-RAV4-stephen-andrews-28Lmg9YaTFY-unsplash.jpg", + "Toyota-RAV4-stephen-andrews-28Lmg9YaTFY-unsplash1.jpg", + "Toyota-RAV4-stephen-andrews-28Lmg9YaTFY-unsplash2.jpg" + ], + "Toyota Tundra": [ + "toyota-tundra-1241658_1920.jpg", + "toyota-tundra-1241658_19201.jpg", + "toyota-tundra-1241658_19202.jpg" + ], + "VW Caddy 1": [ + "VW_Caddy-ollie-walls-jDo2IZm-uFc-unsplash.jpg", + "VW_Caddy-ollie-walls-jDo2IZm-uFc-unsplash1.jpg", + "VW_Caddy-ollie-walls-jDo2IZm-uFc-unsplash2.jpg" + ], + "VW Caddy 2": [ + "VW-Caddy-daniil-lyusov-fesux3IvcVo-unsplash-2.jpg", + "VW-Caddy-daniil-lyusov-fesux3IvcVo-unsplash-21.jpg", + "VW-Caddy-daniil-lyusov-fesux3IvcVo-unsplash-22.jpg" + ], + "VW Golf": [ + "vw-Golf-4332807_1280.jpg", + "vw-Golf-4332807_12801.jpg", + "VW-Golf-car-5671331_1280.jpg" + ], + "VW Passat black": [ + "VW-passat-768159_1280.jpg", + "VW-passat-768159_12801.jpg", + "VW-passat-768159_12802.jpg" + ], + "VW Passat gray": [ + "VW-Passat-car-866764_1280.jpg", + "VW-Passat-car-866769_1280.jpg", + "VW-Passat-car-866769_12801.jpg" + ], + "VW Polo": [ + "VW-Polo-pexels-hellojoshwithers-16625624.jpg", + "VW-Polo-pexels-hellojoshwithers-166256241.jpg", + "VW-Polo-pexels-hellojoshwithers-166256242.jpg" + ], + "VW Touareg 1": [ + "4-1-VW-Touareg-automobile-4061855_1920.jpg", + "4-2-VW-Touareg-338895_1920.jpg", + "4-3-VW-Touareg-automobile-4061855_1920.jpg" + ], + "VW Touareg 2": [ + "5-1-VW-Touareg-pexels-ardit-mbrati-216809103-16646414.jpg", + "5-2-VW-Touareg-pexels-ardit-mbrati-216809103-16646416.jpg", + "5-3-VW-Touareg-pexels-ardit-mbrati-216809103-16646412.jpg" + ], + "Ford Focus silver": [ + "1-1-Ford-Focus-car-3300587_1280.jpg", + "1-2-Ford-Focus-car-3300588_1280.jpg", + "1-3-Ford-Focus-car-3300587_1280.jpg" + ], + "Hyundai Elantra graphite": [ + "mazda-3-7498005_1920.jpg", + "mazda-3-7498005_19201.jpg", + "mazda-3-7498005_19202.jpg" + ], + "Subaru Outback autumn green": [ + "Toyota-RAV4-stephen-andrews-28Lmg9YaTFY-unsplash.jpg", + "Toyota-RAV4-stephen-andrews-28Lmg9YaTFY-unsplash1.jpg", + "Toyota-RAV4-stephen-andrews-28Lmg9YaTFY-unsplash2.jpg" + ], + "Mazda CX-5 machine gray": [ + "Toyota-RAV4-krish-parmar-PmSwFm4Lw1c-unsplash.jpg", + "Toyota-RAV4-krish-parmar-PmSwFm4Lw1c-unsplash1.jpg", + "Toyota-RAV4-krish-parmar-PmSwFm4Lw1c-unsplash2.jpg" + ], + "Chevrolet Malibu summit white": [ + "tesla-3-5937063_1920.jpg", + "tesla-3-5937063_19201.jpg", + "tesla-3-5937063_19202.jpg" + ], + "Nissan Altima gun metallic": [ + "VW-Passat-car-866764_1280.jpg", + "VW-Passat-car-866769_1280.jpg", + "VW-Passat-car-866769_12801.jpg" + ], + "Honda CR-V urban gray": [ + "Toyota-RAV4-gino-marcelo-hernandez-sanchez-MN0-x2hDNrc-unsplash.jpg", + "Toyota-RAV4-gino-marcelo-hernandez-sanchez-MN0-x2hDNrc-unsplash1.jpg", + "Toyota-RAV4-gino-marcelo-hernandez-sanchez-MN0-x2hDNrc-unsplash2.jpg" + ], + "Ford Escape carbonized gray": [ + "Hyundai-ix35-5505817_1920.jpg", + "Hyundai-ix35-5505817_19201.jpg", + "Hyundai-ix35-5505817_19202.jpg" + ], + "Volkswagen Tiguan atlantic blue": [ + "5-1-VW-Touareg-pexels-ardit-mbrati-216809103-16646414.jpg", + "5-2-VW-Touareg-pexels-ardit-mbrati-216809103-16646416.jpg", + "5-3-VW-Touareg-pexels-ardit-mbrati-216809103-16646412.jpg" + ], + "Jeep Cherokee granite crystal": [ + "toyota-tundra-1241658_1920.jpg", + "toyota-tundra-1241658_19201.jpg", + "toyota-tundra-1241658_19202.jpg" + ], + "Chevrolet Equinox mosaic black": [ + "KIA-EV6-GT-line edited hyundai-motor-group-ErBFxMkJpKk-unsplash-detail1.jpg", + "KIA-EV6-GT-line edited hyundai-motor-group-ErBFxMkJpKk-unsplash.jpg", + "KIA-EV6-GT-line edited hyundai-motor-group-ErBFxMkJpKk-unsplash2.jpg" + ], + "Hyundai Tucson amazon gray": [ + "Hyundai-Kona-electric-4964667_1920.jpg", + "Hyundai-Kona-electric-4964667_19201.jpg", + "Hyundai-Kona-electric-4964667_19202.jpg" + ], + "Nissan Rogue caspian blue": [ + "Kia-Sportage-2382086_1920.jpg", + "Kia-Sportage-2382086_19201.jpg", + "Kia-Sportage-2382086_19202.jpg" + ], + "Ford Transit Connect frozen white": [ + "VW_Caddy-ollie-walls-jDo2IZm-uFc-unsplash.jpg", + "VW_Caddy-ollie-walls-jDo2IZm-uFc-unsplash1.jpg", + "VW_Caddy-ollie-walls-jDo2IZm-uFc-unsplash2.jpg" + ], + "Ram ProMaster City bright white": [ + "VW-Caddy-daniil-lyusov-fesux3IvcVo-unsplash-2.jpg", + "VW-Caddy-daniil-lyusov-fesux3IvcVo-unsplash-21.jpg", + "VW-Caddy-daniil-lyusov-fesux3IvcVo-unsplash-22.jpg" + ] +} diff --git a/projects/fleet-management-grid/src/app/app.component.scss b/projects/fleet-management-grid/src/app/app.component.scss index 0853dbd0..478abc91 100644 --- a/projects/fleet-management-grid/src/app/app.component.scss +++ b/projects/fleet-management-grid/src/app/app.component.scss @@ -1,14 +1,4 @@ -@use "igniteui-angular/theming" as *; - -@include core(); -@include typography(); -@include theme( - $palette: $dark-material-palette, - $schema: $dark-material-schema -); - -.overlay igx-card { - border: 0px; - border-radius: 4px; - box-shadow: var(--ig-elevation-24); +:host { + display: block; + height: 100%; } diff --git a/projects/fleet-management-grid/src/app/fleet-management-grid/fleet-management-grid.component.html b/projects/fleet-management-grid/src/app/fleet-management-grid/fleet-management-grid.component.html index 91584b7f..f8255457 100644 --- a/projects/fleet-management-grid/src/app/fleet-management-grid/fleet-management-grid.component.html +++ b/projects/fleet-management-grid/src/app/fleet-management-grid/fleet-management-grid.component.html @@ -1,332 +1,192 @@ - - - Fleet Management - - @if (grid.sortingExpressions.length > 0) { - - } - - - - - - - - - - - - {{ value }} - - - - - - - - - - - {{ value }} - - - - - - {{ cell.value }} - - - - - - - - - Details - - - -
-