@ojiepermana/angular-chart 22.0.36 → 22.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,8 +32,8 @@ import { BarChart } from '@ojiepermana/angular-chart/bar';
32
32
 
33
33
  ### Supported chart entrypoints
34
34
 
35
- | Import path | Selector | Best for |
36
- | ---------------------- | -------------- | ----------------------------------------------------------------- |
35
+ | Import path | Selector | Best for |
36
+ | ------------------------------------ | -------------- | ----------------------------------------------------------------- |
37
37
  | `@ojiepermana/angular-chart/bar` | `ChartBar` | grouped, stacked, horizontal, active, and mixed-color bar layouts |
38
38
  | `@ojiepermana/angular-chart/line` | `ChartLine` | trends, comparisons, and zoomable time-series lines |
39
39
  | `@ojiepermana/angular-chart/area` | `ChartArea` | filled trend bands, stacked areas, and expanded percent views |
@@ -47,17 +47,17 @@ import { BarChart } from '@ojiepermana/angular-chart/bar';
47
47
  The Angular structure keeps the same high-level idea as shadcn Chart: define a shared chart config once, then compose a specific chart type with optional grid, axis, tooltip, legend, and center content.
48
48
 
49
49
  ```text
50
- chart
51
- ├── chart-bar | chart-line | chart-area | chart-scatter
52
- │ ├── svg:g[chart-grid]
53
- │ ├── svg:g[chart-axis-x]
54
- │ ├── svg:g[chart-axis-y]
55
- │ └── svg:g[chart-crosshair] / svg:g[chart-brush] (optional)
56
- ├── chart-pie | chart-radial | chart-radar
57
- │ └── chart-pie-center | chart-radial-center (optional)
58
- ├── chart-tooltip
59
- ├── chart-legend
60
- └── chart-zoom-controls (optional)
50
+ Chart
51
+ ├── ChartBar | ChartLine | ChartArea | ChartScatter
52
+ │ ├── svg:g[ChartGrid]
53
+ │ ├── svg:g[ChartAxisX]
54
+ │ ├── svg:g[ChartAxisY]
55
+ │ └── svg:g[ChartCrosshair] / svg:g[ChartBrush] (optional)
56
+ ├── ChartPie | ChartRadial | ChartRadar
57
+ │ └── ChartPieCenter | ChartRadialCenter (optional)
58
+ ├── ChartTooltip
59
+ ├── ChartLegend
60
+ └── ChartZoomControls (optional)
61
61
  ```
62
62
 
63
63
  All child primitives read dimensions, series metadata, and scoped color tokens from `Chart`.
@@ -168,7 +168,7 @@ Use `colorKey` when the dataset itself carries a color token such as `fill`, and
168
168
  | --------- | ---------------- | ---------------- |
169
169
  | `config` | `ChartConfig` | — |
170
170
  | `aspect` | `string` | `'aspect-video'` |
171
- | `ChartId` | `string \| null` | auto-generated |
171
+ | `chartId` | `string \| null` | auto-generated |
172
172
 
173
173
  `ChartConfig` is a readonly record of series keys to labels, icons, colors, or theme-aware light/dark color maps.
174
174
 
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, input, output, computed, effect, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { seriesColorVar, ChartContext, CartesianContext, elementClientCenter } from '@ojiepermana/angular-chart/core';
3
+ import { seriesColorVar, ChartContext, CartesianContext, ChartThemeRadius, elementClientCenter } from '@ojiepermana/angular-chart/core';
4
4
  import { scaleBand, scaleLinear } from 'd3-scale';
5
5
  import { min, max } from 'd3-array';
6
6
  import { stack } from 'd3-shape';
@@ -329,6 +329,7 @@ const defaultBarValueFormatter = (value) => `${value}`;
329
329
  class BarChart {
330
330
  root = inject(ChartContext);
331
331
  cart = inject(CartesianContext);
332
+ themeRadius = inject(ChartThemeRadius);
332
333
  data = input.required(/* @ts-ignore */
333
334
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
334
335
  xKey = input.required(/* @ts-ignore */
@@ -345,7 +346,7 @@ class BarChart {
345
346
  ...(ngDevMode ? [{ debugName: "bandPadding" }] : /* istanbul ignore next */ []));
346
347
  groupPadding = input(0.05, /* @ts-ignore */
347
348
  ...(ngDevMode ? [{ debugName: "groupPadding" }] : /* istanbul ignore next */ []));
348
- cornerRadius = input(4, /* @ts-ignore */
349
+ cornerRadius = input(null, /* @ts-ignore */
349
350
  ...(ngDevMode ? [{ debugName: "cornerRadius" }] : /* istanbul ignore next */ []));
350
351
  colorKey = input(undefined, /* @ts-ignore */
351
352
  ...(ngDevMode ? [{ debugName: "colorKey" }] : /* istanbul ignore next */ []));
@@ -364,7 +365,7 @@ class BarChart {
364
365
  dotGap = input(2, /* @ts-ignore */
365
366
  ...(ngDevMode ? [{ debugName: "dotGap" }] : /* istanbul ignore next */ []));
366
367
  /** Corner radius of each `dot` cell. */
367
- dotCornerRadius = input(2, /* @ts-ignore */
368
+ dotCornerRadius = input(null, /* @ts-ignore */
368
369
  ...(ngDevMode ? [{ debugName: "dotCornerRadius" }] : /* istanbul ignore next */ []));
369
370
  /** Render the faint full-height cell track behind the `dot` values. */
370
371
  showDotTrack = input(true, /* @ts-ignore */
@@ -374,6 +375,12 @@ class BarChart {
374
375
  ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
375
376
  innerHeight = computed(() => Math.max(0, this.root.dimensions().height - this.margin().top - this.margin().bottom), /* @ts-ignore */
376
377
  ...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
378
+ /** Bar corner radius; an unset input follows the live theme-radius axis. */
379
+ resolvedCornerRadius = computed(() => this.cornerRadius() ?? this.themeRadius.px(), /* @ts-ignore */
380
+ ...(ngDevMode ? [{ debugName: "resolvedCornerRadius" }] : /* istanbul ignore next */ []));
381
+ /** Dot-cell corner radius; an unset input follows the theme-radius axis (smaller, SVG clamps to half the cell). */
382
+ resolvedDotCornerRadius = computed(() => this.dotCornerRadius() ?? Math.min(this.themeRadius.px() * 0.5, 4), /* @ts-ignore */
383
+ ...(ngDevMode ? [{ debugName: "resolvedDotCornerRadius" }] : /* istanbul ignore next */ []));
377
384
  layout = computed(() => computeBarLayout({
378
385
  data: this.data(),
379
386
  xKey: this.xKey(),
@@ -424,6 +431,8 @@ class BarChart {
424
431
  return `Bar chart, ${n} categories, ${keys.length} series: ${keys.join(', ')}.`;
425
432
  }, /* @ts-ignore */
426
433
  ...(ngDevMode ? [{ debugName: "ariaSummary" }] : /* istanbul ignore next */ []));
434
+ visibleSeriesKeys = computed(() => this.root.visibleSeriesKeys(), /* @ts-ignore */
435
+ ...(ngDevMode ? [{ debugName: "visibleSeriesKeys" }] : /* istanbul ignore next */ []));
427
436
  constructor() {
428
437
  effect(() => {
429
438
  const layout = this.layout();
@@ -495,6 +504,28 @@ class BarChart {
495
504
  const label = this.root.config()[bar.seriesKey]?.label ?? bar.seriesKey;
496
505
  return `${label} in ${bar.category}: ${bar.value}`;
497
506
  }
507
+ onKeydown(event, index) {
508
+ const key = event.key;
509
+ let targetIndex = -1;
510
+ if (key === 'ArrowRight' || key === 'ArrowDown') {
511
+ targetIndex = index + 1;
512
+ }
513
+ else if (key === 'ArrowLeft' || key === 'ArrowUp') {
514
+ targetIndex = index - 1;
515
+ }
516
+ if (targetIndex >= 0 && targetIndex < this.bars().length) {
517
+ event.preventDefault();
518
+ const svgEl = event.currentTarget;
519
+ const parent = svgEl.parentElement;
520
+ if (parent) {
521
+ const rects = parent.querySelectorAll('.chart-bar');
522
+ const targetRect = rects[targetIndex];
523
+ if (targetRect) {
524
+ targetRect.focus();
525
+ }
526
+ }
527
+ }
528
+ }
498
529
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: BarChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
499
530
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.3", type: BarChart, isStandalone: true, selector: "ChartBar", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, xKey: { classPropertyName: "xKey", publicName: "xKey", isSignal: true, isRequired: true, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, styles: { classPropertyName: "styles", publicName: "styles", isSignal: true, isRequired: false, transformFunction: null }, margin: { classPropertyName: "margin", publicName: "margin", isSignal: true, isRequired: false, transformFunction: null }, bandPadding: { classPropertyName: "bandPadding", publicName: "bandPadding", isSignal: true, isRequired: false, transformFunction: null }, groupPadding: { classPropertyName: "groupPadding", publicName: "groupPadding", isSignal: true, isRequired: false, transformFunction: null }, cornerRadius: { classPropertyName: "cornerRadius", publicName: "cornerRadius", isSignal: true, isRequired: false, transformFunction: null }, colorKey: { classPropertyName: "colorKey", publicName: "colorKey", isSignal: true, isRequired: false, transformFunction: null }, activeKey: { classPropertyName: "activeKey", publicName: "activeKey", isSignal: true, isRequired: false, transformFunction: null }, activeValue: { classPropertyName: "activeValue", publicName: "activeValue", isSignal: true, isRequired: false, transformFunction: null }, showValueLabels: { classPropertyName: "showValueLabels", publicName: "showValueLabels", isSignal: true, isRequired: false, transformFunction: null }, valueLabelFormat: { classPropertyName: "valueLabelFormat", publicName: "valueLabelFormat", isSignal: true, isRequired: false, transformFunction: null }, dotSize: { classPropertyName: "dotSize", publicName: "dotSize", isSignal: true, isRequired: false, transformFunction: null }, dotGap: { classPropertyName: "dotGap", publicName: "dotGap", isSignal: true, isRequired: false, transformFunction: null }, dotCornerRadius: { classPropertyName: "dotCornerRadius", publicName: "dotCornerRadius", isSignal: true, isRequired: false, transformFunction: null }, showDotTrack: { classPropertyName: "showDotTrack", publicName: "showDotTrack", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { barClick: "barClick" }, host: { properties: { "attr.data-style": "styles()" }, classAttribute: "relative block h-full w-full" }, providers: [CartesianContext], ngImport: i0, template: `
500
531
  <svg:svg
@@ -516,8 +547,8 @@ class BarChart {
516
547
  [attr.y]="cell.y"
517
548
  [attr.width]="cell.width"
518
549
  [attr.height]="cell.height"
519
- [attr.rx]="dotCornerRadius()"
520
- [attr.ry]="dotCornerRadius()"
550
+ [attr.rx]="resolvedDotCornerRadius()"
551
+ [attr.ry]="resolvedDotCornerRadius()"
521
552
  fill="hsl(var(--muted))" />
522
553
  }
523
554
  @for (cell of dotValueCells(); track cell.key) {
@@ -527,22 +558,22 @@ class BarChart {
527
558
  [attr.y]="cell.y"
528
559
  [attr.width]="cell.width"
529
560
  [attr.height]="cell.height"
530
- [attr.rx]="dotCornerRadius()"
531
- [attr.ry]="dotCornerRadius()"
561
+ [attr.rx]="resolvedDotCornerRadius()"
562
+ [attr.ry]="resolvedDotCornerRadius()"
532
563
  [attr.fill]="cell.color"
533
564
  [attr.opacity]="dotCellOpacity(cell)" />
534
565
  }
535
566
  </svg:g>
536
567
  }
537
- @for (bar of bars(); track bar.key) {
568
+ @for (bar of bars(); track bar.key; let idx = $index) {
538
569
  <svg:rect
539
570
  class="chart-bar cursor-pointer outline-none transition-opacity hover:opacity-80"
540
571
  [attr.x]="bar.x"
541
572
  [attr.y]="bar.y"
542
573
  [attr.width]="bar.width"
543
574
  [attr.height]="bar.height"
544
- [attr.rx]="cornerRadius()"
545
- [attr.ry]="cornerRadius()"
575
+ [attr.rx]="resolvedCornerRadius()"
576
+ [attr.ry]="resolvedCornerRadius()"
546
577
  [attr.fill]="barFill(bar)"
547
578
  [attr.opacity]="barOpacity(bar)"
548
579
  [attr.stroke]="bar.active ? 'hsl(var(--foreground))' : null"
@@ -553,7 +584,8 @@ class BarChart {
553
584
  (blur)="clearActivePoint()"
554
585
  (click)="emitClick(bar)"
555
586
  (keydown.enter)="emitClick(bar)"
556
- (keydown.space)="emitClick(bar); $event.preventDefault()" />
587
+ (keydown.space)="emitClick(bar); $event.preventDefault()"
588
+ (keydown)="onKeydown($event, idx)" />
557
589
  @if (showValueLabels() && bar.height > 0 && bar.width > 0) {
558
590
  <svg:text
559
591
  class="chart-bar-value pointer-events-none fill-muted-foreground text-2xs"
@@ -574,6 +606,27 @@ class BarChart {
574
606
  </svg:svg>
575
607
  <ng-content select="ChartTooltip" />
576
608
  <ng-content select="ChartLegend" />
609
+
610
+ <table class="sr-only" [attr.aria-label]="ariaSummary()">
611
+ <thead>
612
+ <tr>
613
+ <th scope="col">{{ xKey() }}</th>
614
+ @for (key of visibleSeriesKeys(); track key) {
615
+ <th scope="col">{{ key }}</th>
616
+ }
617
+ </tr>
618
+ </thead>
619
+ <tbody>
620
+ @for (row of data(); track row) {
621
+ <tr>
622
+ <th scope="row">{{ row[xKey()] }}</th>
623
+ @for (key of visibleSeriesKeys(); track key) {
624
+ <td>{{ row[key] }}</td>
625
+ }
626
+ </tr>
627
+ }
628
+ </tbody>
629
+ </table>
577
630
  `, isInline: true, dependencies: [{ kind: "directive", type: ChartPointerTracker, selector: "svg:svg[ChartPointerTracker]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
578
631
  }
579
632
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: BarChart, decorators: [{
@@ -604,8 +657,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
604
657
  [attr.y]="cell.y"
605
658
  [attr.width]="cell.width"
606
659
  [attr.height]="cell.height"
607
- [attr.rx]="dotCornerRadius()"
608
- [attr.ry]="dotCornerRadius()"
660
+ [attr.rx]="resolvedDotCornerRadius()"
661
+ [attr.ry]="resolvedDotCornerRadius()"
609
662
  fill="hsl(var(--muted))" />
610
663
  }
611
664
  @for (cell of dotValueCells(); track cell.key) {
@@ -615,22 +668,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
615
668
  [attr.y]="cell.y"
616
669
  [attr.width]="cell.width"
617
670
  [attr.height]="cell.height"
618
- [attr.rx]="dotCornerRadius()"
619
- [attr.ry]="dotCornerRadius()"
671
+ [attr.rx]="resolvedDotCornerRadius()"
672
+ [attr.ry]="resolvedDotCornerRadius()"
620
673
  [attr.fill]="cell.color"
621
674
  [attr.opacity]="dotCellOpacity(cell)" />
622
675
  }
623
676
  </svg:g>
624
677
  }
625
- @for (bar of bars(); track bar.key) {
678
+ @for (bar of bars(); track bar.key; let idx = $index) {
626
679
  <svg:rect
627
680
  class="chart-bar cursor-pointer outline-none transition-opacity hover:opacity-80"
628
681
  [attr.x]="bar.x"
629
682
  [attr.y]="bar.y"
630
683
  [attr.width]="bar.width"
631
684
  [attr.height]="bar.height"
632
- [attr.rx]="cornerRadius()"
633
- [attr.ry]="cornerRadius()"
685
+ [attr.rx]="resolvedCornerRadius()"
686
+ [attr.ry]="resolvedCornerRadius()"
634
687
  [attr.fill]="barFill(bar)"
635
688
  [attr.opacity]="barOpacity(bar)"
636
689
  [attr.stroke]="bar.active ? 'hsl(var(--foreground))' : null"
@@ -641,7 +694,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
641
694
  (blur)="clearActivePoint()"
642
695
  (click)="emitClick(bar)"
643
696
  (keydown.enter)="emitClick(bar)"
644
- (keydown.space)="emitClick(bar); $event.preventDefault()" />
697
+ (keydown.space)="emitClick(bar); $event.preventDefault()"
698
+ (keydown)="onKeydown($event, idx)" />
645
699
  @if (showValueLabels() && bar.height > 0 && bar.width > 0) {
646
700
  <svg:text
647
701
  class="chart-bar-value pointer-events-none fill-muted-foreground text-2xs"
@@ -662,6 +716,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
662
716
  </svg:svg>
663
717
  <ng-content select="ChartTooltip" />
664
718
  <ng-content select="ChartLegend" />
719
+
720
+ <table class="sr-only" [attr.aria-label]="ariaSummary()">
721
+ <thead>
722
+ <tr>
723
+ <th scope="col">{{ xKey() }}</th>
724
+ @for (key of visibleSeriesKeys(); track key) {
725
+ <th scope="col">{{ key }}</th>
726
+ }
727
+ </tr>
728
+ </thead>
729
+ <tbody>
730
+ @for (row of data(); track row) {
731
+ <tr>
732
+ <th scope="row">{{ row[xKey()] }}</th>
733
+ @for (key of visibleSeriesKeys(); track key) {
734
+ <td>{{ row[key] }}</td>
735
+ }
736
+ </tr>
737
+ }
738
+ </tbody>
739
+ </table>
665
740
  `,
666
741
  }]
667
742
  }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], xKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "xKey", required: true }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], styles: [{ type: i0.Input, args: [{ isSignal: true, alias: "styles", required: false }] }], margin: [{ type: i0.Input, args: [{ isSignal: true, alias: "margin", required: false }] }], bandPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "bandPadding", required: false }] }], groupPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupPadding", required: false }] }], cornerRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "cornerRadius", required: false }] }], colorKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "colorKey", required: false }] }], activeKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeKey", required: false }] }], activeValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeValue", required: false }] }], showValueLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValueLabels", required: false }] }], valueLabelFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueLabelFormat", required: false }] }], dotSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotSize", required: false }] }], dotGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotGap", required: false }] }], dotCornerRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotCornerRadius", required: false }] }], showDotTrack: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDotTrack", required: false }] }], barClick: [{ type: i0.Output, args: ["barClick"] }] } });
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Injectable, inject, ElementRef, Renderer2, effect, ChangeDetectionStrategy, Component, NgZone, PLATFORM_ID, DestroyRef, viewChild, input, afterNextRender } from '@angular/core';
3
- import { isPlatformBrowser } from '@angular/common';
2
+ import { signal, computed, Injectable, inject, ElementRef, Renderer2, effect, ChangeDetectionStrategy, Component, NgZone, PLATFORM_ID, DestroyRef, viewChild, input, afterNextRender, Service } from '@angular/core';
3
+ import { isPlatformBrowser, DOCUMENT } from '@angular/common';
4
4
  import { scalePoint, scaleLinear, scaleBand } from 'd3-scale';
5
5
  import { max } from 'd3-array';
6
6
  import { curveLinear, curveStep, curveMonotoneX, line, area, stack, stackOffsetExpand } from 'd3-shape';
@@ -756,8 +756,68 @@ function provideCartesianFromLineLayout(ctx, layout, orientation, innerWidth, in
756
756
  ctx.categories.set(layout.categories);
757
757
  }
758
758
 
759
+ /** 0.625rem @ 16px root — matches the theme `theme-radius='md'` default. */
760
+ const FALLBACK_RADIUS_PX = 10;
761
+ /**
762
+ * Resolves the live theme corner radius (the `--radius` token) in pixels for SVG
763
+ * chart geometry, and re-emits when the global `theme-radius` axis switches.
764
+ *
765
+ * SVG `rx`/`ry` need px numbers, so this reads the computed `--radius` off
766
+ * `<html>` and converts rem→px. A `MutationObserver` on the `<html theme-radius>`
767
+ * attribute keeps the `px` signal in sync with `ThemeRadiusService` at runtime;
768
+ * charts default their corner radius to this value (still overridable per chart).
769
+ *
770
+ * SSR-safe: with no document it returns the `md` fallback and skips observing.
771
+ */
772
+ class ChartThemeRadius {
773
+ document = inject(DOCUMENT, { optional: true });
774
+ basePx = signal(this.readBasePx(), /* @ts-ignore */
775
+ ...(ngDevMode ? [{ debugName: "basePx" }] : /* istanbul ignore next */ []));
776
+ /** Resolved base radius (`--radius`) in px; reactive to the theme-radius axis. */
777
+ px = this.basePx.asReadonly();
778
+ constructor() {
779
+ const root = this.document?.documentElement;
780
+ const view = this.document?.defaultView;
781
+ if (!root || !view || typeof view.MutationObserver !== 'function') {
782
+ return;
783
+ }
784
+ const observer = new view.MutationObserver(() => this.basePx.set(this.readBasePx()));
785
+ observer.observe(root, { attributes: true, attributeFilter: ['theme-radius'] });
786
+ inject(DestroyRef).onDestroy(() => observer.disconnect());
787
+ }
788
+ readBasePx() {
789
+ const root = this.document?.documentElement;
790
+ const view = this.document?.defaultView;
791
+ if (!root || !view) {
792
+ return FALLBACK_RADIUS_PX;
793
+ }
794
+ const raw = view.getComputedStyle(root).getPropertyValue('--radius').trim();
795
+ return this.toPx(raw, view, root) ?? FALLBACK_RADIUS_PX;
796
+ }
797
+ toPx(value, view, root) {
798
+ if (!value) {
799
+ return null;
800
+ }
801
+ const numeric = Number.parseFloat(value);
802
+ if (!Number.isFinite(numeric)) {
803
+ return null;
804
+ }
805
+ if (value.endsWith('rem')) {
806
+ const rootFontSize = Number.parseFloat(view.getComputedStyle(root).fontSize) || 16;
807
+ return numeric * rootFontSize;
808
+ }
809
+ // 'px' and the 9999px 'full' extreme parse directly.
810
+ return numeric;
811
+ }
812
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ChartThemeRadius, deps: [], target: i0.ɵɵFactoryTarget.Service });
813
+ static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ChartThemeRadius });
814
+ }
815
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ChartThemeRadius, decorators: [{
816
+ type: Service
817
+ }], ctorParameters: () => [] });
818
+
759
819
  /**
760
820
  * Generated bundle index. Do not edit.
761
821
  */
762
822
 
763
- export { CHART_DATA_ATTRIBUTE, CHART_THEMES, CartesianContext, CategoricalViewportContext, ChartContainer, ChartContext, ChartStyle, ScatterViewportContext, bandTicks, buildCartesianScales, buildChartCss, cloneLinear, computeAreaLayout, computeLineLayout, effectiveIndexRange, elementClientCenter, indexRangeSize, linearTicks, nearestCategoryIndex, normalizeIndexRange, normalizeNumericDomain, panIndexRange, panNumericDomain, pointToBandAdapter, provideCartesianFromLineLayout, seriesColorVar, sliceByIndexRange, xScale, yScale, zoomIndexRange, zoomNumericDomain };
823
+ export { CHART_DATA_ATTRIBUTE, CHART_THEMES, CartesianContext, CategoricalViewportContext, ChartContainer, ChartContext, ChartStyle, ChartThemeRadius, ScatterViewportContext, bandTicks, buildCartesianScales, buildChartCss, cloneLinear, computeAreaLayout, computeLineLayout, effectiveIndexRange, elementClientCenter, indexRangeSize, linearTicks, nearestCategoryIndex, normalizeIndexRange, normalizeNumericDomain, panIndexRange, panNumericDomain, pointToBandAdapter, provideCartesianFromLineLayout, seriesColorVar, sliceByIndexRange, xScale, yScale, zoomIndexRange, zoomNumericDomain };
@@ -1,5 +1,5 @@
1
1
  import { pie, arc } from 'd3-shape';
2
- import { seriesColorVar, ChartContext } from '@ojiepermana/angular-chart/core';
2
+ import { seriesColorVar, ChartContext, ChartThemeRadius } from '@ojiepermana/angular-chart/core';
3
3
  import * as i0 from '@angular/core';
4
4
  import { inject, input, output, computed, ChangeDetectionStrategy, Component } from '@angular/core';
5
5
 
@@ -48,6 +48,7 @@ function computePieLayout(input) {
48
48
  const DEFAULT_MARGIN = { top: 8, right: 8, bottom: 8, left: 8 };
49
49
  class PieChart {
50
50
  root = inject(ChartContext);
51
+ themeRadius = inject(ChartThemeRadius);
51
52
  data = input.required(/* @ts-ignore */
52
53
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
53
54
  valueKey = input.required(/* @ts-ignore */
@@ -64,7 +65,7 @@ class PieChart {
64
65
  ...(ngDevMode ? [{ debugName: "innerRadius" }] : /* istanbul ignore next */ []));
65
66
  padAngle = input(0.01, /* @ts-ignore */
66
67
  ...(ngDevMode ? [{ debugName: "padAngle" }] : /* istanbul ignore next */ []));
67
- cornerRadius = input(0, /* @ts-ignore */
68
+ cornerRadius = input(null, /* @ts-ignore */
68
69
  ...(ngDevMode ? [{ debugName: "cornerRadius" }] : /* istanbul ignore next */ []));
69
70
  startAngle = input(-Math.PI / 2, /* @ts-ignore */
70
71
  ...(ngDevMode ? [{ debugName: "startAngle" }] : /* istanbul ignore next */ []));
@@ -77,6 +78,8 @@ class PieChart {
77
78
  activeOffset = input(12, /* @ts-ignore */
78
79
  ...(ngDevMode ? [{ debugName: "activeOffset" }] : /* istanbul ignore next */ []));
79
80
  sliceClick = output();
81
+ resolvedCornerRadius = computed(() => this.cornerRadius() ?? this.themeRadius.px(), /* @ts-ignore */
82
+ ...(ngDevMode ? [{ debugName: "resolvedCornerRadius" }] : /* istanbul ignore next */ []));
80
83
  innerWidth = computed(() => Math.max(0, this.root.dimensions().width - this.margin().left - this.margin().right), /* @ts-ignore */
81
84
  ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
82
85
  innerHeight = computed(() => Math.max(0, this.root.dimensions().height - this.margin().top - this.margin().bottom), /* @ts-ignore */
@@ -90,7 +93,7 @@ class PieChart {
90
93
  innerHeight: this.innerHeight(),
91
94
  innerRadius: this.innerRadius(),
92
95
  padAngle: this.padAngle(),
93
- cornerRadius: this.cornerRadius(),
96
+ cornerRadius: this.resolvedCornerRadius(),
94
97
  startAngle: this.startAngle(),
95
98
  endAngle: this.endAngle(),
96
99
  activeIndex: this.activeIndex(),
@@ -1,6 +1,6 @@
1
1
  import { arc } from 'd3-shape';
2
2
  import { max } from 'd3-array';
3
- import { seriesColorVar, ChartContext } from '@ojiepermana/angular-chart/core';
3
+ import { seriesColorVar, ChartContext, ChartThemeRadius } from '@ojiepermana/angular-chart/core';
4
4
  import * as i0 from '@angular/core';
5
5
  import { inject, input, output, computed, ChangeDetectionStrategy, Component } from '@angular/core';
6
6
 
@@ -46,6 +46,7 @@ const DEFAULT_MARGIN = { top: 8, right: 8, bottom: 8, left: 8 };
46
46
  const defaultRadialValueFormatter = (value) => `${value}`;
47
47
  class RadialChart {
48
48
  root = inject(ChartContext);
49
+ themeRadius = inject(ChartThemeRadius);
49
50
  data = input.required(/* @ts-ignore */
50
51
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
51
52
  nameKey = input.required(/* @ts-ignore */
@@ -60,7 +61,7 @@ class RadialChart {
60
61
  ...(ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []));
61
62
  trackPadding = input(4, /* @ts-ignore */
62
63
  ...(ngDevMode ? [{ debugName: "trackPadding" }] : /* istanbul ignore next */ []));
63
- cornerRadius = input(8, /* @ts-ignore */
64
+ cornerRadius = input(null, /* @ts-ignore */
64
65
  ...(ngDevMode ? [{ debugName: "cornerRadius" }] : /* istanbul ignore next */ []));
65
66
  startAngle = input(-Math.PI / 2, /* @ts-ignore */
66
67
  ...(ngDevMode ? [{ debugName: "startAngle" }] : /* istanbul ignore next */ []));
@@ -75,6 +76,8 @@ class RadialChart {
75
76
  valueLabelFormat = input(defaultRadialValueFormatter, /* @ts-ignore */
76
77
  ...(ngDevMode ? [{ debugName: "valueLabelFormat" }] : /* istanbul ignore next */ []));
77
78
  barClick = output();
79
+ resolvedCornerRadius = computed(() => this.cornerRadius() ?? this.themeRadius.px(), /* @ts-ignore */
80
+ ...(ngDevMode ? [{ debugName: "resolvedCornerRadius" }] : /* istanbul ignore next */ []));
78
81
  innerWidth = computed(() => Math.max(0, this.root.dimensions().width - this.margin().left - this.margin().right), /* @ts-ignore */
79
82
  ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
80
83
  innerHeight = computed(() => Math.max(0, this.root.dimensions().height - this.margin().top - this.margin().bottom), /* @ts-ignore */
@@ -89,7 +92,7 @@ class RadialChart {
89
92
  trackPadding: this.trackPadding(),
90
93
  startAngle: this.startAngle(),
91
94
  endAngle: this.endAngle(),
92
- cornerRadius: this.cornerRadius(),
95
+ cornerRadius: this.resolvedCornerRadius(),
93
96
  maxValue: this.maxValue(),
94
97
  }), /* @ts-ignore */
95
98
  ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ojiepermana/angular-chart",
3
- "version": "22.0.36",
3
+ "version": "22.0.41",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/edsis/angular.git"
@@ -12,7 +12,7 @@
12
12
  "peerDependencies": {
13
13
  "@angular/common": ">=22.0.0",
14
14
  "@angular/core": ">=22.0.0",
15
- "@ojiepermana/angular-component": "^22.0.36"
15
+ "@ojiepermana/angular-component": "^22.0.41"
16
16
  },
17
17
  "dependencies": {
18
18
  "d3-array": "^3.2.4",
@@ -116,6 +116,7 @@ interface BarClickEvent {
116
116
  declare class BarChart {
117
117
  private readonly root;
118
118
  private readonly cart;
119
+ private readonly themeRadius;
119
120
  readonly data: _angular_core.InputSignal<readonly Readonly<Record<string, unknown>>[]>;
120
121
  readonly xKey: _angular_core.InputSignal<string>;
121
122
  readonly orientation: _angular_core.InputSignal<ChartOrientation>;
@@ -124,7 +125,7 @@ declare class BarChart {
124
125
  readonly margin: _angular_core.InputSignal<ChartMargin>;
125
126
  readonly bandPadding: _angular_core.InputSignal<number>;
126
127
  readonly groupPadding: _angular_core.InputSignal<number>;
127
- readonly cornerRadius: _angular_core.InputSignal<number>;
128
+ readonly cornerRadius: _angular_core.InputSignal<number | null>;
128
129
  readonly colorKey: _angular_core.InputSignal<string | undefined>;
129
130
  readonly activeKey: _angular_core.InputSignal<string | undefined>;
130
131
  readonly activeValue: _angular_core.InputSignal<string | number | undefined>;
@@ -135,12 +136,16 @@ declare class BarChart {
135
136
  /** Gap between consecutive cells along the value axis for the `dot` style. */
136
137
  readonly dotGap: _angular_core.InputSignal<number>;
137
138
  /** Corner radius of each `dot` cell. */
138
- readonly dotCornerRadius: _angular_core.InputSignal<number>;
139
+ readonly dotCornerRadius: _angular_core.InputSignal<number | null>;
139
140
  /** Render the faint full-height cell track behind the `dot` values. */
140
141
  readonly showDotTrack: _angular_core.InputSignal<boolean>;
141
142
  readonly barClick: _angular_core.OutputEmitterRef<BarClickEvent>;
142
143
  protected readonly innerWidth: _angular_core.Signal<number>;
143
144
  protected readonly innerHeight: _angular_core.Signal<number>;
145
+ /** Bar corner radius; an unset input follows the live theme-radius axis. */
146
+ protected readonly resolvedCornerRadius: _angular_core.Signal<number>;
147
+ /** Dot-cell corner radius; an unset input follows the theme-radius axis (smaller, SVG clamps to half the cell). */
148
+ protected readonly resolvedDotCornerRadius: _angular_core.Signal<number>;
144
149
  protected readonly layout: _angular_core.Signal<_ojiepermana_angular_chart_bar.BarLayoutResult>;
145
150
  protected readonly bars: _angular_core.Signal<readonly BarRect[]>;
146
151
  protected readonly dotLayout: _angular_core.Signal<BarDotLayoutResult | null>;
@@ -149,6 +154,7 @@ declare class BarChart {
149
154
  protected readonly viewBox: _angular_core.Signal<string>;
150
155
  protected readonly innerTransform: _angular_core.Signal<string>;
151
156
  protected readonly ariaSummary: _angular_core.Signal<string>;
157
+ protected readonly visibleSeriesKeys: _angular_core.Signal<readonly string[]>;
152
158
  constructor();
153
159
  protected emitClick(bar: BarRect): void;
154
160
  protected setActivePoint(event: FocusEvent, bar: BarRect): void;
@@ -163,6 +169,7 @@ declare class BarChart {
163
169
  protected barLabelY(bar: BarRect): number;
164
170
  protected barLabelAnchor(bar: BarRect): 'middle' | 'start' | 'end';
165
171
  protected barAriaLabel(bar: BarRect): string;
172
+ protected onKeydown(event: KeyboardEvent, index: number): void;
166
173
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<BarChart, never>;
167
174
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<BarChart, "ChartBar", never, { "data": { "alias": "data"; "required": true; "isSignal": true; }; "xKey": { "alias": "xKey"; "required": true; "isSignal": true; }; "orientation": { "alias": "orientation"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "styles": { "alias": "styles"; "required": false; "isSignal": true; }; "margin": { "alias": "margin"; "required": false; "isSignal": true; }; "bandPadding": { "alias": "bandPadding"; "required": false; "isSignal": true; }; "groupPadding": { "alias": "groupPadding"; "required": false; "isSignal": true; }; "cornerRadius": { "alias": "cornerRadius"; "required": false; "isSignal": true; }; "colorKey": { "alias": "colorKey"; "required": false; "isSignal": true; }; "activeKey": { "alias": "activeKey"; "required": false; "isSignal": true; }; "activeValue": { "alias": "activeValue"; "required": false; "isSignal": true; }; "showValueLabels": { "alias": "showValueLabels"; "required": false; "isSignal": true; }; "valueLabelFormat": { "alias": "valueLabelFormat"; "required": false; "isSignal": true; }; "dotSize": { "alias": "dotSize"; "required": false; "isSignal": true; }; "dotGap": { "alias": "dotGap"; "required": false; "isSignal": true; }; "dotCornerRadius": { "alias": "dotCornerRadius"; "required": false; "isSignal": true; }; "showDotTrack": { "alias": "showDotTrack"; "required": false; "isSignal": true; }; }, { "barClick": "barClick"; }, never, ["svg\\:g[ChartGrid]", "svg\\:g[ChartAxisX]", "svg\\:g[ChartAxisY]", "svg\\:g[ChartCrosshair]", "*", "ChartTooltip", "ChartLegend"], true, never>;
168
175
  }
@@ -365,5 +365,28 @@ declare function provideCartesianFromLineLayout(ctx: CartesianContext, layout: {
365
365
  categories: readonly string[];
366
366
  }, orientation: 'vertical' | 'horizontal', innerWidth: number, innerHeight: number): void;
367
367
 
368
- export { CHART_DATA_ATTRIBUTE, CHART_THEMES, CartesianContext, CategoricalViewportContext, ChartContainer, ChartContext, ChartStyle, ScatterViewportContext, bandTicks, buildCartesianScales, buildChartCss, cloneLinear, computeAreaLayout, computeLineLayout, effectiveIndexRange, elementClientCenter, indexRangeSize, linearTicks, nearestCategoryIndex, normalizeIndexRange, normalizeNumericDomain, panIndexRange, panNumericDomain, pointToBandAdapter, provideCartesianFromLineLayout, seriesColorVar, sliceByIndexRange, xScale, yScale, zoomIndexRange, zoomNumericDomain };
368
+ /**
369
+ * Resolves the live theme corner radius (the `--radius` token) in pixels for SVG
370
+ * chart geometry, and re-emits when the global `theme-radius` axis switches.
371
+ *
372
+ * SVG `rx`/`ry` need px numbers, so this reads the computed `--radius` off
373
+ * `<html>` and converts rem→px. A `MutationObserver` on the `<html theme-radius>`
374
+ * attribute keeps the `px` signal in sync with `ThemeRadiusService` at runtime;
375
+ * charts default their corner radius to this value (still overridable per chart).
376
+ *
377
+ * SSR-safe: with no document it returns the `md` fallback and skips observing.
378
+ */
379
+ declare class ChartThemeRadius {
380
+ private readonly document;
381
+ private readonly basePx;
382
+ /** Resolved base radius (`--radius`) in px; reactive to the theme-radius axis. */
383
+ readonly px: _angular_core.Signal<number>;
384
+ constructor();
385
+ private readBasePx;
386
+ private toPx;
387
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ChartThemeRadius, never>;
388
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<ChartThemeRadius>;
389
+ }
390
+
391
+ export { CHART_DATA_ATTRIBUTE, CHART_THEMES, CartesianContext, CategoricalViewportContext, ChartContainer, ChartContext, ChartStyle, ChartThemeRadius, ScatterViewportContext, bandTicks, buildCartesianScales, buildChartCss, cloneLinear, computeAreaLayout, computeLineLayout, effectiveIndexRange, elementClientCenter, indexRangeSize, linearTicks, nearestCategoryIndex, normalizeIndexRange, normalizeNumericDomain, panIndexRange, panNumericDomain, pointToBandAdapter, provideCartesianFromLineLayout, seriesColorVar, sliceByIndexRange, xScale, yScale, zoomIndexRange, zoomNumericDomain };
369
392
  export type { AreaLayoutInput, AreaLayoutResult, CartesianBase, CategoryScale, ChartActivePoint, ChartConfig, ChartDatum, ChartDimensions, ChartIndexRange, ChartMargin, ChartOrientation, ChartSeriesConfig, ChartStyleVariant, ChartThemeKey, ChartTick, ClientPoint, LineCurve, LineLayoutResult, LinePoint, LineSeriesPath, NumericDomain, ValueScale };
@@ -59,6 +59,7 @@ interface PieSliceClickEvent {
59
59
  }
60
60
  declare class PieChart {
61
61
  private readonly root;
62
+ private readonly themeRadius;
62
63
  readonly data: _angular_core.InputSignal<readonly Readonly<Record<string, unknown>>[]>;
63
64
  readonly valueKey: _angular_core.InputSignal<string>;
64
65
  readonly nameKey: _angular_core.InputSignal<string>;
@@ -67,13 +68,14 @@ declare class PieChart {
67
68
  readonly margin: _angular_core.InputSignal<ChartMargin>;
68
69
  readonly innerRadius: _angular_core.InputSignal<number>;
69
70
  readonly padAngle: _angular_core.InputSignal<number>;
70
- readonly cornerRadius: _angular_core.InputSignal<number>;
71
+ readonly cornerRadius: _angular_core.InputSignal<number | null>;
71
72
  readonly startAngle: _angular_core.InputSignal<number>;
72
73
  readonly endAngle: _angular_core.InputSignal<number>;
73
74
  readonly showLabels: _angular_core.InputSignal<boolean>;
74
75
  readonly activeIndex: _angular_core.InputSignal<number | undefined>;
75
76
  readonly activeOffset: _angular_core.InputSignal<number>;
76
77
  readonly sliceClick: _angular_core.OutputEmitterRef<PieSliceClickEvent>;
78
+ protected readonly resolvedCornerRadius: _angular_core.Signal<number>;
77
79
  protected readonly innerWidth: _angular_core.Signal<number>;
78
80
  protected readonly innerHeight: _angular_core.Signal<number>;
79
81
  protected readonly layout: _angular_core.Signal<_ojiepermana_angular_chart_pie.PieLayout>;
@@ -49,6 +49,7 @@ interface RadialBarClickEvent {
49
49
  }
50
50
  declare class RadialChart {
51
51
  private readonly root;
52
+ private readonly themeRadius;
52
53
  readonly data: _angular_core.InputSignal<readonly Readonly<Record<string, unknown>>[]>;
53
54
  readonly nameKey: _angular_core.InputSignal<string>;
54
55
  readonly valueKey: _angular_core.InputSignal<string>;
@@ -56,7 +57,7 @@ declare class RadialChart {
56
57
  readonly styles: _angular_core.InputSignal<"base">;
57
58
  readonly margin: _angular_core.InputSignal<ChartMargin>;
58
59
  readonly trackPadding: _angular_core.InputSignal<number>;
59
- readonly cornerRadius: _angular_core.InputSignal<number>;
60
+ readonly cornerRadius: _angular_core.InputSignal<number | null>;
60
61
  readonly startAngle: _angular_core.InputSignal<number>;
61
62
  readonly endAngle: _angular_core.InputSignal<number>;
62
63
  readonly maxValue: _angular_core.InputSignal<number | undefined>;
@@ -64,6 +65,7 @@ declare class RadialChart {
64
65
  readonly showValueLabels: _angular_core.InputSignal<boolean>;
65
66
  readonly valueLabelFormat: _angular_core.InputSignal<RadialValueLabelFormatter>;
66
67
  readonly barClick: _angular_core.OutputEmitterRef<RadialBarClickEvent>;
68
+ protected readonly resolvedCornerRadius: _angular_core.Signal<number>;
67
69
  protected readonly innerWidth: _angular_core.Signal<number>;
68
70
  protected readonly innerHeight: _angular_core.Signal<number>;
69
71
  protected readonly layout: _angular_core.Signal<_ojiepermana_angular_chart_radial.RadialLayout>;