@caribeng/ui 22.1.0 → 22.2.0
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 +52 -12
- package/fesm2022/caribeng-ui-accordion.mjs +2 -2
- package/fesm2022/caribeng-ui-anchor.mjs +169 -0
- package/fesm2022/caribeng-ui-autocomplete.mjs +10 -8
- package/fesm2022/caribeng-ui-avatargroup.mjs +9 -3
- package/fesm2022/caribeng-ui-blockui.mjs +12 -4
- package/fesm2022/caribeng-ui-breadcrumb.mjs +5 -2
- package/fesm2022/caribeng-ui-button.mjs +22 -2
- package/fesm2022/caribeng-ui-caribe-alert.mjs +5 -2
- package/fesm2022/caribeng-ui-caribe-animation-lottie.mjs +6 -3
- package/fesm2022/caribeng-ui-caribe-button-group.mjs +6 -3
- package/fesm2022/caribeng-ui-caribe-calendar.mjs +11 -6
- package/fesm2022/caribeng-ui-caribe-card-content.mjs +6 -3
- package/fesm2022/caribeng-ui-caribe-card-move.mjs +29 -7
- package/fesm2022/caribeng-ui-caribe-card-select.mjs +56 -14
- package/fesm2022/caribeng-ui-caribe-circle-loading.mjs +10 -7
- package/fesm2022/caribeng-ui-caribe-coachmark.mjs +42 -19
- package/fesm2022/caribeng-ui-caribe-data-table.mjs +15 -9
- package/fesm2022/caribeng-ui-caribe-dots-indicator.mjs +38 -5
- package/fesm2022/caribeng-ui-caribe-expansion-panel.mjs +16 -5
- package/fesm2022/caribeng-ui-caribe-fab-menu.mjs +56 -7
- package/fesm2022/caribeng-ui-caribe-file-download.mjs +68 -27
- package/fesm2022/caribeng-ui-caribe-file-select.mjs +22 -17
- package/fesm2022/caribeng-ui-caribe-float-menu.mjs +6 -3
- package/fesm2022/caribeng-ui-caribe-footer.mjs +8 -4
- package/fesm2022/caribeng-ui-caribe-header.mjs +6 -3
- package/fesm2022/caribeng-ui-caribe-icon-button.mjs +6 -3
- package/fesm2022/caribeng-ui-caribe-illustration.mjs +6 -3
- package/fesm2022/caribeng-ui-caribe-inline-alert.mjs +5 -2
- package/fesm2022/caribeng-ui-caribe-input-search.mjs +14 -10
- package/fesm2022/caribeng-ui-caribe-input-select.mjs +12 -10
- package/fesm2022/caribeng-ui-caribe-input-token.mjs +61 -11
- package/fesm2022/caribeng-ui-caribe-link.mjs +7 -2
- package/fesm2022/caribeng-ui-caribe-loader.mjs +8 -5
- package/fesm2022/caribeng-ui-caribe-modal.mjs +32 -16
- package/fesm2022/caribeng-ui-caribe-off-canvas.mjs +26 -12
- package/fesm2022/caribeng-ui-caribe-onboarding.mjs +44 -19
- package/fesm2022/caribeng-ui-caribe-pictogram.mjs +6 -3
- package/fesm2022/caribeng-ui-caribe-pop-over.mjs +34 -10
- package/fesm2022/caribeng-ui-caribe-preloader.mjs +9 -6
- package/fesm2022/caribeng-ui-caribe-progress-bar.mjs +5 -2
- package/fesm2022/caribeng-ui-caribe-shortcut.mjs +8 -3
- package/fesm2022/caribeng-ui-caribe-step-indicator.mjs +12 -5
- package/fesm2022/caribeng-ui-caribe-stroke.mjs +6 -3
- package/fesm2022/caribeng-ui-caribe-switch.mjs +5 -3
- package/fesm2022/caribeng-ui-caribe-tag-button.mjs +1 -1
- package/fesm2022/caribeng-ui-caribe-tracker.mjs +5 -2
- package/fesm2022/caribeng-ui-caribe-transaction-status.mjs +1 -1
- package/fesm2022/caribeng-ui-caribe-voucher.mjs +11 -8
- package/fesm2022/caribeng-ui-carousel.mjs +49 -14
- package/fesm2022/caribeng-ui-cascadeselect.mjs +22 -8
- package/fesm2022/caribeng-ui-chart.mjs +2598 -303
- package/fesm2022/caribeng-ui-chip.mjs +9 -4
- package/fesm2022/caribeng-ui-chips.mjs +9 -5
- package/fesm2022/caribeng-ui-colorpicker.mjs +9 -7
- package/fesm2022/caribeng-ui-confirmdialog.mjs +74 -9
- package/fesm2022/caribeng-ui-confirmpopup.mjs +134 -9
- package/fesm2022/caribeng-ui-contextmenu.mjs +6 -3
- package/fesm2022/caribeng-ui-dataview.mjs +211 -6
- package/fesm2022/caribeng-ui-datepicker.mjs +1630 -393
- package/fesm2022/caribeng-ui-descriptions.mjs +124 -0
- package/fesm2022/caribeng-ui-dialog.mjs +10 -7
- package/fesm2022/caribeng-ui-dock.mjs +6 -3
- package/fesm2022/caribeng-ui-drawer.mjs +10 -7
- package/fesm2022/caribeng-ui-dynamicdialog.mjs +210 -6
- package/fesm2022/caribeng-ui-editor.mjs +4392 -243
- package/fesm2022/caribeng-ui-empty.mjs +80 -0
- package/fesm2022/caribeng-ui-file-card.mjs +2 -2
- package/fesm2022/caribeng-ui-fileupload.mjs +96 -19
- package/fesm2022/caribeng-ui-floatlabel.mjs +4 -2
- package/fesm2022/caribeng-ui-folder-card.mjs +10 -3
- package/fesm2022/caribeng-ui-galleria.mjs +76 -45
- package/fesm2022/caribeng-ui-image.mjs +52 -29
- package/fesm2022/caribeng-ui-inplace.mjs +22 -6
- package/fesm2022/caribeng-ui-input-number.mjs +9 -7
- package/fesm2022/caribeng-ui-inputgroup.mjs +2 -2
- package/fesm2022/caribeng-ui-inputmask.mjs +14 -11
- package/fesm2022/caribeng-ui-inputotp.mjs +12 -6
- package/fesm2022/caribeng-ui-keyfilter.mjs +72 -0
- package/fesm2022/caribeng-ui-listbox.mjs +13 -11
- package/fesm2022/caribeng-ui-megamenu.mjs +222 -30
- package/fesm2022/caribeng-ui-menu.mjs +11 -5
- package/fesm2022/caribeng-ui-menubar.mjs +10 -5
- package/fesm2022/caribeng-ui-message.mjs +7 -4
- package/fesm2022/caribeng-ui-metergroup.mjs +29 -11
- package/fesm2022/caribeng-ui-multiselect.mjs +399 -97
- package/fesm2022/caribeng-ui-orderlist.mjs +230 -11
- package/fesm2022/caribeng-ui-organizationchart.mjs +12 -7
- package/fesm2022/caribeng-ui-paginator.mjs +18 -15
- package/fesm2022/caribeng-ui-panel.mjs +5 -2
- package/fesm2022/caribeng-ui-panelmenu.mjs +6 -3
- package/fesm2022/caribeng-ui-password.mjs +8 -6
- package/fesm2022/caribeng-ui-pdfviewer.mjs +1553 -0
- package/fesm2022/caribeng-ui-picklist.mjs +251 -14
- package/fesm2022/caribeng-ui-pricing.mjs +17 -14
- package/fesm2022/caribeng-ui-rating.mjs +7 -3
- package/fesm2022/caribeng-ui-repeater.mjs +223 -0
- package/fesm2022/caribeng-ui-result.mjs +91 -0
- package/fesm2022/caribeng-ui-scrollpanel.mjs +6 -3
- package/fesm2022/caribeng-ui-scrolltop.mjs +6 -3
- package/fesm2022/caribeng-ui-select.mjs +379 -87
- package/fesm2022/caribeng-ui-selectbutton.mjs +2 -2
- package/fesm2022/caribeng-ui-slider.mjs +171 -75
- package/fesm2022/caribeng-ui-speeddial.mjs +7 -4
- package/fesm2022/caribeng-ui-spinner.mjs +5 -2
- package/fesm2022/caribeng-ui-splitbutton.mjs +163 -0
- package/fesm2022/caribeng-ui-splitter.mjs +5 -2
- package/fesm2022/caribeng-ui-statistic.mjs +183 -0
- package/fesm2022/caribeng-ui-stepper.mjs +8 -5
- package/fesm2022/caribeng-ui-steps.mjs +6 -3
- package/fesm2022/caribeng-ui-table.mjs +708 -82
- package/fesm2022/caribeng-ui-tabmenu.mjs +6 -3
- package/fesm2022/caribeng-ui-tag.mjs +2 -2
- package/fesm2022/caribeng-ui-terminal.mjs +45 -9
- package/fesm2022/caribeng-ui-themetoggle.mjs +82 -0
- package/fesm2022/caribeng-ui-tieredmenu.mjs +9 -6
- package/fesm2022/caribeng-ui-timeline.mjs +175 -6
- package/fesm2022/caribeng-ui-toast.mjs +7 -4
- package/fesm2022/caribeng-ui-togglebutton.mjs +8 -6
- package/fesm2022/caribeng-ui-toolbar.mjs +6 -3
- package/fesm2022/caribeng-ui-tree.mjs +302 -6
- package/fesm2022/caribeng-ui-treeselect.mjs +26 -10
- package/fesm2022/caribeng-ui-treetable.mjs +269 -6
- package/fesm2022/caribeng-ui-virtualscroller.mjs +123 -10
- package/fesm2022/caribeng-ui.mjs +829 -6
- package/package.json +70 -11
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.js +125 -0
- package/schematics/ng-add/schema.js +2 -0
- package/schematics/ng-add/schema.json +66 -0
- package/styles/caribeng.css +380 -0
- package/styles/scss/_mixins.scss +46 -0
- package/styles/scss/_overlay.scss +15 -0
- package/styles/scss/_tokens.scss +267 -0
- package/styles/scss/_utilities.scss +73 -0
- package/styles/scss/_variables.scss +33 -0
- package/styles/scss/index.scss +16 -0
- package/types/caribeng-ui-anchor.d.ts +50 -0
- package/types/caribeng-ui-autocomplete.d.ts +4 -2
- package/types/caribeng-ui-avatargroup.d.ts +3 -1
- package/types/caribeng-ui-blockui.d.ts +9 -4
- package/types/caribeng-ui-breadcrumb.d.ts +3 -0
- package/types/caribeng-ui-button.d.ts +9 -1
- package/types/caribeng-ui-caribe-alert.d.ts +3 -0
- package/types/caribeng-ui-caribe-animation-lottie.d.ts +4 -1
- package/types/caribeng-ui-caribe-button-group.d.ts +4 -1
- package/types/caribeng-ui-caribe-calendar.d.ts +5 -1
- package/types/caribeng-ui-caribe-card-content.d.ts +4 -1
- package/types/caribeng-ui-caribe-card-move.d.ts +13 -13
- package/types/caribeng-ui-caribe-card-select.d.ts +10 -1
- package/types/caribeng-ui-caribe-circle-loading.d.ts +4 -1
- package/types/caribeng-ui-caribe-coachmark.d.ts +6 -0
- package/types/caribeng-ui-caribe-data-table.d.ts +7 -2
- package/types/caribeng-ui-caribe-dots-indicator.d.ts +9 -0
- package/types/caribeng-ui-caribe-expansion-panel.d.ts +7 -10
- package/types/caribeng-ui-caribe-fab-menu.d.ts +10 -1
- package/types/caribeng-ui-caribe-file-download.d.ts +8 -10
- package/types/caribeng-ui-caribe-file-select.d.ts +7 -1
- package/types/caribeng-ui-caribe-float-menu.d.ts +4 -1
- package/types/caribeng-ui-caribe-header.d.ts +3 -0
- package/types/caribeng-ui-caribe-icon-button.d.ts +4 -1
- package/types/caribeng-ui-caribe-illustration.d.ts +4 -1
- package/types/caribeng-ui-caribe-inline-alert.d.ts +3 -0
- package/types/caribeng-ui-caribe-input-search.d.ts +4 -2
- package/types/caribeng-ui-caribe-input-select.d.ts +4 -2
- package/types/caribeng-ui-caribe-input-token.d.ts +16 -1
- package/types/caribeng-ui-caribe-link.d.ts +3 -0
- package/types/caribeng-ui-caribe-loader.d.ts +4 -1
- package/types/caribeng-ui-caribe-modal.d.ts +7 -10
- package/types/caribeng-ui-caribe-off-canvas.d.ts +6 -1
- package/types/caribeng-ui-caribe-onboarding.d.ts +6 -0
- package/types/caribeng-ui-caribe-pictogram.d.ts +4 -1
- package/types/caribeng-ui-caribe-pop-over.d.ts +7 -10
- package/types/caribeng-ui-caribe-preloader.d.ts +4 -1
- package/types/caribeng-ui-caribe-progress-bar.d.ts +3 -0
- package/types/caribeng-ui-caribe-shortcut.d.ts +3 -1
- package/types/caribeng-ui-caribe-step-indicator.d.ts +6 -0
- package/types/caribeng-ui-caribe-stroke.d.ts +4 -1
- package/types/caribeng-ui-caribe-switch.d.ts +3 -1
- package/types/caribeng-ui-caribe-tracker.d.ts +3 -0
- package/types/caribeng-ui-caribe-voucher.d.ts +5 -2
- package/types/caribeng-ui-carousel.d.ts +10 -1
- package/types/caribeng-ui-cascadeselect.d.ts +7 -2
- package/types/caribeng-ui-chart.d.ts +469 -44
- package/types/caribeng-ui-chip.d.ts +6 -0
- package/types/caribeng-ui-chips.d.ts +6 -2
- package/types/caribeng-ui-colorpicker.d.ts +3 -1
- package/types/caribeng-ui-confirmdialog.d.ts +25 -6
- package/types/caribeng-ui-confirmpopup.d.ts +37 -5
- package/types/caribeng-ui-contextmenu.d.ts +4 -1
- package/types/caribeng-ui-dataview.d.ts +63 -5
- package/types/caribeng-ui-datepicker.d.ts +186 -21
- package/types/caribeng-ui-descriptions.d.ts +63 -0
- package/types/caribeng-ui-dialog.d.ts +4 -1
- package/types/caribeng-ui-dock.d.ts +4 -1
- package/types/caribeng-ui-drawer.d.ts +4 -1
- package/types/caribeng-ui-dynamicdialog.d.ts +93 -3
- package/types/caribeng-ui-editor.d.ts +486 -13
- package/types/caribeng-ui-empty.d.ts +34 -0
- package/types/caribeng-ui-file-card.d.ts +1 -1
- package/types/caribeng-ui-fileupload.d.ts +27 -4
- package/types/caribeng-ui-folder-card.d.ts +4 -1
- package/types/caribeng-ui-galleria.d.ts +9 -0
- package/types/caribeng-ui-image.d.ts +10 -0
- package/types/caribeng-ui-inplace.d.ts +8 -0
- package/types/caribeng-ui-input-number.d.ts +3 -1
- package/types/caribeng-ui-inputmask.d.ts +5 -0
- package/types/caribeng-ui-inputotp.d.ts +7 -2
- package/types/caribeng-ui-keyfilter.d.ts +30 -0
- package/types/caribeng-ui-listbox.d.ts +5 -3
- package/types/caribeng-ui-megamenu.d.ts +26 -2
- package/types/caribeng-ui-menu.d.ts +7 -2
- package/types/caribeng-ui-menubar.d.ts +5 -1
- package/types/caribeng-ui-message.d.ts +3 -0
- package/types/caribeng-ui-metergroup.d.ts +9 -1
- package/types/caribeng-ui-multiselect.d.ts +71 -10
- package/types/caribeng-ui-orderlist.d.ts +58 -6
- package/types/caribeng-ui-organizationchart.d.ts +7 -1
- package/types/caribeng-ui-paginator.d.ts +3 -0
- package/types/caribeng-ui-panel.d.ts +3 -0
- package/types/caribeng-ui-panelmenu.d.ts +4 -1
- package/types/caribeng-ui-password.d.ts +4 -2
- package/types/caribeng-ui-pdfviewer.d.ts +258 -0
- package/types/caribeng-ui-picklist.d.ts +67 -7
- package/types/caribeng-ui-pricing.d.ts +8 -5
- package/types/caribeng-ui-rating.d.ts +6 -2
- package/types/caribeng-ui-repeater.d.ts +79 -0
- package/types/caribeng-ui-result.d.ts +35 -0
- package/types/caribeng-ui-scrollpanel.d.ts +4 -1
- package/types/caribeng-ui-scrolltop.d.ts +4 -1
- package/types/caribeng-ui-select.d.ts +73 -9
- package/types/caribeng-ui-slider.d.ts +63 -10
- package/types/caribeng-ui-speeddial.d.ts +5 -2
- package/types/caribeng-ui-spinner.d.ts +3 -0
- package/types/caribeng-ui-splitbutton.d.ts +49 -0
- package/types/caribeng-ui-splitter.d.ts +3 -0
- package/types/caribeng-ui-statistic.d.ts +65 -0
- package/types/caribeng-ui-stepper.d.ts +3 -0
- package/types/caribeng-ui-steps.d.ts +4 -1
- package/types/caribeng-ui-table.d.ts +184 -16
- package/types/caribeng-ui-tabmenu.d.ts +4 -1
- package/types/caribeng-ui-terminal.d.ts +10 -0
- package/types/caribeng-ui-themetoggle.d.ts +32 -0
- package/types/caribeng-ui-tieredmenu.d.ts +5 -2
- package/types/caribeng-ui-timeline.d.ts +63 -4
- package/types/caribeng-ui-toast.d.ts +3 -0
- package/types/caribeng-ui-togglebutton.d.ts +5 -3
- package/types/caribeng-ui-toolbar.d.ts +7 -4
- package/types/caribeng-ui-tree.d.ts +76 -4
- package/types/caribeng-ui-treeselect.d.ts +10 -2
- package/types/caribeng-ui-treetable.d.ts +68 -4
- package/types/caribeng-ui-virtualscroller.d.ts +45 -5
- package/types/caribeng-ui.d.ts +501 -12
|
@@ -1,367 +1,2662 @@
|
|
|
1
|
+
import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
|
|
1
2
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
+
import { signal, Injectable, input, ChangeDetectionStrategy, Component, Directive, forwardRef, booleanAttribute, numberAttribute, output, contentChildren, viewChild, inject, ElementRef, computed, DestroyRef, afterNextRender, effect, untracked, ViewEncapsulation } from '@angular/core';
|
|
4
|
+
import { injectCbTranslation } from '@caribeng/ui';
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'var(--cb-color-
|
|
8
|
-
'var(--cb-color-
|
|
9
|
-
'var(--cb-color-
|
|
6
|
+
// Tipos públicos del gráfico y modelo interno de la escena que dibuja cada renderer.
|
|
7
|
+
// --- Modelo interno -----------------------------------------------------------------
|
|
8
|
+
const PALETTE = [
|
|
9
|
+
'var(--cb-color-primary-500)',
|
|
10
|
+
'var(--cb-color-warning-500)',
|
|
11
|
+
'var(--cb-color-success-500)',
|
|
12
|
+
'var(--cb-color-danger-500)',
|
|
13
|
+
'var(--cb-color-info-500)',
|
|
14
|
+
'var(--cb-color-help-500)',
|
|
10
15
|
];
|
|
16
|
+
const paletteColor = (i) => PALETTE[i % PALETTE.length];
|
|
17
|
+
/** Ancho lógico del lienzo; el SVG escala al ancho del contenedor. */
|
|
18
|
+
const W = 520;
|
|
19
|
+
const emptyScene = () => ({ shapes: [], hits: [], items: [], legend: [], table: { head: null, rows: [] } });
|
|
20
|
+
|
|
21
|
+
/** Estado compartido por las gráficas de un `<cb-chart-group>`. */
|
|
22
|
+
class CbChartGroupState {
|
|
23
|
+
constructor() {
|
|
24
|
+
/** Valor de X activo (cursor) en cualquiera de las gráficas. */
|
|
25
|
+
this.key = signal(null, /* @ts-ignore */
|
|
26
|
+
...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
|
|
27
|
+
/** Ventana visible común del eje X (zoom y desplazamiento). */
|
|
28
|
+
this.window = signal(null, /* @ts-ignore */
|
|
29
|
+
...(ngDevMode ? [{ debugName: "window" }] : /* istanbul ignore next */ []));
|
|
30
|
+
}
|
|
31
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartGroupState, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
32
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartGroupState }); }
|
|
33
|
+
}
|
|
34
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartGroupState, decorators: [{
|
|
35
|
+
type: Injectable
|
|
36
|
+
}] });
|
|
37
|
+
/**
|
|
38
|
+
* Agrupa gráficas del modo composable que comparten el eje X: el cursor, el
|
|
39
|
+
* tooltip y el zoom se sincronizan (p. ej. precio, volumen y spread).
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```html
|
|
43
|
+
* <cb-chart-group ariaLabel="PRIM/USD">
|
|
44
|
+
* <cb-chart [data]="velas" zoom [height]="260"><cb-chart-candlestick x="ts" /></cb-chart>
|
|
45
|
+
* <cb-chart [data]="velas" zoom [height]="100"><cb-chart-bar x="ts" y="volumen" /></cb-chart>
|
|
46
|
+
* </cb-chart-group>
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
class CbChartGroup {
|
|
50
|
+
constructor() {
|
|
51
|
+
/** Nombre accesible del conjunto. */
|
|
52
|
+
this.ariaLabel = input(undefined, /* @ts-ignore */
|
|
53
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
54
|
+
}
|
|
55
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartGroup, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
56
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.5", type: CbChartGroup, isStandalone: true, selector: "cb-chart-group", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "group" }, properties: { "attr.aria-label": "ariaLabel() || null" }, classAttribute: "cb-chart-group" }, providers: [CbChartGroupState], ngImport: i0, template: `<ng-content />`, isInline: true, styles: [":host{display:grid;gap:var(--cb-space-2, .5rem);width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
57
|
+
}
|
|
58
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartGroup, decorators: [{
|
|
59
|
+
type: Component,
|
|
60
|
+
args: [{ selector: 'cb-chart-group', changeDetection: ChangeDetectionStrategy.OnPush, providers: [CbChartGroupState], host: {
|
|
61
|
+
class: 'cb-chart-group',
|
|
62
|
+
role: 'group',
|
|
63
|
+
'[attr.aria-label]': 'ariaLabel() || null',
|
|
64
|
+
}, template: `<ng-content />`, styles: [":host{display:grid;gap:var(--cb-space-2, .5rem);width:100%}\n"] }]
|
|
65
|
+
}], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Base de las series del modo composable (`<cb-chart>` con hijos). Cada serie lee
|
|
69
|
+
* de `data` (la del gráfico o la suya) los campos que indique.
|
|
70
|
+
*/
|
|
71
|
+
class CbChartSeries {
|
|
72
|
+
constructor() {
|
|
73
|
+
/** Nombre en la leyenda, el tooltip y la tabla accesible. */
|
|
74
|
+
this.name = input('', /* @ts-ignore */
|
|
75
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
76
|
+
/** Campo con el valor del eje X (categoría, número o fecha). */
|
|
77
|
+
this.x = input('x', /* @ts-ignore */
|
|
78
|
+
...(ngDevMode ? [{ debugName: "x" }] : /* istanbul ignore next */ []));
|
|
79
|
+
this.color = input(undefined, /* @ts-ignore */
|
|
80
|
+
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
81
|
+
/** Eje Y de la serie. */
|
|
82
|
+
this.axis = input('left', /* @ts-ignore */
|
|
83
|
+
...(ngDevMode ? [{ debugName: "axis" }] : /* istanbul ignore next */ []));
|
|
84
|
+
/** Datos propios; si se omite, usa los del gráfico. */
|
|
85
|
+
this.data = input(undefined, /* @ts-ignore */
|
|
86
|
+
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
87
|
+
}
|
|
88
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartSeries, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
89
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartSeries, isStandalone: true, inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, x: { classPropertyName: "x", publicName: "x", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, axis: { classPropertyName: "axis", publicName: "axis", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
|
|
90
|
+
}
|
|
91
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartSeries, decorators: [{
|
|
92
|
+
type: Directive
|
|
93
|
+
}], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], x: [{ type: i0.Input, args: [{ isSignal: true, alias: "x", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], axis: [{ type: i0.Input, args: [{ isSignal: true, alias: "axis", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }] } });
|
|
94
|
+
/** Barras. Las series con el mismo `stack` se apilan. */
|
|
95
|
+
class CbChartBar extends CbChartSeries {
|
|
96
|
+
constructor() {
|
|
97
|
+
super(...arguments);
|
|
98
|
+
this.kind = 'bar';
|
|
99
|
+
/** Campo del valor. */
|
|
100
|
+
this.y = input('y', /* @ts-ignore */
|
|
101
|
+
...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
|
|
102
|
+
/** Grupo de apilado: las barras con el mismo valor se apilan. */
|
|
103
|
+
this.stack = input(undefined, /* @ts-ignore */
|
|
104
|
+
...(ngDevMode ? [{ debugName: "stack" }] : /* istanbul ignore next */ []));
|
|
105
|
+
}
|
|
106
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartBar, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
|
|
107
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartBar, isStandalone: true, selector: "cb-chart-bar", inputs: { y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: false, transformFunction: null }, stack: { classPropertyName: "stack", publicName: "stack", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartBar) }], usesInheritance: true, ngImport: i0 }); }
|
|
108
|
+
}
|
|
109
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartBar, decorators: [{
|
|
110
|
+
type: Directive,
|
|
111
|
+
args: [{
|
|
112
|
+
selector: 'cb-chart-bar',
|
|
113
|
+
providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartBar) }],
|
|
114
|
+
}]
|
|
115
|
+
}], propDecorators: { y: [{ type: i0.Input, args: [{ isSignal: true, alias: "y", required: false }] }], stack: [{ type: i0.Input, args: [{ isSignal: true, alias: "stack", required: false }] }] } });
|
|
116
|
+
/** Línea. */
|
|
117
|
+
class CbChartLine extends CbChartSeries {
|
|
118
|
+
constructor() {
|
|
119
|
+
super(...arguments);
|
|
120
|
+
this.kind = 'line';
|
|
121
|
+
this.y = input('y', /* @ts-ignore */
|
|
122
|
+
...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
|
|
123
|
+
this.curve = input('linear', /* @ts-ignore */
|
|
124
|
+
...(ngDevMode ? [{ debugName: "curve" }] : /* istanbul ignore next */ []));
|
|
125
|
+
/** Trazo discontinuo (p. ej. para una proyección o una media). */
|
|
126
|
+
this.dashed = input(false, { ...(ngDevMode ? { debugName: "dashed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
127
|
+
/** Dibuja un punto en cada dato. */
|
|
128
|
+
this.points = input(true, { ...(ngDevMode ? { debugName: "points" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
129
|
+
}
|
|
130
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartLine, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
|
|
131
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartLine, isStandalone: true, selector: "cb-chart-line", inputs: { y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: false, transformFunction: null }, curve: { classPropertyName: "curve", publicName: "curve", isSignal: true, isRequired: false, transformFunction: null }, dashed: { classPropertyName: "dashed", publicName: "dashed", isSignal: true, isRequired: false, transformFunction: null }, points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartLine) }], usesInheritance: true, ngImport: i0 }); }
|
|
132
|
+
}
|
|
133
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartLine, decorators: [{
|
|
134
|
+
type: Directive,
|
|
135
|
+
args: [{
|
|
136
|
+
selector: 'cb-chart-line',
|
|
137
|
+
providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartLine) }],
|
|
138
|
+
}]
|
|
139
|
+
}], propDecorators: { y: [{ type: i0.Input, args: [{ isSignal: true, alias: "y", required: false }] }], curve: [{ type: i0.Input, args: [{ isSignal: true, alias: "curve", required: false }] }], dashed: [{ type: i0.Input, args: [{ isSignal: true, alias: "dashed", required: false }] }], points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: false }] }] } });
|
|
140
|
+
/** Área: una línea con relleno hasta el 0 (o hasta la serie de debajo si se apila). */
|
|
141
|
+
class CbChartArea extends CbChartSeries {
|
|
142
|
+
constructor() {
|
|
143
|
+
super(...arguments);
|
|
144
|
+
this.kind = 'area';
|
|
145
|
+
this.y = input('y', /* @ts-ignore */
|
|
146
|
+
...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
|
|
147
|
+
this.curve = input('linear', /* @ts-ignore */
|
|
148
|
+
...(ngDevMode ? [{ debugName: "curve" }] : /* istanbul ignore next */ []));
|
|
149
|
+
/** Grupo de apilado: las áreas con el mismo valor se apilan. */
|
|
150
|
+
this.stack = input(undefined, /* @ts-ignore */
|
|
151
|
+
...(ngDevMode ? [{ debugName: "stack" }] : /* istanbul ignore next */ []));
|
|
152
|
+
}
|
|
153
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartArea, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
|
|
154
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartArea, isStandalone: true, selector: "cb-chart-area", inputs: { y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: false, transformFunction: null }, curve: { classPropertyName: "curve", publicName: "curve", isSignal: true, isRequired: false, transformFunction: null }, stack: { classPropertyName: "stack", publicName: "stack", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartArea) }], usesInheritance: true, ngImport: i0 }); }
|
|
155
|
+
}
|
|
156
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartArea, decorators: [{
|
|
157
|
+
type: Directive,
|
|
158
|
+
args: [{
|
|
159
|
+
selector: 'cb-chart-area',
|
|
160
|
+
providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartArea) }],
|
|
161
|
+
}]
|
|
162
|
+
}], propDecorators: { y: [{ type: i0.Input, args: [{ isSignal: true, alias: "y", required: false }] }], curve: [{ type: i0.Input, args: [{ isSignal: true, alias: "curve", required: false }] }], stack: [{ type: i0.Input, args: [{ isSignal: true, alias: "stack", required: false }] }] } });
|
|
163
|
+
/** Dispersión: un punto por registro. */
|
|
164
|
+
class CbChartScatter extends CbChartSeries {
|
|
165
|
+
constructor() {
|
|
166
|
+
super(...arguments);
|
|
167
|
+
this.kind = 'scatter';
|
|
168
|
+
this.y = input('y', /* @ts-ignore */
|
|
169
|
+
...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
|
|
170
|
+
/** Radio de los puntos. */
|
|
171
|
+
this.size = input(5, { ...(ngDevMode ? { debugName: "size" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
172
|
+
}
|
|
173
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartScatter, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
|
|
174
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartScatter, isStandalone: true, selector: "cb-chart-scatter", inputs: { y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartScatter) }], usesInheritance: true, ngImport: i0 }); }
|
|
175
|
+
}
|
|
176
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartScatter, decorators: [{
|
|
177
|
+
type: Directive,
|
|
178
|
+
args: [{
|
|
179
|
+
selector: 'cb-chart-scatter',
|
|
180
|
+
providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartScatter) }],
|
|
181
|
+
}]
|
|
182
|
+
}], propDecorators: { y: [{ type: i0.Input, args: [{ isSignal: true, alias: "y", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
183
|
+
/** Banda entre un mínimo y un máximo (rango, intervalo de confianza…). */
|
|
184
|
+
class CbChartRangeArea extends CbChartSeries {
|
|
185
|
+
constructor() {
|
|
186
|
+
super(...arguments);
|
|
187
|
+
this.kind = 'rangeArea';
|
|
188
|
+
this.low = input('low', /* @ts-ignore */
|
|
189
|
+
...(ngDevMode ? [{ debugName: "low" }] : /* istanbul ignore next */ []));
|
|
190
|
+
this.high = input('high', /* @ts-ignore */
|
|
191
|
+
...(ngDevMode ? [{ debugName: "high" }] : /* istanbul ignore next */ []));
|
|
192
|
+
this.curve = input('linear', /* @ts-ignore */
|
|
193
|
+
...(ngDevMode ? [{ debugName: "curve" }] : /* istanbul ignore next */ []));
|
|
194
|
+
}
|
|
195
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartRangeArea, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
|
|
196
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartRangeArea, isStandalone: true, selector: "cb-chart-range-area", inputs: { low: { classPropertyName: "low", publicName: "low", isSignal: true, isRequired: false, transformFunction: null }, high: { classPropertyName: "high", publicName: "high", isSignal: true, isRequired: false, transformFunction: null }, curve: { classPropertyName: "curve", publicName: "curve", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartRangeArea) }], usesInheritance: true, ngImport: i0 }); }
|
|
197
|
+
}
|
|
198
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartRangeArea, decorators: [{
|
|
199
|
+
type: Directive,
|
|
200
|
+
args: [{
|
|
201
|
+
selector: 'cb-chart-range-area',
|
|
202
|
+
providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartRangeArea) }],
|
|
203
|
+
}]
|
|
204
|
+
}], propDecorators: { low: [{ type: i0.Input, args: [{ isSignal: true, alias: "low", required: false }] }], high: [{ type: i0.Input, args: [{ isSignal: true, alias: "high", required: false }] }], curve: [{ type: i0.Input, args: [{ isSignal: true, alias: "curve", required: false }] }] } });
|
|
205
|
+
/** Velas japonesas u OHLC (apertura, máximo, mínimo y cierre). */
|
|
206
|
+
class CbChartCandlestick extends CbChartSeries {
|
|
207
|
+
constructor() {
|
|
208
|
+
super(...arguments);
|
|
209
|
+
this.kind = 'candlestick';
|
|
210
|
+
this.open = input('open', /* @ts-ignore */
|
|
211
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
212
|
+
this.high = input('high', /* @ts-ignore */
|
|
213
|
+
...(ngDevMode ? [{ debugName: "high" }] : /* istanbul ignore next */ []));
|
|
214
|
+
this.low = input('low', /* @ts-ignore */
|
|
215
|
+
...(ngDevMode ? [{ debugName: "low" }] : /* istanbul ignore next */ []));
|
|
216
|
+
this.close = input('close', /* @ts-ignore */
|
|
217
|
+
...(ngDevMode ? [{ debugName: "close" }] : /* istanbul ignore next */ []));
|
|
218
|
+
/** `candle`: cuerpo relleno; `ohlc`: barra con marcas de apertura y cierre. */
|
|
219
|
+
this.variant = input('candle', /* @ts-ignore */
|
|
220
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
221
|
+
/** Color cuando cierra por encima de la apertura. */
|
|
222
|
+
this.upColor = input('var(--cb-color-success-500)', /* @ts-ignore */
|
|
223
|
+
...(ngDevMode ? [{ debugName: "upColor" }] : /* istanbul ignore next */ []));
|
|
224
|
+
/** Color cuando cierra por debajo. */
|
|
225
|
+
this.downColor = input('var(--cb-color-danger-500)', /* @ts-ignore */
|
|
226
|
+
...(ngDevMode ? [{ debugName: "downColor" }] : /* istanbul ignore next */ []));
|
|
227
|
+
}
|
|
228
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartCandlestick, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
|
|
229
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartCandlestick, isStandalone: true, selector: "cb-chart-candlestick", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, high: { classPropertyName: "high", publicName: "high", isSignal: true, isRequired: false, transformFunction: null }, low: { classPropertyName: "low", publicName: "low", isSignal: true, isRequired: false, transformFunction: null }, close: { classPropertyName: "close", publicName: "close", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, upColor: { classPropertyName: "upColor", publicName: "upColor", isSignal: true, isRequired: false, transformFunction: null }, downColor: { classPropertyName: "downColor", publicName: "downColor", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartCandlestick) }], usesInheritance: true, ngImport: i0 }); }
|
|
230
|
+
}
|
|
231
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartCandlestick, decorators: [{
|
|
232
|
+
type: Directive,
|
|
233
|
+
args: [{
|
|
234
|
+
selector: 'cb-chart-candlestick',
|
|
235
|
+
providers: [{ provide: CbChartSeries, useExisting: forwardRef(() => CbChartCandlestick) }],
|
|
236
|
+
}]
|
|
237
|
+
}], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], high: [{ type: i0.Input, args: [{ isSignal: true, alias: "high", required: false }] }], low: [{ type: i0.Input, args: [{ isSignal: true, alias: "low", required: false }] }], close: [{ type: i0.Input, args: [{ isSignal: true, alias: "close", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], upColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "upColor", required: false }] }], downColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "downColor", required: false }] }] } });
|
|
238
|
+
/** Configuración de un eje: posición, tipo, rango, formato y título. */
|
|
239
|
+
class CbChartAxis {
|
|
240
|
+
constructor() {
|
|
241
|
+
/** `bottom` es el eje X; `left` y `right`, los ejes Y. */
|
|
242
|
+
this.position = input('left', /* @ts-ignore */
|
|
243
|
+
...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
244
|
+
/** Solo para el eje X. */
|
|
245
|
+
this.type = input('auto', /* @ts-ignore */
|
|
246
|
+
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
247
|
+
this.min = input(undefined, /* @ts-ignore */
|
|
248
|
+
...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
249
|
+
this.max = input(undefined, /* @ts-ignore */
|
|
250
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
251
|
+
/** Título del eje. */
|
|
252
|
+
this.label = input(undefined, /* @ts-ignore */
|
|
253
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
254
|
+
/** Formato de las marcas (por defecto, el `valueFormat` del gráfico en los ejes Y). */
|
|
255
|
+
this.format = input(undefined, /* @ts-ignore */
|
|
256
|
+
...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
|
|
257
|
+
/** Líneas de rejilla en las marcas (por defecto, solo el eje Y principal). */
|
|
258
|
+
this.grid = input(undefined, /* @ts-ignore */
|
|
259
|
+
...(ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []));
|
|
260
|
+
}
|
|
261
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartAxis, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
262
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartAxis, isStandalone: true, selector: "cb-chart-axis", inputs: { position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
|
|
263
|
+
}
|
|
264
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartAxis, decorators: [{
|
|
265
|
+
type: Directive,
|
|
266
|
+
args: [{ selector: 'cb-chart-axis' }]
|
|
267
|
+
}], propDecorators: { position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", required: false }] }] } });
|
|
268
|
+
/** Línea de referencia (meta, umbral, media…) en un valor de un eje. */
|
|
269
|
+
class CbChartReferenceLine {
|
|
270
|
+
constructor() {
|
|
271
|
+
this.value = input.required(/* @ts-ignore */
|
|
272
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
273
|
+
/** `left` / `right`: línea horizontal en ese eje Y; `x`: línea vertical. */
|
|
274
|
+
this.axis = input('left', /* @ts-ignore */
|
|
275
|
+
...(ngDevMode ? [{ debugName: "axis" }] : /* istanbul ignore next */ []));
|
|
276
|
+
this.label = input(undefined, /* @ts-ignore */
|
|
277
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
278
|
+
this.color = input(undefined, /* @ts-ignore */
|
|
279
|
+
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
280
|
+
}
|
|
281
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartReferenceLine, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
282
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartReferenceLine, isStandalone: true, selector: "cb-chart-reference-line", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, axis: { classPropertyName: "axis", publicName: "axis", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
|
|
283
|
+
}
|
|
284
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartReferenceLine, decorators: [{
|
|
285
|
+
type: Directive,
|
|
286
|
+
args: [{ selector: 'cb-chart-reference-line' }]
|
|
287
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], axis: [{ type: i0.Input, args: [{ isSignal: true, alias: "axis", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }] } });
|
|
288
|
+
/** Franja de referencia entre dos valores de un eje (rango objetivo, horario…). */
|
|
289
|
+
class CbChartReferenceBand {
|
|
290
|
+
constructor() {
|
|
291
|
+
this.from = input.required(/* @ts-ignore */
|
|
292
|
+
...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
|
|
293
|
+
this.to = input.required(/* @ts-ignore */
|
|
294
|
+
...(ngDevMode ? [{ debugName: "to" }] : /* istanbul ignore next */ []));
|
|
295
|
+
this.axis = input('left', /* @ts-ignore */
|
|
296
|
+
...(ngDevMode ? [{ debugName: "axis" }] : /* istanbul ignore next */ []));
|
|
297
|
+
this.label = input(undefined, /* @ts-ignore */
|
|
298
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
299
|
+
this.color = input(undefined, /* @ts-ignore */
|
|
300
|
+
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
301
|
+
}
|
|
302
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartReferenceBand, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
303
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartReferenceBand, isStandalone: true, selector: "cb-chart-reference-band", inputs: { from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: true, transformFunction: null }, to: { classPropertyName: "to", publicName: "to", isSignal: true, isRequired: true, transformFunction: null }, axis: { classPropertyName: "axis", publicName: "axis", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
|
|
304
|
+
}
|
|
305
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartReferenceBand, decorators: [{
|
|
306
|
+
type: Directive,
|
|
307
|
+
args: [{ selector: 'cb-chart-reference-band' }]
|
|
308
|
+
}], propDecorators: { from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: true }] }], to: [{ type: i0.Input, args: [{ isSignal: true, alias: "to", required: true }] }], axis: [{ type: i0.Input, args: [{ isSignal: true, alias: "axis", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }] } });
|
|
309
|
+
/** Anotación: marca un punto (x, y) con una etiqueta, p. ej. "Máximo de la sesión". */
|
|
310
|
+
class CbChartAnnotation {
|
|
311
|
+
constructor() {
|
|
312
|
+
/** Valor de X (número, fecha o categoría). */
|
|
313
|
+
this.x = input.required(/* @ts-ignore */
|
|
314
|
+
...(ngDevMode ? [{ debugName: "x" }] : /* istanbul ignore next */ []));
|
|
315
|
+
/** Valor en el eje Y. */
|
|
316
|
+
this.y = input.required(/* @ts-ignore */
|
|
317
|
+
...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
|
|
318
|
+
this.axis = input('left', /* @ts-ignore */
|
|
319
|
+
...(ngDevMode ? [{ debugName: "axis" }] : /* istanbul ignore next */ []));
|
|
320
|
+
this.label = input.required(/* @ts-ignore */
|
|
321
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
322
|
+
this.color = input(undefined, /* @ts-ignore */
|
|
323
|
+
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
324
|
+
}
|
|
325
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartAnnotation, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
326
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: CbChartAnnotation, isStandalone: true, selector: "cb-chart-annotation", inputs: { x: { classPropertyName: "x", publicName: "x", isSignal: true, isRequired: true, transformFunction: null }, y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: true, transformFunction: null }, axis: { classPropertyName: "axis", publicName: "axis", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
|
|
327
|
+
}
|
|
328
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChartAnnotation, decorators: [{
|
|
329
|
+
type: Directive,
|
|
330
|
+
args: [{ selector: 'cb-chart-annotation' }]
|
|
331
|
+
}], propDecorators: { x: [{ type: i0.Input, args: [{ isSignal: true, alias: "x", required: true }] }], y: [{ type: i0.Input, args: [{ isSignal: true, alias: "y", required: true }] }], axis: [{ type: i0.Input, args: [{ isSignal: true, alias: "axis", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }] } });
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Calcula una escala legible que contiene `[min, max]`, con unas `count`
|
|
335
|
+
* divisiones de tamaño 1, 2, 2.5 o 5 × 10ⁿ. Siempre incluye el 0.
|
|
336
|
+
*/
|
|
337
|
+
function niceScale(min, max, count = 4) {
|
|
338
|
+
let lo = Math.min(0, min);
|
|
339
|
+
let hi = Math.max(0, max);
|
|
340
|
+
if (lo === hi)
|
|
341
|
+
hi = lo + 1;
|
|
342
|
+
const rough = (hi - lo) / Math.max(1, count);
|
|
343
|
+
const magnitude = 10 ** Math.floor(Math.log10(rough));
|
|
344
|
+
const residual = rough / magnitude;
|
|
345
|
+
const nice = residual > 5 ? 10 : residual > 2.5 ? 5 : residual > 2 ? 2.5 : residual > 1 ? 2 : 1;
|
|
346
|
+
const step = nice * magnitude;
|
|
347
|
+
lo = Math.floor(lo / step) * step;
|
|
348
|
+
hi = Math.ceil(hi / step) * step;
|
|
349
|
+
const ticks = [];
|
|
350
|
+
for (let v = lo; v <= hi + step / 2; v += step)
|
|
351
|
+
ticks.push(round(v));
|
|
352
|
+
return { min: round(lo), max: round(hi), step, ticks };
|
|
353
|
+
}
|
|
354
|
+
/** Elimina el ruido de coma flotante (0.30000000000000004 → 0.3). */
|
|
355
|
+
function round(value) {
|
|
356
|
+
return Math.round(value * 1e9) / 1e9;
|
|
357
|
+
}
|
|
358
|
+
/** Punto de una circunferencia; el ángulo 0 está arriba y crece en sentido horario. */
|
|
359
|
+
function polar(cx, cy, r, angle) {
|
|
360
|
+
const rad = ((angle - 90) * Math.PI) / 180;
|
|
361
|
+
return [round(cx + r * Math.cos(rad)), round(cy + r * Math.sin(rad))];
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Trazado SVG de un sector (torta, `inner = 0`) o de un anillo (dona) entre dos
|
|
365
|
+
* ángulos en grados. Un sector completo (360°) se dibuja como dos mitades.
|
|
366
|
+
*/
|
|
367
|
+
function arcPath(cx, cy, outer, inner, start, end) {
|
|
368
|
+
const sweep = Math.min(359.999, end - start);
|
|
369
|
+
if (sweep <= 0)
|
|
370
|
+
return '';
|
|
371
|
+
const stop = start + sweep;
|
|
372
|
+
const large = sweep > 180 ? 1 : 0;
|
|
373
|
+
const [x1, y1] = polar(cx, cy, outer, start);
|
|
374
|
+
const [x2, y2] = polar(cx, cy, outer, stop);
|
|
375
|
+
if (inner <= 0) {
|
|
376
|
+
return `M ${cx} ${cy} L ${x1} ${y1} A ${outer} ${outer} 0 ${large} 1 ${x2} ${y2} Z`;
|
|
377
|
+
}
|
|
378
|
+
const [x3, y3] = polar(cx, cy, inner, stop);
|
|
379
|
+
const [x4, y4] = polar(cx, cy, inner, start);
|
|
380
|
+
return `M ${x1} ${y1} A ${outer} ${outer} 0 ${large} 1 ${x2} ${y2} L ${x3} ${y3} A ${inner} ${inner} 0 ${large} 0 ${x4} ${y4} Z`;
|
|
381
|
+
}
|
|
382
|
+
/** Trazado de una polilínea (`closed` la cierra, para polígonos). */
|
|
383
|
+
function polyline(points, closed = false) {
|
|
384
|
+
if (!points.length)
|
|
385
|
+
return '';
|
|
386
|
+
return points.map((p, i) => `${i ? 'L' : 'M'} ${p.x} ${p.y}`).join(' ') + (closed ? ' Z' : '');
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Reparte un rectángulo en celdas de área proporcional a cada valor, lo más
|
|
390
|
+
* cuadradas posible (algoritmo "squarified" de Bruls, Huizing y van Wijk).
|
|
391
|
+
* Devuelve las celdas en el mismo orden que `values`; los valores ≤ 0 quedan vacíos.
|
|
392
|
+
*/
|
|
393
|
+
function squarify(values, rect) {
|
|
394
|
+
const result = values.map(() => null);
|
|
395
|
+
const total = values.reduce((sum, v) => sum + Math.max(0, v), 0);
|
|
396
|
+
if (total <= 0 || rect.w <= 0 || rect.h <= 0)
|
|
397
|
+
return result;
|
|
398
|
+
const scale = (rect.w * rect.h) / total;
|
|
399
|
+
const order = values
|
|
400
|
+
.map((v, i) => ({ i, area: Math.max(0, v) * scale }))
|
|
401
|
+
.filter((n) => n.area > 0)
|
|
402
|
+
.sort((a, b) => b.area - a.area);
|
|
403
|
+
let free = { ...rect };
|
|
404
|
+
let row = [];
|
|
405
|
+
const worst = (r, side) => {
|
|
406
|
+
const sum = r.reduce((s, n) => s + n.area, 0);
|
|
407
|
+
const max = Math.max(...r.map((n) => n.area));
|
|
408
|
+
const min = Math.min(...r.map((n) => n.area));
|
|
409
|
+
return Math.max((side * side * max) / (sum * sum), (sum * sum) / (side * side * min));
|
|
410
|
+
};
|
|
411
|
+
const layout = (r) => {
|
|
412
|
+
const sum = r.reduce((s, n) => s + n.area, 0);
|
|
413
|
+
const horizontal = free.w >= free.h;
|
|
414
|
+
const thickness = sum / (horizontal ? free.h : free.w);
|
|
415
|
+
let offset = 0;
|
|
416
|
+
for (const n of r) {
|
|
417
|
+
const length = n.area / thickness;
|
|
418
|
+
result[n.i] = horizontal
|
|
419
|
+
? { x: round(free.x), y: round(free.y + offset), w: round(thickness), h: round(length) }
|
|
420
|
+
: { x: round(free.x + offset), y: round(free.y), w: round(length), h: round(thickness) };
|
|
421
|
+
offset += length;
|
|
422
|
+
}
|
|
423
|
+
free = horizontal
|
|
424
|
+
? { x: free.x + thickness, y: free.y, w: free.w - thickness, h: free.h }
|
|
425
|
+
: { x: free.x, y: free.y + thickness, w: free.w, h: free.h - thickness };
|
|
426
|
+
};
|
|
427
|
+
for (const node of order) {
|
|
428
|
+
const side = Math.min(free.w, free.h);
|
|
429
|
+
if (!row.length || worst([...row, node], side) <= worst(row, side)) {
|
|
430
|
+
row.push(node);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
layout(row);
|
|
434
|
+
row = [node];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (row.length)
|
|
438
|
+
layout(row);
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Marco de un gráfico cartesiano: área de dibujo, posiciones, rejilla, ejes y
|
|
444
|
+
* franjas por categoría que reciben el ratón.
|
|
445
|
+
*/
|
|
446
|
+
function frame(ctx, scale, horizontal) {
|
|
447
|
+
const pad = ctx.sparkline
|
|
448
|
+
? { t: 6, r: 6, b: 6, l: 6 }
|
|
449
|
+
: horizontal
|
|
450
|
+
? { t: 12, r: 20, b: 28, l: 96 }
|
|
451
|
+
: { t: 16, r: 16, b: 28, l: 60 };
|
|
452
|
+
const h = ctx.height;
|
|
453
|
+
const plot = {
|
|
454
|
+
left: pad.l,
|
|
455
|
+
right: ctx.width - pad.r,
|
|
456
|
+
top: pad.t,
|
|
457
|
+
bottom: h - pad.b,
|
|
458
|
+
width: ctx.width - pad.l - pad.r,
|
|
459
|
+
height: h - pad.t - pad.b,
|
|
460
|
+
};
|
|
461
|
+
const labels = ctx.data.labels;
|
|
462
|
+
const band = (horizontal ? plot.height : plot.width) / Math.max(1, labels.length);
|
|
463
|
+
const valuePos = (v) => {
|
|
464
|
+
const ratio = (v - scale.min) / (scale.max - scale.min || 1);
|
|
465
|
+
return round(horizontal ? plot.left + ratio * plot.width : plot.bottom - ratio * plot.height);
|
|
466
|
+
};
|
|
467
|
+
const bandCenter = (i) => round((horizontal ? plot.top : plot.left) + band * (i + 0.5));
|
|
468
|
+
const shapes = [];
|
|
469
|
+
if (!ctx.sparkline) {
|
|
470
|
+
for (const value of scale.ticks) {
|
|
471
|
+
const pos = valuePos(value);
|
|
472
|
+
const cls = value === 0 ? 'cb-chart__grid cb-chart__grid--zero' : 'cb-chart__grid';
|
|
473
|
+
const text = ctx.format(value);
|
|
474
|
+
if (horizontal) {
|
|
475
|
+
shapes.push({ k: 'line', cls, x1: pos, y1: plot.top, x2: pos, y2: plot.bottom });
|
|
476
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x: pos, y: h - 8, text, anchor: 'middle' });
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
shapes.push({ k: 'line', cls, x1: plot.left, y1: pos, x2: plot.right, y2: pos });
|
|
480
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x: plot.left - 8, y: pos, text, anchor: 'end', baseline: 'central' });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Con muchas categorías se muestran solo algunas etiquetas para que no se pisen.
|
|
484
|
+
const every = horizontal ? 1 : Math.ceil(labels.length / 10);
|
|
485
|
+
labels.forEach((text, index) => {
|
|
486
|
+
if (index % every)
|
|
487
|
+
return;
|
|
488
|
+
const pos = bandCenter(index);
|
|
489
|
+
shapes.push(horizontal
|
|
490
|
+
? { k: 'text', cls: 'cb-chart__axis', x: plot.left - 8, y: pos, text, anchor: 'end', baseline: 'central' }
|
|
491
|
+
: { k: 'text', cls: 'cb-chart__axis', x: pos, y: h - 8, text, anchor: 'middle' });
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const hits = labels.map((_, index) => horizontal
|
|
495
|
+
? { k: 'rect', item: index, x: plot.left, y: round(plot.top + band * index), w: plot.width, h: round(band) }
|
|
496
|
+
: { k: 'rect', item: index, x: round(plot.left + band * index), y: plot.top, w: round(band), h: plot.height });
|
|
497
|
+
return { plot, band, valuePos, bandCenter, shapes, hits };
|
|
498
|
+
}
|
|
499
|
+
/** Barras (verticales u horizontales), líneas, áreas y combinados. */
|
|
500
|
+
function renderCartesian(ctx) {
|
|
501
|
+
const { type, data, stacked } = ctx;
|
|
502
|
+
const horizontal = type === 'horizontalBar';
|
|
503
|
+
const n = data.labels.length;
|
|
504
|
+
const series = data.datasets
|
|
505
|
+
.map((ds, index) => ({
|
|
506
|
+
index,
|
|
507
|
+
label: ds.label,
|
|
508
|
+
data: ds.data,
|
|
509
|
+
color: ds.color ?? paletteColor(index),
|
|
510
|
+
asLine: type === 'line' || type === 'area' || (type === 'bar' && ds.type === 'line'),
|
|
511
|
+
}))
|
|
512
|
+
.filter((s) => !ctx.hidden.has(s.index));
|
|
513
|
+
const barSeries = type === 'bar' || type === 'horizontalBar' ? series.filter((s) => !s.asLine) : [];
|
|
514
|
+
const lineSeries = series.filter((s) => s.asLine);
|
|
515
|
+
// Series que se apilan (barras, o áreas en `area`).
|
|
516
|
+
const stackSeries = !stacked ? [] : type === 'area' ? lineSeries : barSeries;
|
|
517
|
+
const values = [];
|
|
518
|
+
const stacking = new Set(stackSeries);
|
|
519
|
+
for (const s of series)
|
|
520
|
+
if (!stacking.has(s))
|
|
521
|
+
values.push(...s.data.slice(0, n));
|
|
522
|
+
for (let i = 0; i < n; i++) {
|
|
523
|
+
let pos = 0;
|
|
524
|
+
let neg = 0;
|
|
525
|
+
for (const s of stacking) {
|
|
526
|
+
const v = s.data[i] ?? 0;
|
|
527
|
+
if (v >= 0)
|
|
528
|
+
pos += v;
|
|
529
|
+
else
|
|
530
|
+
neg += v;
|
|
531
|
+
}
|
|
532
|
+
values.push(pos, neg);
|
|
533
|
+
}
|
|
534
|
+
const scale = niceScale(Math.min(0, ...values), Math.max(0, ...values), ctx.sparkline ? 2 : 4);
|
|
535
|
+
const f = frame(ctx, scale, horizontal);
|
|
536
|
+
const { plot, band, valuePos, bandCenter } = f;
|
|
537
|
+
// --- Barras ---
|
|
538
|
+
const bars = [];
|
|
539
|
+
if (barSeries.length) {
|
|
540
|
+
const groupSize = stacked ? Math.min(48, band * 0.6) : Math.min(band * 0.75, 36 * barSeries.length);
|
|
541
|
+
const slot = stacked ? groupSize : groupSize / barSeries.length;
|
|
542
|
+
const zero = valuePos(0);
|
|
543
|
+
data.labels.forEach((_, i) => {
|
|
544
|
+
const start = bandCenter(i) - groupSize / 2;
|
|
545
|
+
let pos = 0;
|
|
546
|
+
let neg = 0;
|
|
547
|
+
barSeries.forEach((s, si) => {
|
|
548
|
+
const v = s.data[i] ?? 0;
|
|
549
|
+
let from = 0;
|
|
550
|
+
if (stacked) {
|
|
551
|
+
from = v >= 0 ? pos : neg;
|
|
552
|
+
if (v >= 0)
|
|
553
|
+
pos += v;
|
|
554
|
+
else
|
|
555
|
+
neg += v;
|
|
556
|
+
}
|
|
557
|
+
const a = stacked ? valuePos(from) : zero;
|
|
558
|
+
const b = valuePos(from + v);
|
|
559
|
+
const offset = round(start + (stacked ? 0 : slot * si) + 1);
|
|
560
|
+
const thickness = round(Math.max(2, slot - 2));
|
|
561
|
+
const lo = Math.min(a, b);
|
|
562
|
+
const length = round(Math.max(1, Math.abs(b - a)));
|
|
563
|
+
bars.push(horizontal
|
|
564
|
+
? { category: i, x: lo, y: offset, w: length, h: thickness, color: s.color, horizontal }
|
|
565
|
+
: { category: i, x: offset, y: lo, w: thickness, h: length, color: s.color, horizontal });
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
// --- Líneas y áreas (acumuladas si se apilan) ---
|
|
570
|
+
const stackLines = type === 'area' && stacked;
|
|
571
|
+
const totals = data.labels.map(() => 0);
|
|
572
|
+
const linePoints = lineSeries.map((s) => {
|
|
573
|
+
const base = [...totals];
|
|
574
|
+
const points = data.labels.map((_, i) => {
|
|
575
|
+
const v = (s.data[i] ?? 0) + (stackLines ? totals[i] : 0);
|
|
576
|
+
if (stackLines)
|
|
577
|
+
totals[i] = v;
|
|
578
|
+
return { x: bandCenter(i), y: valuePos(v) };
|
|
579
|
+
});
|
|
580
|
+
const floor = base.map((b, i) => ({ x: bandCenter(i), y: valuePos(stackLines ? b : 0) }));
|
|
581
|
+
return { series: s, points, floor };
|
|
582
|
+
});
|
|
583
|
+
const shapes = [...f.shapes];
|
|
584
|
+
if (type === 'area') {
|
|
585
|
+
for (const { series: s, points, floor } of linePoints) {
|
|
586
|
+
const d = [polyline(points), ...[...floor].reverse().map((p) => `L ${p.x} ${p.y}`), 'Z'].join(' ');
|
|
587
|
+
shapes.push({ k: 'path', cls: 'cb-chart__area', d, fill: s.color });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
for (const bar of bars) {
|
|
591
|
+
shapes.push({
|
|
592
|
+
k: 'rect',
|
|
593
|
+
cls: bar.horizontal ? 'cb-chart__bar cb-chart__bar--horizontal' : 'cb-chart__bar',
|
|
594
|
+
item: bar.category,
|
|
595
|
+
x: bar.x,
|
|
596
|
+
y: bar.y,
|
|
597
|
+
w: bar.w,
|
|
598
|
+
h: bar.h,
|
|
599
|
+
rx: ctx.sparkline ? 1 : 3,
|
|
600
|
+
fill: bar.color,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
// La guía vertical se dibuja aparte (va debajo de las líneas): ver ChartItem.cursor.
|
|
604
|
+
const lineShapes = [];
|
|
605
|
+
for (const { series: s, points } of linePoints) {
|
|
606
|
+
lineShapes.push({ k: 'path', cls: 'cb-chart__line', d: polyline(points), stroke: s.color, pathLength: 1 });
|
|
607
|
+
points.forEach((pt, i) => lineShapes.push({ k: 'circle', cls: 'cb-chart__point', item: i, cx: pt.x, cy: pt.y, r: ctx.sparkline ? 0 : 3, rActive: 5, fill: s.color }));
|
|
608
|
+
}
|
|
609
|
+
// --- Elementos navegables: una categoría con el valor de cada serie ---
|
|
610
|
+
const items = data.labels.map((label, i) => {
|
|
611
|
+
const rows = series.map((s) => ({ label: s.label, value: ctx.format(s.data[i] ?? 0), color: s.color }));
|
|
612
|
+
const tops = [
|
|
613
|
+
...bars.filter((b) => b.category === i).map((b) => (horizontal ? b.x + b.w : b.y)),
|
|
614
|
+
...linePoints.map((l) => (horizontal ? 0 : (l.points[i]?.y ?? plot.top))),
|
|
615
|
+
];
|
|
616
|
+
return horizontal
|
|
617
|
+
? { index: i, label, title: label, rows, x: Math.max(plot.left, ...tops), y: bandCenter(i) }
|
|
618
|
+
: {
|
|
619
|
+
index: i,
|
|
620
|
+
label,
|
|
621
|
+
title: label,
|
|
622
|
+
rows,
|
|
623
|
+
x: bandCenter(i),
|
|
624
|
+
y: Math.min(plot.bottom, ...tops),
|
|
625
|
+
cursor: { x: bandCenter(i), y1: plot.top, y2: plot.bottom },
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
return {
|
|
629
|
+
shapes,
|
|
630
|
+
front: lineShapes,
|
|
631
|
+
hits: f.hits,
|
|
632
|
+
items,
|
|
633
|
+
legend: data.datasets.map((ds, index) => ({ index, label: ds.label, color: ds.color ?? paletteColor(index) })),
|
|
634
|
+
table: seriesTable(ctx),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/** Cascada: variaciones que suben o bajan desde el acumulado anterior, con subtotales. */
|
|
638
|
+
function renderWaterfall(ctx) {
|
|
639
|
+
const ds = ctx.data.datasets[0];
|
|
640
|
+
const labels = ctx.data.labels;
|
|
641
|
+
const sums = new Set(ds?.sums ?? []);
|
|
642
|
+
const up = 'var(--cb-color-success-500)';
|
|
643
|
+
const down = 'var(--cb-color-danger-500)';
|
|
644
|
+
const total = ds?.color ?? 'var(--cb-color-primary-500)';
|
|
645
|
+
// Tramos [desde, hasta] de cada barra.
|
|
646
|
+
let running = 0;
|
|
647
|
+
const steps = labels.map((label, i) => {
|
|
648
|
+
if (sums.has(i))
|
|
649
|
+
return { label, from: 0, to: running, delta: running, sum: true };
|
|
650
|
+
const delta = ds?.data[i] ?? 0;
|
|
651
|
+
const from = running;
|
|
652
|
+
running += delta;
|
|
653
|
+
return { label, from, to: running, delta, sum: false };
|
|
654
|
+
});
|
|
655
|
+
const values = steps.flatMap((s) => [s.from, s.to]);
|
|
656
|
+
const scale = niceScale(Math.min(0, ...values), Math.max(0, ...values), ctx.sparkline ? 2 : 4);
|
|
657
|
+
const f = frame(ctx, scale, false);
|
|
658
|
+
const { band, valuePos, bandCenter, plot } = f;
|
|
659
|
+
const width = Math.min(48, band * 0.6);
|
|
660
|
+
const shapes = [...f.shapes];
|
|
661
|
+
const items = [];
|
|
662
|
+
steps.forEach((s, i) => {
|
|
663
|
+
const a = valuePos(s.from);
|
|
664
|
+
const b = valuePos(s.to);
|
|
665
|
+
const color = ds?.colors?.[i] ?? (s.sum ? total : s.delta >= 0 ? up : down);
|
|
666
|
+
const x = round(bandCenter(i) - width / 2);
|
|
667
|
+
const y = Math.min(a, b);
|
|
668
|
+
shapes.push({ k: 'rect', cls: 'cb-chart__bar', item: i, x, y, w: round(width), h: round(Math.max(1, Math.abs(b - a))), rx: 3, fill: color });
|
|
669
|
+
// Conector con la barra siguiente, a la altura del acumulado.
|
|
670
|
+
if (i < steps.length - 1 && !ctx.sparkline) {
|
|
671
|
+
shapes.push({ k: 'line', cls: 'cb-chart__connector', x1: round(x + width), y1: b, x2: round(bandCenter(i + 1) - width / 2), y2: b });
|
|
672
|
+
}
|
|
673
|
+
const rows = s.sum
|
|
674
|
+
? [{ label: ctx.text.total, value: ctx.format(s.to), color }]
|
|
675
|
+
: [
|
|
676
|
+
{ label: ctx.text.change, value: (s.delta > 0 ? '+' : '') + ctx.format(s.delta), color },
|
|
677
|
+
{ label: ctx.text.total, value: ctx.format(s.to), color: total },
|
|
678
|
+
];
|
|
679
|
+
items.push({ index: i, label: s.label, title: s.label, rows, x: bandCenter(i), y: y, cursor: { x: bandCenter(i), y1: plot.top, y2: plot.bottom } });
|
|
680
|
+
});
|
|
681
|
+
return {
|
|
682
|
+
shapes,
|
|
683
|
+
hits: f.hits,
|
|
684
|
+
items,
|
|
685
|
+
legend: [],
|
|
686
|
+
table: {
|
|
687
|
+
head: [ctx.text.change, ctx.text.total],
|
|
688
|
+
rows: steps.map((s) => ({ label: s.label, cells: [s.sum ? '' : ctx.format(s.delta), ctx.format(s.to)] })),
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/** Tabla accesible: una fila por categoría y una columna por serie. */
|
|
693
|
+
function seriesTable(ctx) {
|
|
694
|
+
const { labels, datasets } = ctx.data;
|
|
695
|
+
return {
|
|
696
|
+
head: datasets.map((ds) => ds.label),
|
|
697
|
+
rows: labels.map((label, i) => ({ label, cells: datasets.map((ds) => ctx.format(ds.data.at(i) ?? 0)) })),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** Torta, dona y barras radiales: un elemento por categoría del primer dataset. */
|
|
702
|
+
function renderCircular(ctx) {
|
|
703
|
+
const { labels, datasets } = ctx.data;
|
|
704
|
+
const ds = datasets[0];
|
|
705
|
+
const colorOf = (i) => ds?.colors?.[i] ?? paletteColor(i);
|
|
706
|
+
const legend = labels.map((label, index) => ({ index, label, color: colorOf(index) }));
|
|
707
|
+
const table = { head: null, rows: labels.map((label, i) => ({ label, cells: [ctx.format(ds?.data.at(i) ?? 0)] })) };
|
|
708
|
+
if (!ds)
|
|
709
|
+
return { shapes: [], hits: [], items: [], legend, table };
|
|
710
|
+
const cx = ctx.width / 2;
|
|
711
|
+
const cy = ctx.height / 2;
|
|
712
|
+
const outer = Math.max(20, Math.min(cy - 10, 110));
|
|
713
|
+
const visible = labels
|
|
714
|
+
.map((label, index) => ({ index, label, value: Math.max(0, ds.data[index] ?? 0), color: colorOf(index) }))
|
|
715
|
+
.filter((item) => !ctx.hidden.has(item.index));
|
|
716
|
+
const shapes = [];
|
|
717
|
+
const items = [];
|
|
718
|
+
const add = (item, percent, path, tipAngle, tipRadius) => {
|
|
719
|
+
const pos = items.length;
|
|
720
|
+
shapes.push({ k: 'path', cls: 'cb-chart__slice', item: pos, d: path, fill: item.color });
|
|
721
|
+
const [x, y] = polar(cx, cy, tipRadius, tipAngle);
|
|
722
|
+
const value = `${ctx.format(item.value)} (${ctx.percent(percent)})`;
|
|
723
|
+
items.push({ index: item.index, label: item.label, title: item.label, rows: [{ label: '', value, color: item.color }], x, y });
|
|
724
|
+
};
|
|
725
|
+
if (ctx.type === 'radialBar') {
|
|
726
|
+
const gap = 4;
|
|
727
|
+
const thickness = Math.max(6, Math.min(16, (outer * 0.6) / Math.max(1, visible.length) - gap));
|
|
728
|
+
const max = ctx.max || 1;
|
|
729
|
+
// Las pistas van primero, debajo de todos los anillos.
|
|
730
|
+
visible.forEach((_, i) => {
|
|
731
|
+
const r = outer - i * (thickness + gap);
|
|
732
|
+
shapes.push({ k: 'path', cls: 'cb-chart__track', d: arcPath(cx, cy, r, r - thickness, 0, 360) });
|
|
733
|
+
});
|
|
734
|
+
visible.forEach((item, i) => {
|
|
735
|
+
const r = outer - i * (thickness + gap);
|
|
736
|
+
const percent = Math.min(1, item.value / max);
|
|
737
|
+
add(item, percent, arcPath(cx, cy, r, r - thickness, 0, 360 * percent), (360 * percent) / 2, r - thickness / 2);
|
|
738
|
+
});
|
|
739
|
+
const center = visible.length === 1 ? { x: cx, y: cy, text: ctx.percent(Math.min(1, visible[0].value / max)) } : undefined;
|
|
740
|
+
return { shapes, hits: [], items, legend, table, center };
|
|
741
|
+
}
|
|
742
|
+
const total = visible.reduce((sum, item) => sum + item.value, 0) || 1;
|
|
743
|
+
const inner = ctx.type === 'doughnut' ? outer * 0.6 : 0;
|
|
744
|
+
let angle = 0;
|
|
745
|
+
for (const item of visible) {
|
|
746
|
+
const sweep = (item.value / total) * 360;
|
|
747
|
+
const start = angle;
|
|
748
|
+
angle += sweep;
|
|
749
|
+
add(item, item.value / total, arcPath(cx, cy, outer, inner, start, start + sweep), start + sweep / 2, (outer + inner) / 2 || outer * 0.6);
|
|
750
|
+
}
|
|
751
|
+
const center = ctx.type === 'doughnut' ? { x: cx, y: cy, text: ctx.format(visible.reduce((sum, item) => sum + item.value, 0)) } : undefined;
|
|
752
|
+
return { shapes, hits: [], items, legend, table, center };
|
|
753
|
+
}
|
|
754
|
+
/** Medidor semicircular: el primer valor del primer dataset entre `min` y `max`, con franjas opcionales. */
|
|
755
|
+
function renderGauge(ctx) {
|
|
756
|
+
const ds = ctx.data.datasets[0];
|
|
757
|
+
const label = ctx.data.labels[0] ?? ds?.label ?? '';
|
|
758
|
+
const value = ds?.data[0] ?? 0;
|
|
759
|
+
const min = ctx.min;
|
|
760
|
+
const max = ctx.max > min ? ctx.max : min + 1;
|
|
761
|
+
const ratio = Math.min(1, Math.max(0, (value - min) / (max - min)));
|
|
762
|
+
const color = ds?.color ?? ds?.colors?.[0] ?? bandColor(ctx, value) ?? paletteColor(0);
|
|
763
|
+
const cx = ctx.width / 2;
|
|
764
|
+
const outer = Math.max(30, Math.min(ctx.width / 2 - 24, ctx.height - 44));
|
|
765
|
+
const cy = round(Math.min(ctx.height - 24, 16 + outer + (ctx.height - 40 - outer) / 2));
|
|
766
|
+
const thickness = Math.max(10, outer * 0.22);
|
|
767
|
+
const inner = outer - thickness;
|
|
768
|
+
const angleOf = (v) => -90 + 180 * Math.min(1, Math.max(0, (v - min) / (max - min)));
|
|
769
|
+
const shapes = [{ k: 'path', cls: 'cb-chart__track', d: arcPath(cx, cy, outer, inner, -90, 90) }];
|
|
770
|
+
// Franjas: un aro fino por fuera del medidor.
|
|
771
|
+
for (const band of ctx.bands) {
|
|
772
|
+
const d = arcPath(cx, cy, outer + 8, outer + 3, angleOf(band.from), angleOf(band.to));
|
|
773
|
+
if (d)
|
|
774
|
+
shapes.push({ k: 'path', cls: 'cb-chart__band', d, fill: band.color });
|
|
775
|
+
}
|
|
776
|
+
shapes.push({ k: 'path', cls: 'cb-chart__slice', item: 0, d: arcPath(cx, cy, outer, inner, -90, -90 + 180 * ratio), fill: color });
|
|
777
|
+
if (!ctx.sparkline) {
|
|
778
|
+
const [x0, y0] = polar(cx, cy, inner + thickness / 2, -90);
|
|
779
|
+
const [x1, y1] = polar(cx, cy, inner + thickness / 2, 90);
|
|
780
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x: x0, y: y0 + 16, text: ctx.format(min), anchor: 'middle' });
|
|
781
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x: x1, y: y1 + 16, text: ctx.format(max), anchor: 'middle' });
|
|
782
|
+
}
|
|
783
|
+
const [tx, ty] = polar(cx, cy, outer, angleOf(value));
|
|
784
|
+
return {
|
|
785
|
+
shapes,
|
|
786
|
+
hits: [],
|
|
787
|
+
items: [{ index: 0, label, title: label, rows: [{ label: '', value: `${ctx.format(value)} (${ctx.percent(ratio)})`, color }], x: tx, y: ty }],
|
|
788
|
+
legend: [],
|
|
789
|
+
table: { head: null, rows: [{ label, cells: [ctx.format(value)] }] },
|
|
790
|
+
center: { x: cx, y: round(cy - inner * 0.28), text: ctx.format(value), sub: label },
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
/** Color de la franja que contiene el valor (para colorear el medidor según umbrales). */
|
|
794
|
+
function bandColor(ctx, value) {
|
|
795
|
+
return ctx.bands.find((b) => value >= Math.min(b.from, b.to) && value <= Math.max(b.from, b.to))?.color;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** Intensidad (0–1) → color entre la superficie y el color base. */
|
|
799
|
+
function heat(base, t) {
|
|
800
|
+
return `color-mix(in srgb, ${base} ${Math.round(12 + 88 * t)}%, var(--cb-color-surface-raised))`;
|
|
801
|
+
}
|
|
802
|
+
/** Mapa de calor: columnas = `labels`, filas = datasets; el color indica el valor. */
|
|
803
|
+
function renderHeatmap(ctx) {
|
|
804
|
+
const { labels, datasets } = ctx.data;
|
|
805
|
+
const rows = datasets;
|
|
806
|
+
const cols = labels.length;
|
|
807
|
+
const values = rows.flatMap((ds) => ds.data.slice(0, cols));
|
|
808
|
+
const min = Math.min(...values);
|
|
809
|
+
const max = Math.max(...values);
|
|
810
|
+
const span = max - min || 1;
|
|
811
|
+
const base = rows[0]?.color ?? 'var(--cb-color-primary-600)';
|
|
812
|
+
const pad = ctx.sparkline ? { t: 2, r: 2, b: 2, l: 2 } : { t: 8, r: 8, b: 44, l: 96 };
|
|
813
|
+
const plot = { left: pad.l, top: pad.t, width: ctx.width - pad.l - pad.r, height: ctx.height - pad.t - pad.b };
|
|
814
|
+
const cw = plot.width / Math.max(1, cols);
|
|
815
|
+
const ch = plot.height / Math.max(1, rows.length);
|
|
816
|
+
const gap = cw > 8 && ch > 8 ? 2 : 0;
|
|
817
|
+
const shapes = [];
|
|
818
|
+
const hits = [];
|
|
819
|
+
const items = [];
|
|
820
|
+
rows.forEach((ds, r) => {
|
|
821
|
+
if (!ctx.sparkline) {
|
|
822
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x: plot.left - 8, y: round(plot.top + ch * (r + 0.5)), text: ds.label, anchor: 'end', baseline: 'central' });
|
|
823
|
+
}
|
|
824
|
+
labels.forEach((col, c) => {
|
|
825
|
+
const v = ds.data[c] ?? 0;
|
|
826
|
+
const t = (v - min) / span;
|
|
827
|
+
const x = round(plot.left + cw * c + gap / 2);
|
|
828
|
+
const y = round(plot.top + ch * r + gap / 2);
|
|
829
|
+
const w = round(Math.max(1, cw - gap));
|
|
830
|
+
const h = round(Math.max(1, ch - gap));
|
|
831
|
+
const item = items.length;
|
|
832
|
+
const fill = ds.colors?.[c] ?? heat(base, t);
|
|
833
|
+
shapes.push({ k: 'rect', cls: 'cb-chart__cell', item, x, y, w, h, rx: 2, fill });
|
|
834
|
+
if (!ctx.sparkline && w >= 34 && h >= 18) {
|
|
835
|
+
shapes.push({
|
|
836
|
+
k: 'text',
|
|
837
|
+
cls: t > 0.55 ? 'cb-chart__label cb-chart__label--inverse' : 'cb-chart__label',
|
|
838
|
+
x: round(x + w / 2),
|
|
839
|
+
y: round(y + h / 2),
|
|
840
|
+
text: ctx.format(v),
|
|
841
|
+
anchor: 'middle',
|
|
842
|
+
baseline: 'central',
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
hits.push({ k: 'rect', item, x, y, w, h });
|
|
846
|
+
items.push({
|
|
847
|
+
index: item,
|
|
848
|
+
label: `${ds.label} · ${col}`,
|
|
849
|
+
title: `${ds.label} · ${col}`,
|
|
850
|
+
rows: [{ label: '', value: ctx.format(v), color: fill }],
|
|
851
|
+
x: round(x + w / 2),
|
|
852
|
+
y,
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
if (!ctx.sparkline) {
|
|
857
|
+
const every = Math.ceil(cols / 12);
|
|
858
|
+
labels.forEach((text, c) => {
|
|
859
|
+
if (c % every)
|
|
860
|
+
return;
|
|
861
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x: round(plot.left + cw * (c + 0.5)), y: plot.top + plot.height + 16, text, anchor: 'middle' });
|
|
862
|
+
});
|
|
863
|
+
// Escala de color: mínimo, degradado en pasos y máximo.
|
|
864
|
+
const y = ctx.height - 12;
|
|
865
|
+
const steps = 8;
|
|
866
|
+
const sw = 18;
|
|
867
|
+
const x0 = round(ctx.width - pad.r - steps * sw - 40);
|
|
868
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x: x0 - 6, y, text: ctx.format(min), anchor: 'end', baseline: 'central' });
|
|
869
|
+
for (let i = 0; i < steps; i++) {
|
|
870
|
+
shapes.push({ k: 'rect', cls: 'cb-chart__scale', x: x0 + i * sw, y: y - 5, w: sw, h: 10, fill: heat(base, i / (steps - 1)) });
|
|
871
|
+
}
|
|
872
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x: x0 + steps * sw + 6, y, text: ctx.format(max), anchor: 'start', baseline: 'central' });
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
shapes,
|
|
876
|
+
hits,
|
|
877
|
+
items,
|
|
878
|
+
columns: cols,
|
|
879
|
+
legend: [],
|
|
880
|
+
table: { head: labels, rows: rows.map((ds) => ({ label: ds.label, cells: labels.map((_, c) => ctx.format(ds.data[c] ?? 0)) })) },
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
/** Treemap: rectángulos de área proporcional a cada valor del primer dataset. */
|
|
884
|
+
function renderTreemap(ctx) {
|
|
885
|
+
const { labels, datasets } = ctx.data;
|
|
886
|
+
const ds = datasets[0];
|
|
887
|
+
const colorOf = (i) => ds?.colors?.[i] ?? paletteColor(i);
|
|
888
|
+
const legend = labels.map((label, index) => ({ index, label, color: colorOf(index) }));
|
|
889
|
+
const table = { head: null, rows: labels.map((label, i) => ({ label, cells: [ctx.format(ds?.data.at(i) ?? 0)] })) };
|
|
890
|
+
if (!ds)
|
|
891
|
+
return { shapes: [], hits: [], items: [], legend, table };
|
|
892
|
+
const values = labels.map((_, i) => (ctx.hidden.has(i) ? 0 : Math.max(0, ds.data[i] ?? 0)));
|
|
893
|
+
const total = values.reduce((sum, v) => sum + v, 0) || 1;
|
|
894
|
+
const pad = ctx.sparkline ? 1 : 4;
|
|
895
|
+
const cells = squarify(values, { x: pad, y: pad, w: ctx.width - pad * 2, h: ctx.height - pad * 2 });
|
|
896
|
+
// Orden de lectura: de mayor a menor.
|
|
897
|
+
const order = labels.map((_, i) => i).filter((i) => cells[i]).sort((a, b) => values[b] - values[a]);
|
|
898
|
+
const shapes = [];
|
|
899
|
+
const hits = [];
|
|
900
|
+
const items = [];
|
|
901
|
+
for (const i of order) {
|
|
902
|
+
const cell = cells[i];
|
|
903
|
+
const item = items.length;
|
|
904
|
+
const color = colorOf(i);
|
|
905
|
+
shapes.push({ k: 'rect', cls: 'cb-chart__cell cb-chart__cell--tree', item, x: cell.x, y: cell.y, w: cell.w, h: cell.h, rx: 3, fill: color });
|
|
906
|
+
if (!ctx.sparkline && cell.w >= 56 && cell.h >= 34) {
|
|
907
|
+
shapes.push({ k: 'text', cls: 'cb-chart__label cb-chart__label--inverse cb-chart__label--strong', x: cell.x + 8, y: cell.y + 16, text: labels[i], anchor: 'start' });
|
|
908
|
+
shapes.push({ k: 'text', cls: 'cb-chart__label cb-chart__label--inverse', x: cell.x + 8, y: cell.y + 32, text: ctx.format(values[i]), anchor: 'start' });
|
|
909
|
+
}
|
|
910
|
+
hits.push({ k: 'rect', item, x: cell.x, y: cell.y, w: cell.w, h: cell.h });
|
|
911
|
+
items.push({
|
|
912
|
+
index: i,
|
|
913
|
+
label: labels[i],
|
|
914
|
+
title: labels[i],
|
|
915
|
+
rows: [{ label: '', value: `${ctx.format(values[i])} (${ctx.percent(values[i] / total)})`, color }],
|
|
916
|
+
x: round(cell.x + cell.w / 2),
|
|
917
|
+
y: cell.y,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
return { shapes, hits, items, legend, table };
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Radar: un eje por categoría y un polígono por serie. */
|
|
924
|
+
function renderRadar(ctx) {
|
|
925
|
+
const { labels, datasets } = ctx.data;
|
|
926
|
+
const n = labels.length;
|
|
927
|
+
const series = datasets
|
|
928
|
+
.map((ds, index) => ({ index, label: ds.label, data: ds.data, color: ds.color ?? paletteColor(index) }))
|
|
929
|
+
.filter((s) => !ctx.hidden.has(s.index));
|
|
930
|
+
const values = series.flatMap((s) => s.data.slice(0, n));
|
|
931
|
+
const scale = niceScale(Math.min(0, ...values), Math.max(0, ...values), ctx.sparkline ? 2 : 4);
|
|
932
|
+
const cx = ctx.width / 2;
|
|
933
|
+
const cy = ctx.height / 2;
|
|
934
|
+
const radius = Math.max(20, Math.min(cy - (ctx.sparkline ? 6 : 24), cx - (ctx.sparkline ? 6 : 90)));
|
|
935
|
+
const step = 360 / Math.max(1, n);
|
|
936
|
+
const r = (v) => (radius * (v - scale.min)) / (scale.max - scale.min || 1);
|
|
937
|
+
const point = (v, i) => {
|
|
938
|
+
const [x, y] = polar(cx, cy, r(v), i * step);
|
|
939
|
+
return { x, y };
|
|
940
|
+
};
|
|
941
|
+
const shapes = [];
|
|
942
|
+
if (!ctx.sparkline) {
|
|
943
|
+
// Anillos poligonales de la escala y radios de cada eje.
|
|
944
|
+
for (const tick of scale.ticks) {
|
|
945
|
+
if (tick === scale.min)
|
|
946
|
+
continue;
|
|
947
|
+
shapes.push({ k: 'path', cls: 'cb-chart__grid', d: polyline(labels.map((_, i) => point(tick, i)), true), fill: 'none' });
|
|
948
|
+
}
|
|
949
|
+
labels.forEach((label, i) => {
|
|
950
|
+
const end = point(scale.max, i);
|
|
951
|
+
shapes.push({ k: 'line', cls: 'cb-chart__grid', x1: cx, y1: cy, x2: end.x, y2: end.y });
|
|
952
|
+
const [lx, ly] = polar(cx, cy, radius + 12, i * step);
|
|
953
|
+
const angle = (i * step) % 360;
|
|
954
|
+
const anchor = angle === 0 || angle === 180 ? 'middle' : angle < 180 ? 'start' : 'end';
|
|
955
|
+
const baseline = angle === 0 ? 'auto' : angle === 180 ? 'hanging' : 'central';
|
|
956
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x: lx, y: ly, text: label, anchor, baseline });
|
|
957
|
+
});
|
|
958
|
+
for (const tick of scale.ticks) {
|
|
959
|
+
if (tick === scale.min)
|
|
960
|
+
continue;
|
|
961
|
+
const p = point(tick, 0);
|
|
962
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis cb-chart__axis--tick', x: p.x + 4, y: p.y, text: ctx.format(tick), anchor: 'start', baseline: 'central' });
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const front = [];
|
|
966
|
+
for (const s of series) {
|
|
967
|
+
const pts = labels.map((_, i) => point(s.data[i] ?? 0, i));
|
|
968
|
+
shapes.push({ k: 'path', cls: 'cb-chart__area', d: polyline(pts, true), fill: s.color });
|
|
969
|
+
front.push({ k: 'path', cls: 'cb-chart__line', d: polyline(pts, true), stroke: s.color, pathLength: 1 });
|
|
970
|
+
pts.forEach((p, i) => front.push({ k: 'circle', cls: 'cb-chart__point', item: i, cx: p.x, cy: p.y, r: ctx.sparkline ? 0 : 3, rActive: 5, fill: s.color }));
|
|
971
|
+
}
|
|
972
|
+
// Cada eje es un elemento: su sector recibe el ratón.
|
|
973
|
+
const hits = labels.map((_, i) => ({ k: 'path', item: i, d: arcPath(cx, cy, radius + 8, 0, i * step - step / 2, i * step + step / 2) }));
|
|
974
|
+
const items = labels.map((label, i) => {
|
|
975
|
+
const peak = Math.max(scale.min, ...series.map((s) => s.data[i] ?? 0));
|
|
976
|
+
const p = point(peak, i);
|
|
977
|
+
return {
|
|
978
|
+
index: i,
|
|
979
|
+
label,
|
|
980
|
+
title: label,
|
|
981
|
+
rows: series.map((s) => ({ label: s.label, value: ctx.format(s.data[i] ?? 0), color: s.color })),
|
|
982
|
+
x: p.x,
|
|
983
|
+
y: p.y,
|
|
984
|
+
};
|
|
985
|
+
});
|
|
986
|
+
return {
|
|
987
|
+
shapes,
|
|
988
|
+
front,
|
|
989
|
+
hits,
|
|
990
|
+
items,
|
|
991
|
+
legend: datasets.map((ds, index) => ({ index, label: ds.label, color: ds.color ?? paletteColor(index) })),
|
|
992
|
+
table: seriesTable(ctx),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
/** Área polar: sectores de igual ángulo cuyo radio depende del valor. */
|
|
996
|
+
function renderPolarArea(ctx) {
|
|
997
|
+
const { labels, datasets } = ctx.data;
|
|
998
|
+
const ds = datasets[0];
|
|
999
|
+
const colorOf = (i) => ds?.colors?.[i] ?? paletteColor(i);
|
|
1000
|
+
const visible = labels
|
|
1001
|
+
.map((label, index) => ({ index, label, value: Math.max(0, ds?.data[index] ?? 0), color: colorOf(index) }))
|
|
1002
|
+
.filter((item) => !ctx.hidden.has(item.index));
|
|
1003
|
+
const scale = niceScale(0, Math.max(0, ...visible.map((v) => v.value)), ctx.sparkline ? 2 : 4);
|
|
1004
|
+
const cx = ctx.width / 2;
|
|
1005
|
+
const cy = ctx.height / 2;
|
|
1006
|
+
const radius = Math.max(20, Math.min(cy - 10, 110));
|
|
1007
|
+
const r = (v) => (radius * v) / (scale.max || 1);
|
|
1008
|
+
const step = 360 / Math.max(1, visible.length);
|
|
1009
|
+
const shapes = [];
|
|
1010
|
+
const items = [];
|
|
1011
|
+
visible.forEach((item, i) => {
|
|
1012
|
+
const start = i * step;
|
|
1013
|
+
shapes.push({ k: 'path', cls: 'cb-chart__slice cb-chart__slice--polar', item: i, d: arcPath(cx, cy, r(item.value), 0, start, start + step), fill: item.color });
|
|
1014
|
+
const [x, y] = polar(cx, cy, Math.max(r(item.value), 12) * 0.7, start + step / 2);
|
|
1015
|
+
items.push({ index: item.index, label: item.label, title: item.label, rows: [{ label: '', value: ctx.format(item.value), color: item.color }], x, y });
|
|
1016
|
+
});
|
|
1017
|
+
if (!ctx.sparkline) {
|
|
1018
|
+
// Anillos de la escala encima de los sectores, para leer los valores.
|
|
1019
|
+
for (const tick of scale.ticks) {
|
|
1020
|
+
if (tick <= 0)
|
|
1021
|
+
continue;
|
|
1022
|
+
shapes.push({ k: 'circle', cls: 'cb-chart__ring', cx, cy, r: round(r(tick)), rActive: round(r(tick)) });
|
|
1023
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis cb-chart__axis--tick', x: cx + 4, y: round(cy - r(tick)), text: ctx.format(tick), anchor: 'start', baseline: 'hanging' });
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
shapes,
|
|
1028
|
+
hits: [],
|
|
1029
|
+
items,
|
|
1030
|
+
legend: labels.map((label, index) => ({ index, label, color: colorOf(index) })),
|
|
1031
|
+
table: { head: null, rows: labels.map((label, i) => ({ label, cells: [ctx.format(ds?.data.at(i) ?? 0)] })) },
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** Un renderer por tipo: recibe el contexto y devuelve la escena a dibujar. */
|
|
1036
|
+
const CHART_RENDERERS = {
|
|
1037
|
+
bar: renderCartesian,
|
|
1038
|
+
horizontalBar: renderCartesian,
|
|
1039
|
+
line: renderCartesian,
|
|
1040
|
+
area: renderCartesian,
|
|
1041
|
+
waterfall: renderWaterfall,
|
|
1042
|
+
pie: renderCircular,
|
|
1043
|
+
doughnut: renderCircular,
|
|
1044
|
+
radialBar: renderCircular,
|
|
1045
|
+
gauge: renderGauge,
|
|
1046
|
+
radar: renderRadar,
|
|
1047
|
+
polarArea: renderPolarArea,
|
|
1048
|
+
heatmap: renderHeatmap,
|
|
1049
|
+
treemap: renderTreemap,
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
/** Escala numérica legible. Con `includeZero` en false se ajusta al rango (útil para precios). */
|
|
1053
|
+
function linearTicks(min, max, count = 4, includeZero = true) {
|
|
1054
|
+
let lo = includeZero ? Math.min(0, min) : min;
|
|
1055
|
+
let hi = includeZero ? Math.max(0, max) : max;
|
|
1056
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi))
|
|
1057
|
+
return { min: 0, max: 1, ticks: [0, 1] };
|
|
1058
|
+
if (lo === hi) {
|
|
1059
|
+
const pad = Math.abs(lo) * 0.1 || 1;
|
|
1060
|
+
lo -= includeZero && lo >= 0 ? 0 : pad;
|
|
1061
|
+
hi += pad;
|
|
1062
|
+
}
|
|
1063
|
+
const rough = (hi - lo) / Math.max(1, count);
|
|
1064
|
+
const magnitude = 10 ** Math.floor(Math.log10(rough));
|
|
1065
|
+
const residual = rough / magnitude;
|
|
1066
|
+
const nice = residual > 5 ? 10 : residual > 2.5 ? 5 : residual > 2 ? 2.5 : residual > 1 ? 2 : 1;
|
|
1067
|
+
const step = nice * magnitude;
|
|
1068
|
+
lo = Math.floor(lo / step) * step;
|
|
1069
|
+
hi = Math.ceil(hi / step) * step;
|
|
1070
|
+
const ticks = [];
|
|
1071
|
+
for (let v = lo; v <= hi + step / 2; v += step)
|
|
1072
|
+
ticks.push(round(v));
|
|
1073
|
+
return { min: round(lo), max: round(hi), ticks };
|
|
1074
|
+
}
|
|
1075
|
+
const SECOND = 1000;
|
|
1076
|
+
const MINUTE = 60 * SECOND;
|
|
1077
|
+
const HOUR = 60 * MINUTE;
|
|
1078
|
+
const DAY = 24 * HOUR;
|
|
1079
|
+
/** Intervalos "redondos" para un eje de tiempo, del más fino al más grueso. */
|
|
1080
|
+
const INTERVALS = [
|
|
1081
|
+
{ ms: SECOND, unit: 'second' },
|
|
1082
|
+
{ ms: 5 * SECOND, unit: 'second' },
|
|
1083
|
+
{ ms: 15 * SECOND, unit: 'second' },
|
|
1084
|
+
{ ms: 30 * SECOND, unit: 'second' },
|
|
1085
|
+
{ ms: MINUTE, unit: 'minute' },
|
|
1086
|
+
{ ms: 5 * MINUTE, unit: 'minute' },
|
|
1087
|
+
{ ms: 15 * MINUTE, unit: 'minute' },
|
|
1088
|
+
{ ms: 30 * MINUTE, unit: 'minute' },
|
|
1089
|
+
{ ms: HOUR, unit: 'hour' },
|
|
1090
|
+
{ ms: 3 * HOUR, unit: 'hour' },
|
|
1091
|
+
{ ms: 6 * HOUR, unit: 'hour' },
|
|
1092
|
+
{ ms: 12 * HOUR, unit: 'hour' },
|
|
1093
|
+
{ ms: DAY, unit: 'day' },
|
|
1094
|
+
{ ms: 2 * DAY, unit: 'day' },
|
|
1095
|
+
{ ms: 7 * DAY, unit: 'week' },
|
|
1096
|
+
{ ms: 30 * DAY, unit: 'month' },
|
|
1097
|
+
{ ms: 91 * DAY, unit: 'month' },
|
|
1098
|
+
{ ms: 365 * DAY, unit: 'year' },
|
|
1099
|
+
];
|
|
1100
|
+
/** Marcas de un eje de tiempo con unas `count` divisiones y la unidad para formatearlas. */
|
|
1101
|
+
function timeTicks(min, max, count = 6) {
|
|
1102
|
+
const span = Math.max(1, max - min);
|
|
1103
|
+
const target = span / Math.max(1, count);
|
|
1104
|
+
const interval = INTERVALS.find((i) => i.ms >= target) ?? INTERVALS[INTERVALS.length - 1];
|
|
1105
|
+
const ticks = [];
|
|
1106
|
+
if (interval.unit === 'month' || interval.unit === 'year') {
|
|
1107
|
+
const months = interval.unit === 'year' ? 12 * Math.max(1, Math.round(target / (365 * DAY))) : Math.max(1, Math.round(interval.ms / (30 * DAY)));
|
|
1108
|
+
const d = new Date(min);
|
|
1109
|
+
d.setDate(1);
|
|
1110
|
+
d.setHours(0, 0, 0, 0);
|
|
1111
|
+
if (interval.unit === 'year')
|
|
1112
|
+
d.setMonth(0);
|
|
1113
|
+
while (d.getTime() < min)
|
|
1114
|
+
d.setMonth(d.getMonth() + months);
|
|
1115
|
+
while (d.getTime() <= max && ticks.length < 200) {
|
|
1116
|
+
ticks.push(d.getTime());
|
|
1117
|
+
d.setMonth(d.getMonth() + months);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
// Alineado al intervalo en hora local (días a medianoche, horas en punto).
|
|
1122
|
+
const offset = new Date(min).getTimezoneOffset() * MINUTE;
|
|
1123
|
+
const start = Math.ceil((min - offset) / interval.ms) * interval.ms + offset;
|
|
1124
|
+
for (let t = start; t <= max && ticks.length < 200; t += interval.ms)
|
|
1125
|
+
ticks.push(t);
|
|
1126
|
+
}
|
|
1127
|
+
return { ticks, unit: interval.unit };
|
|
1128
|
+
}
|
|
1129
|
+
/** Formato de una marca de tiempo según la unidad del eje. */
|
|
1130
|
+
function timeFormat(locale, unit) {
|
|
1131
|
+
const options = unit === 'second'
|
|
1132
|
+
? { hour: '2-digit', minute: '2-digit', second: '2-digit' }
|
|
1133
|
+
: unit === 'minute' || unit === 'hour'
|
|
1134
|
+
? { hour: '2-digit', minute: '2-digit' }
|
|
1135
|
+
: unit === 'day' || unit === 'week'
|
|
1136
|
+
? { day: 'numeric', month: 'short' }
|
|
1137
|
+
: unit === 'month'
|
|
1138
|
+
? { month: 'short', year: '2-digit' }
|
|
1139
|
+
: { year: 'numeric' };
|
|
1140
|
+
const format = new Intl.DateTimeFormat(locale, options).format;
|
|
1141
|
+
return (t) => format(t);
|
|
1142
|
+
}
|
|
1143
|
+
/** Formato largo (tooltip y tabla) de un instante. */
|
|
1144
|
+
function timeFormatLong(locale, unit) {
|
|
1145
|
+
const options = unit === 'second' || unit === 'minute' || unit === 'hour'
|
|
1146
|
+
? { dateStyle: 'medium', timeStyle: unit === 'second' ? 'medium' : 'short' }
|
|
1147
|
+
: { dateStyle: 'medium' };
|
|
1148
|
+
const format = new Intl.DateTimeFormat(locale, options).format;
|
|
1149
|
+
return (t) => format(t);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/** Número de un campo (`Date` → milisegundos, texto numérico → número); `null` si no hay dato. */
|
|
1153
|
+
function num(v) {
|
|
1154
|
+
if (typeof v === 'number')
|
|
1155
|
+
return Number.isFinite(v) ? v : null;
|
|
1156
|
+
if (v instanceof Date)
|
|
1157
|
+
return v.getTime();
|
|
1158
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
1159
|
+
const n = Number(v);
|
|
1160
|
+
return Number.isFinite(n) ? n : null;
|
|
1161
|
+
}
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
/** Trazado de una serie según la curva: recta, suave (monótona, no se sale de los datos) o escalonada. */
|
|
1165
|
+
function curvePath(pts, curve) {
|
|
1166
|
+
if (pts.length < 2 || curve === 'linear')
|
|
1167
|
+
return polyline(pts);
|
|
1168
|
+
if (curve === 'step') {
|
|
1169
|
+
return pts.map((p, i) => (i ? `H ${p.x} V ${p.y}` : `M ${p.x} ${p.y}`)).join(' ');
|
|
1170
|
+
}
|
|
1171
|
+
// Monótona en X (Fritsch–Carlson): suaviza sin crear picos que no están en los datos.
|
|
1172
|
+
const n = pts.length;
|
|
1173
|
+
const m = [];
|
|
1174
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1175
|
+
const dx = pts[i + 1].x - pts[i].x;
|
|
1176
|
+
m.push(dx ? (pts[i + 1].y - pts[i].y) / dx : 0);
|
|
1177
|
+
}
|
|
1178
|
+
const t = [m[0]];
|
|
1179
|
+
for (let i = 1; i < n - 1; i++)
|
|
1180
|
+
t.push(m[i - 1] * m[i] <= 0 ? 0 : (m[i - 1] + m[i]) / 2);
|
|
1181
|
+
t.push(m[n - 2]);
|
|
1182
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1183
|
+
if (m[i] === 0) {
|
|
1184
|
+
t[i] = 0;
|
|
1185
|
+
t[i + 1] = 0;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
const a = t[i] / m[i];
|
|
1189
|
+
const b = t[i + 1] / m[i];
|
|
1190
|
+
const h = a * a + b * b;
|
|
1191
|
+
if (h > 9) {
|
|
1192
|
+
const s = 3 / Math.sqrt(h);
|
|
1193
|
+
t[i] = s * a * m[i];
|
|
1194
|
+
t[i + 1] = s * b * m[i];
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
let d = `M ${pts[0].x} ${pts[0].y}`;
|
|
1198
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1199
|
+
const p = pts[i];
|
|
1200
|
+
const q = pts[i + 1];
|
|
1201
|
+
const dx = (q.x - p.x) / 3;
|
|
1202
|
+
d += ` C ${round(p.x + dx)} ${round(p.y + t[i] * dx)} ${round(q.x - dx)} ${round(q.y - t[i + 1] * dx)} ${q.x} ${q.y}`;
|
|
1203
|
+
}
|
|
1204
|
+
return d;
|
|
1205
|
+
}
|
|
1206
|
+
/** Gráfico composable: series, ejes y referencias declarados como hijos de `<cb-chart>`. */
|
|
1207
|
+
function renderComposed(ctx) {
|
|
1208
|
+
const spec = ctx.composed;
|
|
1209
|
+
const all = spec.series;
|
|
1210
|
+
const visible = all.filter((s) => !ctx.hidden.has(s.index));
|
|
1211
|
+
const axisOf = (pos) => spec.axes.find((a) => a.position === pos);
|
|
1212
|
+
const xAxis = axisOf('bottom');
|
|
1213
|
+
// --- Eje X: tipo y claves --------------------------------------------------------------
|
|
1214
|
+
const rawXs = all.flatMap((s) => s.records.map((r) => r[s.x]));
|
|
1215
|
+
let xType = xAxis?.type ?? 'auto';
|
|
1216
|
+
if (xType === 'auto') {
|
|
1217
|
+
xType = rawXs.some((v) => v instanceof Date)
|
|
1218
|
+
? 'time'
|
|
1219
|
+
: rawXs.some((v) => typeof v === 'number') && rawXs.every((v) => v == null || typeof v === 'number')
|
|
1220
|
+
? 'linear'
|
|
1221
|
+
: 'category';
|
|
1222
|
+
}
|
|
1223
|
+
const categories = [];
|
|
1224
|
+
const catIndex = new Map();
|
|
1225
|
+
const toX = (v, register) => {
|
|
1226
|
+
if (xType === 'category') {
|
|
1227
|
+
if (v == null)
|
|
1228
|
+
return null;
|
|
1229
|
+
const k = String(v);
|
|
1230
|
+
if (!catIndex.has(k)) {
|
|
1231
|
+
if (!register)
|
|
1232
|
+
return null;
|
|
1233
|
+
catIndex.set(k, categories.length);
|
|
1234
|
+
categories.push(k);
|
|
1235
|
+
}
|
|
1236
|
+
return catIndex.get(k) + 0.5;
|
|
1237
|
+
}
|
|
1238
|
+
if (xType === 'time' && typeof v === 'string') {
|
|
1239
|
+
const t = Date.parse(v);
|
|
1240
|
+
return Number.isFinite(t) ? t : null;
|
|
1241
|
+
}
|
|
1242
|
+
return num(v);
|
|
1243
|
+
};
|
|
1244
|
+
// Puntos de cada serie (todas, para que el dominio no cambie al ocultar una).
|
|
1245
|
+
const points = new Map();
|
|
1246
|
+
for (const s of all) {
|
|
1247
|
+
const pts = [];
|
|
1248
|
+
for (const r of s.records) {
|
|
1249
|
+
const key = toX(r[s.x], true);
|
|
1250
|
+
if (key === null)
|
|
1251
|
+
continue;
|
|
1252
|
+
pts.push({ key, y: num(r[s.y]), low: num(r[s.low]), high: num(r[s.high]), open: num(r[s.open]), close: num(r[s.close]), base: 0, top: 0 });
|
|
1253
|
+
}
|
|
1254
|
+
pts.sort((a, b) => a.key - b.key);
|
|
1255
|
+
points.set(s, pts);
|
|
1256
|
+
}
|
|
1257
|
+
const keys = [...new Set(all.flatMap((s) => points.get(s).map((p) => p.key)))].sort((a, b) => a - b);
|
|
1258
|
+
if (!keys.length)
|
|
1259
|
+
return { shapes: [], hits: [], items: [], legend: legendOf(all), table: { head: null, rows: [] } };
|
|
1260
|
+
const hasBands = visible.some((s) => s.kind === 'bar' || s.kind === 'candlestick');
|
|
1261
|
+
let minGap = Infinity;
|
|
1262
|
+
for (let i = 1; i < keys.length; i++)
|
|
1263
|
+
minGap = Math.min(minGap, keys[i] - keys[i - 1]);
|
|
1264
|
+
let full;
|
|
1265
|
+
if (xType === 'category') {
|
|
1266
|
+
full = [0, categories.length];
|
|
1267
|
+
minGap = 1;
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
if (!Number.isFinite(minGap))
|
|
1271
|
+
minGap = Math.abs(keys[0]) * 0.1 || 1;
|
|
1272
|
+
const pad = hasBands ? minGap / 2 : 0;
|
|
1273
|
+
full = keys[0] === keys[keys.length - 1] ? [keys[0] - minGap, keys[0] + minGap] : [keys[0] - pad, keys[keys.length - 1] + pad];
|
|
1274
|
+
}
|
|
1275
|
+
const span = full[1] - full[0];
|
|
1276
|
+
// Ventana visible: la elegida con zoom, los últimos `xWindow` o todo.
|
|
1277
|
+
let d0 = full[0];
|
|
1278
|
+
let d1 = full[1];
|
|
1279
|
+
const win = spec.window;
|
|
1280
|
+
if (win) {
|
|
1281
|
+
const w = Math.min(Math.max(win.end - win.start, minGap), span);
|
|
1282
|
+
d0 = win.pinned ? full[1] - w : Math.min(Math.max(win.start, full[0]), full[1] - w);
|
|
1283
|
+
d1 = d0 + w;
|
|
1284
|
+
}
|
|
1285
|
+
else if (spec.xWindow && spec.xWindow < span) {
|
|
1286
|
+
d0 = full[1] - spec.xWindow;
|
|
1287
|
+
}
|
|
1288
|
+
const inView = (k) => k >= d0 - 1e-9 && k <= d1 + 1e-9;
|
|
1289
|
+
// --- Área de dibujo -------------------------------------------------------------------
|
|
1290
|
+
const refsOn = (axis) => spec.refs.some((r) => r.axis === axis);
|
|
1291
|
+
const usesRight = all.some((s) => s.axis === 'right') || !!axisOf('right') || refsOn('right');
|
|
1292
|
+
const usesLeft = all.some((s) => s.axis === 'left') || !!axisOf('left') || refsOn('left');
|
|
1293
|
+
const yLabels = spec.axes.some((a) => a.label && a.position !== 'bottom');
|
|
1294
|
+
const pad = ctx.sparkline
|
|
1295
|
+
? { t: 6, r: 6, b: 6, l: 6 }
|
|
1296
|
+
: { t: yLabels ? 30 : 16, r: usesRight ? 60 : 16, b: xAxis?.label ? 46 : 30, l: usesLeft ? 60 : 16 };
|
|
1297
|
+
const plot = {
|
|
1298
|
+
left: pad.l,
|
|
1299
|
+
right: ctx.width - pad.r,
|
|
1300
|
+
top: pad.t,
|
|
1301
|
+
bottom: ctx.height - pad.b,
|
|
1302
|
+
width: Math.max(10, ctx.width - pad.l - pad.r),
|
|
1303
|
+
height: Math.max(10, ctx.height - pad.t - pad.b),
|
|
1304
|
+
};
|
|
1305
|
+
const xPos = (k) => round(plot.left + ((k - d0) / (d1 - d0 || 1)) * plot.width);
|
|
1306
|
+
// --- Apilado ---------------------------------------------------------------------------
|
|
1307
|
+
const stacks = new Map();
|
|
1308
|
+
for (const s of visible) {
|
|
1309
|
+
for (const p of points.get(s)) {
|
|
1310
|
+
const v = p.y ?? 0;
|
|
1311
|
+
if ((s.kind === 'bar' || s.kind === 'area') && s.stack) {
|
|
1312
|
+
const id = `${s.axis}:${s.stack}`;
|
|
1313
|
+
if (!stacks.has(id))
|
|
1314
|
+
stacks.set(id, new Map());
|
|
1315
|
+
const totals = stacks.get(id).get(p.key) ?? { pos: 0, neg: 0 };
|
|
1316
|
+
p.base = v >= 0 ? totals.pos : totals.neg;
|
|
1317
|
+
if (v >= 0)
|
|
1318
|
+
totals.pos += v;
|
|
1319
|
+
else
|
|
1320
|
+
totals.neg += v;
|
|
1321
|
+
stacks.get(id).set(p.key, totals);
|
|
1322
|
+
p.top = p.base + v;
|
|
1323
|
+
}
|
|
1324
|
+
else {
|
|
1325
|
+
p.base = 0;
|
|
1326
|
+
p.top = v;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
// --- Ejes Y ----------------------------------------------------------------------------
|
|
1331
|
+
const tickCount = ctx.sparkline ? 2 : Math.max(2, Math.round(plot.height / 55));
|
|
1332
|
+
const yScale = (axis) => {
|
|
1333
|
+
const values = [];
|
|
1334
|
+
for (const s of visible) {
|
|
1335
|
+
if (s.axis !== axis)
|
|
1336
|
+
continue;
|
|
1337
|
+
for (const p of points.get(s)) {
|
|
1338
|
+
if (!inView(p.key))
|
|
1339
|
+
continue;
|
|
1340
|
+
if (s.kind === 'rangeArea' || s.kind === 'candlestick')
|
|
1341
|
+
values.push(...[p.low, p.high].filter((v) => v !== null));
|
|
1342
|
+
else if (p.y !== null)
|
|
1343
|
+
values.push(p.top, p.base);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
for (const r of spec.refs) {
|
|
1347
|
+
if (r.axis !== axis)
|
|
1348
|
+
continue;
|
|
1349
|
+
for (const v of [r.value, r.from, r.to]) {
|
|
1350
|
+
const n = num(v);
|
|
1351
|
+
if (n !== null)
|
|
1352
|
+
values.push(n);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const conf = axisOf(axis);
|
|
1356
|
+
const zero = visible.some((s) => s.axis === axis && (s.kind === 'bar' || s.kind === 'area'));
|
|
1357
|
+
const lo = conf?.min ?? (values.length ? Math.min(...values) : 0);
|
|
1358
|
+
const hi = conf?.max ?? (values.length ? Math.max(...values) : 1);
|
|
1359
|
+
const scale = linearTicks(lo, hi, tickCount, zero && conf?.min === undefined);
|
|
1360
|
+
const min = conf?.min ?? scale.min;
|
|
1361
|
+
const max = conf?.max ?? scale.max;
|
|
1362
|
+
return { min, max, ticks: scale.ticks.filter((t) => t >= min - 1e-9 && t <= max + 1e-9), format: conf?.format ?? ctx.format };
|
|
1363
|
+
};
|
|
1364
|
+
const scales = { left: yScale('left'), right: yScale('right') };
|
|
1365
|
+
const yPos = (axis, v) => {
|
|
1366
|
+
const { min, max } = scales[axis];
|
|
1367
|
+
return round(plot.bottom - ((v - min) / (max - min || 1)) * plot.height);
|
|
1368
|
+
};
|
|
1369
|
+
// --- Formatos del eje X -----------------------------------------------------------------
|
|
1370
|
+
let unit = 'day';
|
|
1371
|
+
let xTicks = [];
|
|
1372
|
+
const xCount = Math.max(2, Math.floor(plot.width / 90));
|
|
1373
|
+
if (xType === 'time')
|
|
1374
|
+
({ ticks: xTicks, unit } = timeTicks(d0, d1, xCount));
|
|
1375
|
+
else if (xType === 'linear')
|
|
1376
|
+
xTicks = linearTicks(d0, d1, xCount, false).ticks.filter((t) => t >= d0 && t <= d1);
|
|
1377
|
+
const xFormatShort = xType === 'time' ? timeFormat(ctx.locale, unit) : xType === 'linear' ? (xAxis?.format ?? ctx.format) : (k) => categories[Math.floor(k)] ?? '';
|
|
1378
|
+
// En el tooltip y la tabla la fecha se escribe completa, con la unidad de los datos.
|
|
1379
|
+
const dataUnit = minGap < 60_000 ? 'second' : minGap < 86_400_000 ? 'minute' : 'day';
|
|
1380
|
+
const xFormatLong = xType === 'time' ? timeFormatLong(ctx.locale, dataUnit) : xFormatShort;
|
|
1381
|
+
// --- Rejilla y ejes ------------------------------------------------------------------
|
|
1382
|
+
const shapes = [];
|
|
1383
|
+
const primary = all.some((s) => s.axis === 'left') || !usesRight ? 'left' : 'right';
|
|
1384
|
+
if (!ctx.sparkline) {
|
|
1385
|
+
for (const axis of ['left', 'right']) {
|
|
1386
|
+
if (axis === 'left' ? !usesLeft : !usesRight)
|
|
1387
|
+
continue;
|
|
1388
|
+
const scale = scales[axis];
|
|
1389
|
+
const grid = axisOf(axis)?.grid ?? axis === primary;
|
|
1390
|
+
for (const tick of scale.ticks) {
|
|
1391
|
+
const y = yPos(axis, tick);
|
|
1392
|
+
if (grid)
|
|
1393
|
+
shapes.push({ k: 'line', cls: tick === 0 ? 'cb-chart__grid cb-chart__grid--zero' : 'cb-chart__grid', x1: plot.left, y1: y, x2: plot.right, y2: y });
|
|
1394
|
+
shapes.push(axis === 'left'
|
|
1395
|
+
? { k: 'text', cls: 'cb-chart__axis', x: plot.left - 8, y, text: scale.format(tick), anchor: 'end', baseline: 'central' }
|
|
1396
|
+
: { k: 'text', cls: 'cb-chart__axis', x: plot.right + 8, y, text: scale.format(tick), anchor: 'start', baseline: 'central' });
|
|
1397
|
+
}
|
|
1398
|
+
const label = axisOf(axis)?.label;
|
|
1399
|
+
if (label) {
|
|
1400
|
+
shapes.push(axis === 'left'
|
|
1401
|
+
? { k: 'text', cls: 'cb-chart__axis cb-chart__axis-title', x: 8, y: plot.top - 14, text: label, anchor: 'start' }
|
|
1402
|
+
: { k: 'text', cls: 'cb-chart__axis cb-chart__axis-title', x: ctx.width - 8, y: plot.top - 14, text: label, anchor: 'end' });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
shapes.push({ k: 'line', cls: 'cb-chart__baseline', x1: plot.left, y1: plot.bottom, x2: plot.right, y2: plot.bottom });
|
|
1406
|
+
if (xType === 'category') {
|
|
1407
|
+
const shown = categories.map((text, i) => ({ text, k: i + 0.5 })).filter((c) => inView(c.k));
|
|
1408
|
+
const longest = Math.max(1, ...shown.map((c) => c.text.length));
|
|
1409
|
+
const every = Math.max(1, Math.ceil((shown.length * (longest * 6.4 + 12)) / plot.width));
|
|
1410
|
+
shown.forEach((c, i) => {
|
|
1411
|
+
if (i % every)
|
|
1412
|
+
return;
|
|
1413
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x: xPos(c.k), y: plot.bottom + 18, text: c.text, anchor: 'middle' });
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
for (const tick of xTicks) {
|
|
1418
|
+
const x = xPos(tick);
|
|
1419
|
+
if (axisOf('bottom')?.grid)
|
|
1420
|
+
shapes.push({ k: 'line', cls: 'cb-chart__grid', x1: x, y1: plot.top, x2: x, y2: plot.bottom });
|
|
1421
|
+
shapes.push({ k: 'line', cls: 'cb-chart__tick', x1: x, y1: plot.bottom, x2: x, y2: plot.bottom + 4 });
|
|
1422
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis', x, y: plot.bottom + 18, text: xFormatShort(tick), anchor: 'middle' });
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (xAxis?.label) {
|
|
1426
|
+
shapes.push({ k: 'text', cls: 'cb-chart__axis cb-chart__axis-title', x: round(plot.left + plot.width / 2), y: plot.bottom + 38, text: xAxis.label, anchor: 'middle' });
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
// --- Referencias: franjas detrás de los datos, líneas delante ---------------------------
|
|
1430
|
+
const data = [];
|
|
1431
|
+
const front = [];
|
|
1432
|
+
const refPos = (axis, v) => {
|
|
1433
|
+
if (axis === 'x') {
|
|
1434
|
+
const k = toX(v, false);
|
|
1435
|
+
return k === null ? null : xPos(k);
|
|
1436
|
+
}
|
|
1437
|
+
const n = num(v);
|
|
1438
|
+
return n === null ? null : yPos(axis, n);
|
|
1439
|
+
};
|
|
1440
|
+
for (const r of spec.refs) {
|
|
1441
|
+
if (r.kind === 'band') {
|
|
1442
|
+
const a = refPos(r.axis, r.from);
|
|
1443
|
+
const b = refPos(r.axis, r.to);
|
|
1444
|
+
if (a === null || b === null)
|
|
1445
|
+
continue;
|
|
1446
|
+
const fill = r.color ?? 'var(--cb-color-primary-500)';
|
|
1447
|
+
if (r.axis === 'x') {
|
|
1448
|
+
data.push({ k: 'rect', cls: 'cb-chart__ref-band', x: Math.min(a, b), y: plot.top, w: Math.abs(b - a), h: plot.height, fill });
|
|
1449
|
+
if (r.label)
|
|
1450
|
+
front.push({ k: 'text', cls: 'cb-chart__ref-label', x: Math.min(a, b) + 4, y: plot.top + 12, text: r.label, anchor: 'start', fill: r.color });
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
data.push({ k: 'rect', cls: 'cb-chart__ref-band', x: plot.left, y: Math.min(a, b), w: plot.width, h: Math.abs(b - a), fill });
|
|
1454
|
+
if (r.label)
|
|
1455
|
+
front.push({ k: 'text', cls: 'cb-chart__ref-label', x: plot.right - 4, y: Math.min(a, b) + 12, text: r.label, anchor: 'end', fill: r.color });
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
// --- Series -----------------------------------------------------------------------------
|
|
1460
|
+
const barLike = visible.filter((s) => s.kind === 'bar');
|
|
1461
|
+
const slots = [...new Set(barLike.map((s) => (s.stack ? `${s.axis}:${s.stack}` : `#${s.index}`)))];
|
|
1462
|
+
const bandPx = (minGap / (d1 - d0 || 1)) * plot.width;
|
|
1463
|
+
const groupW = Math.min(bandPx * 0.75, 40 * Math.max(1, slots.length));
|
|
1464
|
+
const slotW = groupW / Math.max(1, slots.length);
|
|
1465
|
+
const pointTop = new Map();
|
|
1466
|
+
const noteTop = (k, y) => pointTop.set(k, Math.min(pointTop.get(k) ?? plot.bottom, y));
|
|
1467
|
+
for (const s of visible) {
|
|
1468
|
+
const pts = points.get(s);
|
|
1469
|
+
// Un punto fuera de la ventana a cada lado, para que las líneas lleguen al borde.
|
|
1470
|
+
const first = Math.max(0, pts.findIndex((p) => p.key >= d0) - 1);
|
|
1471
|
+
let last = pts.length - 1;
|
|
1472
|
+
for (let i = pts.length - 1; i >= 0; i--)
|
|
1473
|
+
if (pts[i].key <= d1) {
|
|
1474
|
+
last = Math.min(pts.length - 1, i + 1);
|
|
1475
|
+
break;
|
|
1476
|
+
}
|
|
1477
|
+
const win = pts.slice(first, last + 1);
|
|
1478
|
+
const axis = s.axis;
|
|
1479
|
+
if (s.kind === 'bar') {
|
|
1480
|
+
const slot = slots.indexOf(s.stack ? `${s.axis}:${s.stack}` : `#${s.index}`);
|
|
1481
|
+
for (const p of win) {
|
|
1482
|
+
if (p.y === null)
|
|
1483
|
+
continue;
|
|
1484
|
+
const a = yPos(axis, p.base);
|
|
1485
|
+
const b = yPos(axis, p.top);
|
|
1486
|
+
const x = round(xPos(p.key) - groupW / 2 + slot * slotW + 1);
|
|
1487
|
+
const item = keys.indexOf(p.key);
|
|
1488
|
+
data.push({ k: 'rect', cls: 'cb-chart__bar', item, x, y: Math.min(a, b), w: round(Math.max(1, slotW - 2)), h: round(Math.max(1, Math.abs(b - a))), rx: slotW > 8 ? 2 : 0, fill: s.color });
|
|
1489
|
+
noteTop(p.key, Math.min(a, b));
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
else if (s.kind === 'area' || s.kind === 'line') {
|
|
1493
|
+
const valid = win.filter((p) => p.y !== null);
|
|
1494
|
+
const top = valid.map((p) => ({ x: xPos(p.key), y: yPos(axis, p.top) }));
|
|
1495
|
+
if (s.kind === 'area' && top.length) {
|
|
1496
|
+
const floor = valid.map((p) => ({ x: xPos(p.key), y: yPos(axis, p.base) })).reverse();
|
|
1497
|
+
data.push({ k: 'path', cls: 'cb-chart__area', d: `${curvePath(top, s.curve)} L ${floor.map((p) => `${p.x} ${p.y}`).join(' L ')} Z`, fill: s.color });
|
|
1498
|
+
}
|
|
1499
|
+
front.push({ k: 'path', cls: s.dashed ? 'cb-chart__line cb-chart__line--dashed' : 'cb-chart__line', d: curvePath(top, s.curve), stroke: s.color, pathLength: s.dashed ? undefined : 1 });
|
|
1500
|
+
valid.forEach((p, i) => {
|
|
1501
|
+
noteTop(p.key, top[i].y);
|
|
1502
|
+
if (s.kind === 'line' && !s.points && !ctx.sparkline)
|
|
1503
|
+
return;
|
|
1504
|
+
front.push({ k: 'circle', cls: 'cb-chart__point', item: keys.indexOf(p.key), cx: top[i].x, cy: top[i].y, r: ctx.sparkline || !s.points ? 0 : 3, rActive: 5, fill: s.color });
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
else if (s.kind === 'rangeArea') {
|
|
1508
|
+
const valid = win.filter((p) => p.low !== null && p.high !== null);
|
|
1509
|
+
const hi = valid.map((p) => ({ x: xPos(p.key), y: yPos(axis, p.high) }));
|
|
1510
|
+
const lo = valid.map((p) => ({ x: xPos(p.key), y: yPos(axis, p.low) }));
|
|
1511
|
+
if (hi.length) {
|
|
1512
|
+
const back = curvePath([...lo].reverse(), s.curve).replace(/^M/, 'L');
|
|
1513
|
+
data.push({ k: 'path', cls: 'cb-chart__area cb-chart__area--range', d: `${curvePath(hi, s.curve)} ${back} Z`, fill: s.color });
|
|
1514
|
+
front.push({ k: 'path', cls: 'cb-chart__line cb-chart__line--thin', d: curvePath(hi, s.curve), stroke: s.color });
|
|
1515
|
+
front.push({ k: 'path', cls: 'cb-chart__line cb-chart__line--thin', d: curvePath(lo, s.curve), stroke: s.color });
|
|
1516
|
+
hi.forEach((p, i) => noteTop(valid[i].key, p.y));
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
else if (s.kind === 'scatter') {
|
|
1520
|
+
for (const p of win) {
|
|
1521
|
+
if (p.y === null)
|
|
1522
|
+
continue;
|
|
1523
|
+
const cx = xPos(p.key);
|
|
1524
|
+
const cy = yPos(axis, p.y);
|
|
1525
|
+
noteTop(p.key, cy);
|
|
1526
|
+
front.push({ k: 'circle', cls: 'cb-chart__point cb-chart__point--scatter', item: -1, cx, cy, r: s.size, rActive: s.size + 2, fill: s.color });
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
const w = Math.max(1, Math.min(bandPx * 0.65, 18));
|
|
1531
|
+
for (const p of win) {
|
|
1532
|
+
if (p.open === null || p.close === null || p.high === null || p.low === null)
|
|
1533
|
+
continue;
|
|
1534
|
+
const up = p.close >= p.open;
|
|
1535
|
+
const color = up ? s.upColor : s.downColor;
|
|
1536
|
+
const x = xPos(p.key);
|
|
1537
|
+
const item = keys.indexOf(p.key);
|
|
1538
|
+
const yo = yPos(axis, p.open);
|
|
1539
|
+
const yc = yPos(axis, p.close);
|
|
1540
|
+
data.push({ k: 'line', cls: 'cb-chart__wick', x1: x, y1: yPos(axis, p.high), x2: x, y2: yPos(axis, p.low), stroke: color });
|
|
1541
|
+
if (s.variant === 'ohlc') {
|
|
1542
|
+
data.push({ k: 'line', cls: 'cb-chart__wick', x1: round(x - w / 2), y1: yo, x2: x, y2: yo, stroke: color });
|
|
1543
|
+
data.push({ k: 'line', cls: 'cb-chart__wick', x1: x, y1: yc, x2: round(x + w / 2), y2: yc, stroke: color });
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
data.push({ k: 'rect', cls: 'cb-chart__candle', item, x: round(x - w / 2), y: Math.min(yo, yc), w: round(w), h: round(Math.max(1, Math.abs(yc - yo))), fill: color });
|
|
1547
|
+
}
|
|
1548
|
+
noteTop(p.key, yPos(axis, p.high));
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
for (const r of spec.refs) {
|
|
1553
|
+
if (r.kind !== 'line')
|
|
1554
|
+
continue;
|
|
1555
|
+
const pos = refPos(r.axis, r.value);
|
|
1556
|
+
if (pos === null)
|
|
1557
|
+
continue;
|
|
1558
|
+
const stroke = r.color;
|
|
1559
|
+
if (r.axis === 'x') {
|
|
1560
|
+
front.push({ k: 'line', cls: 'cb-chart__ref-line', x1: pos, y1: plot.top, x2: pos, y2: plot.bottom, stroke });
|
|
1561
|
+
if (r.label)
|
|
1562
|
+
front.push({ k: 'text', cls: 'cb-chart__ref-label', x: pos + 4, y: plot.top + 12, text: r.label, anchor: 'start', fill: r.color });
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
front.push({ k: 'line', cls: 'cb-chart__ref-line', x1: plot.left, y1: pos, x2: plot.right, y2: pos, stroke });
|
|
1566
|
+
if (r.label)
|
|
1567
|
+
front.push({ k: 'text', cls: 'cb-chart__ref-label', x: plot.left + 4, y: pos - 5, text: r.label, anchor: 'start', fill: r.color });
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
// Anotaciones: un punto con su etiqueta encima (o debajo si no cabe).
|
|
1571
|
+
const notes = new Map();
|
|
1572
|
+
for (const r of spec.refs) {
|
|
1573
|
+
if (r.kind !== 'note' || !r.label || r.y === undefined)
|
|
1574
|
+
continue;
|
|
1575
|
+
const k = toX(r.value, false);
|
|
1576
|
+
if (k === null || !inView(k))
|
|
1577
|
+
continue;
|
|
1578
|
+
const axis = r.axis === 'right' ? 'right' : 'left';
|
|
1579
|
+
const x = xPos(k);
|
|
1580
|
+
const y = yPos(axis, r.y);
|
|
1581
|
+
const color = r.color ?? 'var(--cb-color-text)';
|
|
1582
|
+
front.push({ k: 'circle', cls: 'cb-chart__note-dot', cx: x, cy: y, r: 4, rActive: 4, fill: color });
|
|
1583
|
+
const below = y < plot.top + 22;
|
|
1584
|
+
front.push({ k: 'text', cls: 'cb-chart__note', x, y: below ? y + 18 : y - 10, text: r.label, anchor: 'middle', fill: r.color });
|
|
1585
|
+
notes.set(k, [...(notes.get(k) ?? []), { label: r.label, color }]);
|
|
1586
|
+
}
|
|
1587
|
+
// --- Elementos navegables ------------------------------------------------------------
|
|
1588
|
+
const t = ctx.text;
|
|
1589
|
+
const rowsAt = (k) => {
|
|
1590
|
+
const rows = [];
|
|
1591
|
+
for (const s of visible) {
|
|
1592
|
+
const p = points.get(s).find((q) => q.key === k);
|
|
1593
|
+
if (!p)
|
|
1594
|
+
continue;
|
|
1595
|
+
const fmt = scales[s.axis].format;
|
|
1596
|
+
if (s.kind === 'candlestick') {
|
|
1597
|
+
if (p.open === null || p.close === null || p.high === null || p.low === null)
|
|
1598
|
+
continue;
|
|
1599
|
+
const color = p.close >= p.open ? s.upColor : s.downColor;
|
|
1600
|
+
rows.push({ label: `${s.name} · ${t.open}`, value: fmt(p.open), color }, { label: t.high, value: fmt(p.high), color }, { label: t.low, value: fmt(p.low), color }, { label: t.close, value: fmt(p.close), color });
|
|
1601
|
+
}
|
|
1602
|
+
else if (s.kind === 'rangeArea') {
|
|
1603
|
+
if (p.low !== null && p.high !== null)
|
|
1604
|
+
rows.push({ label: s.name, value: `${fmt(p.low)} – ${fmt(p.high)}`, color: s.color });
|
|
1605
|
+
}
|
|
1606
|
+
else if (p.y !== null) {
|
|
1607
|
+
rows.push({ label: s.name, value: fmt(p.y), color: s.color });
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
for (const note of notes.get(k) ?? [])
|
|
1611
|
+
rows.push({ label: note.label, value: '', color: note.color });
|
|
1612
|
+
return rows;
|
|
1613
|
+
};
|
|
1614
|
+
const scatterOnly = visible.length > 0 && visible.every((s) => s.kind === 'scatter');
|
|
1615
|
+
const items = [];
|
|
1616
|
+
const hits = [];
|
|
1617
|
+
if (scatterOnly) {
|
|
1618
|
+
// Cada punto es un elemento.
|
|
1619
|
+
for (const s of visible) {
|
|
1620
|
+
for (const p of points.get(s)) {
|
|
1621
|
+
if (p.y === null || !inView(p.key))
|
|
1622
|
+
continue;
|
|
1623
|
+
const item = items.length;
|
|
1624
|
+
const x = xPos(p.key);
|
|
1625
|
+
const y = yPos(s.axis, p.y);
|
|
1626
|
+
const label = `${s.name} · ${xFormatLong(p.key)}`;
|
|
1627
|
+
items.push({
|
|
1628
|
+
index: item,
|
|
1629
|
+
label,
|
|
1630
|
+
title: s.name,
|
|
1631
|
+
rows: [
|
|
1632
|
+
{ label: xAxis?.label ?? 'X', value: xFormatLong(p.key), color: s.color },
|
|
1633
|
+
{ label: axisOf(s.axis)?.label ?? 'Y', value: scales[s.axis].format(p.y), color: s.color },
|
|
1634
|
+
],
|
|
1635
|
+
x,
|
|
1636
|
+
y,
|
|
1637
|
+
});
|
|
1638
|
+
hits.push({ k: 'rect', item, x: x - s.size - 3, y: y - s.size - 3, w: 2 * s.size + 6, h: 2 * s.size + 6 });
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
// Los puntos se resaltan por su posición en `items`.
|
|
1642
|
+
let n = 0;
|
|
1643
|
+
for (const shape of front)
|
|
1644
|
+
if (shape.k === 'circle' && shape.item === -1)
|
|
1645
|
+
shape.item = n++;
|
|
1646
|
+
}
|
|
1647
|
+
else {
|
|
1648
|
+
const shown = keys.filter(inView);
|
|
1649
|
+
shown.forEach((k, i) => {
|
|
1650
|
+
const x = xPos(k);
|
|
1651
|
+
const prev = i ? (xPos(shown[i - 1]) + x) / 2 : plot.left;
|
|
1652
|
+
const next = i < shown.length - 1 ? (xPos(shown[i + 1]) + x) / 2 : plot.right;
|
|
1653
|
+
const position = items.length;
|
|
1654
|
+
const title = xFormatLong(k);
|
|
1655
|
+
items.push({ index: keys.indexOf(k), key: k, label: title, title, rows: rowsAt(k), x, y: pointTop.get(k) ?? plot.top, cursor: { x, y1: plot.top, y2: plot.bottom } });
|
|
1656
|
+
hits.push({ k: 'rect', item: position, x: round(Math.max(plot.left, prev)), y: plot.top, w: round(Math.max(1, Math.min(plot.right, next) - Math.max(plot.left, prev))), h: plot.height });
|
|
1657
|
+
});
|
|
1658
|
+
// Barras, velas y puntos se resaltan por la posición de su clave en `items`.
|
|
1659
|
+
const position = new Map(shown.map((k, i) => [keys.indexOf(k), i]));
|
|
1660
|
+
for (const shape of [...data, ...front]) {
|
|
1661
|
+
if (shape.item !== undefined && shape.item >= 0)
|
|
1662
|
+
shape.item = position.get(shape.item);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return {
|
|
1666
|
+
shapes,
|
|
1667
|
+
data,
|
|
1668
|
+
front,
|
|
1669
|
+
hits,
|
|
1670
|
+
items,
|
|
1671
|
+
legend: legendOf(all),
|
|
1672
|
+
table: {
|
|
1673
|
+
head: all.map((s) => s.name),
|
|
1674
|
+
rows: keys.map((k) => ({
|
|
1675
|
+
label: xFormatLong(k),
|
|
1676
|
+
cells: all.map((s) => {
|
|
1677
|
+
const p = points.get(s).find((q) => q.key === k);
|
|
1678
|
+
const fmt = scales[s.axis].format;
|
|
1679
|
+
if (!p)
|
|
1680
|
+
return '';
|
|
1681
|
+
if (s.kind === 'candlestick') {
|
|
1682
|
+
return [p.open, p.high, p.low, p.close].some((v) => v === null)
|
|
1683
|
+
? ''
|
|
1684
|
+
: `${t.open} ${fmt(p.open)}, ${t.high} ${fmt(p.high)}, ${t.low} ${fmt(p.low)}, ${t.close} ${fmt(p.close)}`;
|
|
1685
|
+
}
|
|
1686
|
+
if (s.kind === 'rangeArea')
|
|
1687
|
+
return p.low === null || p.high === null ? '' : `${fmt(p.low)} – ${fmt(p.high)}`;
|
|
1688
|
+
return p.y === null ? '' : fmt(p.y);
|
|
1689
|
+
}),
|
|
1690
|
+
})),
|
|
1691
|
+
},
|
|
1692
|
+
clip: { x: plot.left, y: plot.top - 6, w: plot.width, h: plot.height + 12 },
|
|
1693
|
+
zoomed: d0 > full[0] + span * 1e-9 || d1 < full[1] - span * 1e-9,
|
|
1694
|
+
domain: full,
|
|
1695
|
+
window: [d0, d1],
|
|
1696
|
+
step: minGap,
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
function legendOf(series) {
|
|
1700
|
+
return series.map((s) => ({ index: s.index, label: s.name, color: s.kind === 'candlestick' ? s.upColor : s.color }));
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
let nextId = 0;
|
|
11
1704
|
/**
|
|
12
|
-
* Chart de CaribeNG: gráficos
|
|
13
|
-
*
|
|
1705
|
+
* Chart de CaribeNG: gráficos SVG accesibles y sin dependencias.
|
|
1706
|
+
*
|
|
1707
|
+
* - Cartesianos: `bar`, `horizontalBar`, `line`, `area` y `waterfall` (cascada).
|
|
1708
|
+
* - Circulares: `pie`, `doughnut`, `radialBar` y `gauge` (medidor).
|
|
1709
|
+
* - Radiales: `radar` y `polarArea`. Matrices: `heatmap` y `treemap`.
|
|
1710
|
+
* - `stacked` apila barras y áreas; en `bar`, una serie con `type: 'line'` se dibuja como línea.
|
|
1711
|
+
* - `sparkline` quita ejes, rejilla y leyenda (minigráfico para tarjetas o tablas).
|
|
1712
|
+
* - `valueFormat` y `locale` formatean los valores.
|
|
1713
|
+
* - Tooltip con ratón y teclado (flechas), leyenda que oculta series y una tabla de
|
|
1714
|
+
* datos para lectores de pantalla.
|
|
1715
|
+
*
|
|
1716
|
+
* Cada tipo lo dibuja un renderer (`renderers/`) que devuelve una escena: formas,
|
|
1717
|
+
* zonas de clic y elementos navegables. Este componente solo la pinta.
|
|
1718
|
+
*
|
|
1719
|
+
* @example
|
|
1720
|
+
* ```html
|
|
1721
|
+
* <cb-chart type="bar" [data]="ventas" [valueFormat]="{ style: 'currency', currency: 'COP' }" />
|
|
1722
|
+
* <cb-chart type="radar" [data]="evaluacion" />
|
|
1723
|
+
* <cb-chart type="gauge" [data]="{ labels: ['Cumplimiento'], datasets: [{ label: 'Meta', data: [72] }] }" />
|
|
1724
|
+
* ```
|
|
14
1725
|
*/
|
|
15
1726
|
class CbChart {
|
|
16
1727
|
constructor() {
|
|
1728
|
+
/** Textos de interfaz en el idioma activo (`provideCaribeNG({ locale })`). */
|
|
1729
|
+
this.t = injectCbTranslation();
|
|
1730
|
+
this.instructionsId = `cb-chart-${++nextId}-help`;
|
|
1731
|
+
this.clipId = `cb-chart-${nextId}-clip`;
|
|
17
1732
|
this.type = input('bar', /* @ts-ignore */
|
|
18
1733
|
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
1734
|
+
/**
|
|
1735
|
+
* Datos. En el modo rápido, `{ labels, datasets }`. En el modo composable (con
|
|
1736
|
+
* series hijas como `<cb-chart-line>`), un array de registros.
|
|
1737
|
+
*/
|
|
19
1738
|
this.data = input({ labels: [], datasets: [] }, /* @ts-ignore */
|
|
20
1739
|
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
1740
|
+
/** Alto del gráfico en píxeles; el ancho se adapta al contenedor. */
|
|
21
1741
|
this.height = input(220, /* @ts-ignore */
|
|
22
1742
|
...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
23
1743
|
this.showLegend = input(true, /* @ts-ignore */
|
|
24
1744
|
...(ngDevMode ? [{ debugName: "showLegend" }] : /* istanbul ignore next */ []));
|
|
25
|
-
this.ariaLabel = input(
|
|
1745
|
+
this.ariaLabel = input(undefined, /* @ts-ignore */
|
|
26
1746
|
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
...(ngDevMode ? [{ debugName: "
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1747
|
+
/** Apila las series de barras o áreas. */
|
|
1748
|
+
this.stacked = input(false, { ...(ngDevMode ? { debugName: "stacked" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1749
|
+
/** Minigráfico sin ejes, rejilla ni leyenda. */
|
|
1750
|
+
this.sparkline = input(false, { ...(ngDevMode ? { debugName: "sparkline" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1751
|
+
/** Formato de los valores (ejes, tooltip y tabla). */
|
|
1752
|
+
this.valueFormat = input(undefined, /* @ts-ignore */
|
|
1753
|
+
...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
|
|
1754
|
+
/** Locale BCP 47 para el formato (por defecto, el del navegador). */
|
|
1755
|
+
this.locale = input(undefined, /* @ts-ignore */
|
|
1756
|
+
...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
1757
|
+
/** Valor mínimo del medidor (`gauge`). */
|
|
1758
|
+
this.min = input(0, /* @ts-ignore */
|
|
1759
|
+
...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
1760
|
+
/** Valor que representa el 100 % en `radialBar` y el máximo del medidor (`gauge`). */
|
|
1761
|
+
this.max = input(100, /* @ts-ignore */
|
|
1762
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
1763
|
+
/** Franjas de color del medidor (`gauge`), p. ej. `[{ from: 0, to: 40, color: 'red' }]`. */
|
|
1764
|
+
this.bands = input([], /* @ts-ignore */
|
|
1765
|
+
...(ngDevMode ? [{ debugName: "bands" }] : /* istanbul ignore next */ []));
|
|
1766
|
+
/** Animación de entrada (se desactiva sola con `prefers-reduced-motion`). */
|
|
1767
|
+
this.animate = input(true, { ...(ngDevMode ? { debugName: "animate" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1768
|
+
/** Modo composable: zoom y desplazamiento en el eje X (rueda, arrastre, `+` / `-` / `0`). */
|
|
1769
|
+
this.zoom = input(false, { ...(ngDevMode ? { debugName: "zoom" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1770
|
+
/**
|
|
1771
|
+
* Modo composable: muestra al inicio solo los últimos `xWindow` del eje X (ms en un
|
|
1772
|
+
* eje de tiempo, categorías…). Con datos en tiempo real la vista avanza con ellos.
|
|
1773
|
+
*/
|
|
1774
|
+
this.xWindow = input(undefined, /* @ts-ignore */
|
|
1775
|
+
...(ngDevMode ? [{ debugName: "xWindow" }] : /* istanbul ignore next */ []));
|
|
1776
|
+
/** Clic o Enter sobre una categoría, un segmento o una celda. */
|
|
1777
|
+
this.itemSelect = output();
|
|
1778
|
+
/** Series, ejes y referencias declarados como hijos (modo composable). */
|
|
1779
|
+
this.seriesDefs = contentChildren(CbChartSeries, /* @ts-ignore */
|
|
1780
|
+
...(ngDevMode ? [{ debugName: "seriesDefs" }] : /* istanbul ignore next */ []));
|
|
1781
|
+
this.axisDefs = contentChildren(CbChartAxis, /* @ts-ignore */
|
|
1782
|
+
...(ngDevMode ? [{ debugName: "axisDefs" }] : /* istanbul ignore next */ []));
|
|
1783
|
+
this.lineDefs = contentChildren(CbChartReferenceLine, /* @ts-ignore */
|
|
1784
|
+
...(ngDevMode ? [{ debugName: "lineDefs" }] : /* istanbul ignore next */ []));
|
|
1785
|
+
this.bandDefs = contentChildren(CbChartReferenceBand, /* @ts-ignore */
|
|
1786
|
+
...(ngDevMode ? [{ debugName: "bandDefs" }] : /* istanbul ignore next */ []));
|
|
1787
|
+
this.noteDefs = contentChildren(CbChartAnnotation, /* @ts-ignore */
|
|
1788
|
+
...(ngDevMode ? [{ debugName: "noteDefs" }] : /* istanbul ignore next */ []));
|
|
1789
|
+
/** Posición del elemento activo en `scene().items`. */
|
|
1790
|
+
this.active = signal(null, /* @ts-ignore */
|
|
1791
|
+
...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
1792
|
+
/** Series o elementos ocultos desde la leyenda. */
|
|
1793
|
+
this.hidden = signal(new Set(), /* @ts-ignore */
|
|
1794
|
+
...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
1795
|
+
/** Ventana elegida con zoom (modo composable, sin grupo). */
|
|
1796
|
+
this.localWindow = signal(null, /* @ts-ignore */
|
|
1797
|
+
...(ngDevMode ? [{ debugName: "localWindow" }] : /* istanbul ignore next */ []));
|
|
1798
|
+
/** Ancho real del área del gráfico (px); hasta medirlo, el ancho lógico por defecto. */
|
|
1799
|
+
this.width = signal(W, /* @ts-ignore */
|
|
1800
|
+
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|
|
1801
|
+
this.plotRef = viewChild.required('plot');
|
|
1802
|
+
this.dragging = null;
|
|
1803
|
+
this.group = inject(CbChartGroupState, { optional: true });
|
|
1804
|
+
this.document = inject(DOCUMENT);
|
|
1805
|
+
this.host = inject(ElementRef);
|
|
1806
|
+
this.name = computed(() => this.ariaLabel() ?? this.t().chart, /* @ts-ignore */
|
|
1807
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
1808
|
+
this.composed = computed(() => this.seriesDefs().length > 0, /* @ts-ignore */
|
|
1809
|
+
...(ngDevMode ? [{ debugName: "composed" }] : /* istanbul ignore next */ []));
|
|
1810
|
+
this.zoomEnabled = computed(() => this.zoom() && this.composed() && !this.sparkline(), /* @ts-ignore */
|
|
1811
|
+
...(ngDevMode ? [{ debugName: "zoomEnabled" }] : /* istanbul ignore next */ []));
|
|
1812
|
+
/** Datos del modo rápido (en el composable, `data` es un array de registros). */
|
|
1813
|
+
this.chartData = computed(() => {
|
|
1814
|
+
const data = this.data();
|
|
1815
|
+
return Array.isArray(data) ? { labels: [], datasets: [] } : data;
|
|
37
1816
|
}, /* @ts-ignore */
|
|
38
|
-
...(ngDevMode ? [{ debugName: "
|
|
39
|
-
this.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const plotWidth = 440;
|
|
45
|
-
const step = plotWidth / labels.length;
|
|
46
|
-
return labels.map((text, index) => ({
|
|
47
|
-
index,
|
|
48
|
-
text,
|
|
49
|
-
x: plotLeft + step * index + step / 2,
|
|
50
|
-
}));
|
|
1817
|
+
...(ngDevMode ? [{ debugName: "chartData" }] : /* istanbul ignore next */ []));
|
|
1818
|
+
this.isEmpty = computed(() => {
|
|
1819
|
+
if (this.composed())
|
|
1820
|
+
return !this.composedSpec().series.some((s) => s.records.length);
|
|
1821
|
+
const { labels, datasets } = this.chartData();
|
|
1822
|
+
return !datasets.length || (this.type() !== 'gauge' && !labels.length);
|
|
51
1823
|
}, /* @ts-ignore */
|
|
52
|
-
...(ngDevMode ? [{ debugName: "
|
|
53
|
-
this.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
labels.forEach((lbl, li) => {
|
|
67
|
-
const groupStart = plotLeft + groupWidth * li + (groupWidth - barWidth * datasets.length) / 2;
|
|
68
|
-
datasets.forEach((ds, di) => {
|
|
69
|
-
const val = Math.max(0, ds.data[li] ?? 0);
|
|
70
|
-
const h = (val / max) * plotHeight;
|
|
71
|
-
const color = ds.color ?? DEFAULT_PALETTE[di % DEFAULT_PALETTE.length];
|
|
72
|
-
result.push({
|
|
73
|
-
key: `${di}-${li}`,
|
|
74
|
-
x: Number((groupStart + di * barWidth).toFixed(2)),
|
|
75
|
-
y: Number((plotBottom - h).toFixed(2)),
|
|
76
|
-
w: Number(Math.max(4, barWidth - 3).toFixed(2)),
|
|
77
|
-
h: Number(Math.max(2, h).toFixed(2)),
|
|
78
|
-
color,
|
|
79
|
-
title: `${ds.label} (${lbl}): ${val}`,
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
return result;
|
|
84
|
-
}, /* @ts-ignore */
|
|
85
|
-
...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
|
|
86
|
-
this.lines = computed(() => {
|
|
87
|
-
const { labels, datasets } = this.data();
|
|
88
|
-
if (!labels.length || !datasets.length)
|
|
89
|
-
return [];
|
|
90
|
-
const plotTop = 18;
|
|
91
|
-
const plotBottom = Math.max(60, this.height() - 32);
|
|
92
|
-
const plotHeight = plotBottom - plotTop;
|
|
93
|
-
const plotLeft = 48;
|
|
94
|
-
const plotWidth = 440;
|
|
95
|
-
const step = plotWidth / labels.length;
|
|
96
|
-
const max = this.maxValue();
|
|
97
|
-
return datasets.map((ds, di) => {
|
|
98
|
-
const color = ds.color ?? DEFAULT_PALETTE[di % DEFAULT_PALETTE.length];
|
|
99
|
-
const points = labels.map((lbl, li) => {
|
|
100
|
-
const val = Math.max(0, ds.data[li] ?? 0);
|
|
101
|
-
const x = Number((plotLeft + step * li + step / 2).toFixed(2));
|
|
102
|
-
const y = Number((plotBottom - (val / max) * plotHeight).toFixed(2));
|
|
103
|
-
return {
|
|
104
|
-
key: `${di}-${li}`,
|
|
105
|
-
x,
|
|
106
|
-
y,
|
|
107
|
-
title: `${ds.label} (${lbl}): ${val}`,
|
|
108
|
-
};
|
|
109
|
-
});
|
|
1824
|
+
...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
|
|
1825
|
+
this.numberFormat = computed(() => this.formatter(this.valueFormat()), /* @ts-ignore */
|
|
1826
|
+
...(ngDevMode ? [{ debugName: "numberFormat" }] : /* istanbul ignore next */ []));
|
|
1827
|
+
this.percentFormat = computed(() => new Intl.NumberFormat(this.locale(), { style: 'percent', maximumFractionDigits: 1 }).format, /* @ts-ignore */
|
|
1828
|
+
...(ngDevMode ? [{ debugName: "percentFormat" }] : /* istanbul ignore next */ []));
|
|
1829
|
+
/** Lo declarado en los hijos, como datos planos para el renderer. */
|
|
1830
|
+
this.composedSpec = computed(() => {
|
|
1831
|
+
const defs = this.seriesDefs();
|
|
1832
|
+
if (!defs.length)
|
|
1833
|
+
return undefined;
|
|
1834
|
+
const shared = this.data();
|
|
1835
|
+
const records = Array.isArray(shared) ? shared : [];
|
|
1836
|
+
const series = defs.map((d, index) => {
|
|
1837
|
+
const field = (name) => (name in d ? d[name]() : name);
|
|
110
1838
|
return {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1839
|
+
kind: d.kind,
|
|
1840
|
+
index,
|
|
1841
|
+
name: d.name() || `${index + 1}`,
|
|
1842
|
+
color: d.color() ?? paletteColor(index),
|
|
1843
|
+
axis: d.axis(),
|
|
1844
|
+
records: d.data() ?? records,
|
|
1845
|
+
x: d.x(),
|
|
1846
|
+
y: field('y'),
|
|
1847
|
+
low: field('low'),
|
|
1848
|
+
high: field('high'),
|
|
1849
|
+
open: field('open'),
|
|
1850
|
+
close: field('close'),
|
|
1851
|
+
curve: 'curve' in d ? d.curve() : 'linear',
|
|
1852
|
+
dashed: 'dashed' in d ? d.dashed() : false,
|
|
1853
|
+
points: 'points' in d ? d.points() : true,
|
|
1854
|
+
stack: 'stack' in d ? d.stack() : undefined,
|
|
1855
|
+
size: 'size' in d ? d.size() : 5,
|
|
1856
|
+
variant: 'variant' in d ? d.variant() : 'candle',
|
|
1857
|
+
upColor: 'upColor' in d ? d.upColor() : '',
|
|
1858
|
+
downColor: 'downColor' in d ? d.downColor() : '',
|
|
115
1859
|
};
|
|
116
1860
|
});
|
|
1861
|
+
return {
|
|
1862
|
+
series,
|
|
1863
|
+
axes: this.axisDefs().map((a) => ({
|
|
1864
|
+
position: a.position(),
|
|
1865
|
+
type: a.type(),
|
|
1866
|
+
min: a.min(),
|
|
1867
|
+
max: a.max(),
|
|
1868
|
+
label: a.label(),
|
|
1869
|
+
format: a.format() === undefined ? undefined : this.formatter(a.format()),
|
|
1870
|
+
grid: a.grid(),
|
|
1871
|
+
})),
|
|
1872
|
+
refs: [
|
|
1873
|
+
...this.bandDefs().map((b) => ({ kind: 'band', axis: b.axis(), from: b.from(), to: b.to(), label: b.label(), color: b.color() })),
|
|
1874
|
+
...this.lineDefs().map((l) => ({ kind: 'line', axis: l.axis(), value: l.value(), label: l.label(), color: l.color() })),
|
|
1875
|
+
...this.noteDefs().map((n) => ({ kind: 'note', axis: n.axis(), value: n.x(), y: n.y(), label: n.label(), color: n.color() })),
|
|
1876
|
+
],
|
|
1877
|
+
window: this.zoomEnabled() ? this.window() : null,
|
|
1878
|
+
xWindow: this.xWindow(),
|
|
1879
|
+
};
|
|
117
1880
|
}, /* @ts-ignore */
|
|
118
|
-
...(ngDevMode ? [{ debugName: "
|
|
119
|
-
this.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
color: DEFAULT_PALETTE[idx % DEFAULT_PALETTE.length],
|
|
139
|
-
dashArray: `${len.toFixed(2)} ${(circumference - len).toFixed(2)}`,
|
|
140
|
-
dashOffset: offset.toFixed(2),
|
|
141
|
-
title: `${lbl}: ${val}`,
|
|
142
|
-
};
|
|
143
|
-
});
|
|
1881
|
+
...(ngDevMode ? [{ debugName: "composedSpec" }] : /* istanbul ignore next */ []));
|
|
1882
|
+
this.context = computed(() => {
|
|
1883
|
+
const t = this.t();
|
|
1884
|
+
return {
|
|
1885
|
+
type: this.type(),
|
|
1886
|
+
data: this.chartData(),
|
|
1887
|
+
width: this.width(),
|
|
1888
|
+
height: this.height(),
|
|
1889
|
+
sparkline: this.sparkline(),
|
|
1890
|
+
stacked: this.stacked(),
|
|
1891
|
+
hidden: this.hidden(),
|
|
1892
|
+
min: this.min(),
|
|
1893
|
+
max: this.max(),
|
|
1894
|
+
bands: this.bands(),
|
|
1895
|
+
format: this.numberFormat(),
|
|
1896
|
+
percent: this.percentFormat(),
|
|
1897
|
+
locale: this.locale(),
|
|
1898
|
+
text: { total: t.chartTotal, change: t.chartChange, open: t.chartOpen, high: t.chartHigh, low: t.chartLow, close: t.chartClose },
|
|
1899
|
+
composed: this.composedSpec(),
|
|
1900
|
+
};
|
|
144
1901
|
}, /* @ts-ignore */
|
|
145
|
-
...(ngDevMode ? [{ debugName: "
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (this.
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return datasets.map((ds, idx) => ({
|
|
155
|
-
label: ds.label,
|
|
156
|
-
color: ds.color ?? DEFAULT_PALETTE[idx % DEFAULT_PALETTE.length],
|
|
157
|
-
}));
|
|
1902
|
+
...(ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []));
|
|
1903
|
+
/** Lo que se dibuja: lo calcula el renderer del tipo (o el composable si hay series hijas). */
|
|
1904
|
+
this.scene = computed(() => {
|
|
1905
|
+
if (this.isEmpty())
|
|
1906
|
+
return emptyScene();
|
|
1907
|
+
if (this.composed())
|
|
1908
|
+
return renderComposed(this.context());
|
|
1909
|
+
const render = CHART_RENDERERS[this.type()] ?? CHART_RENDERERS.bar;
|
|
1910
|
+
return render(this.context());
|
|
158
1911
|
}, /* @ts-ignore */
|
|
159
|
-
...(ngDevMode ? [{ debugName: "
|
|
1912
|
+
...(ngDevMode ? [{ debugName: "scene" }] : /* istanbul ignore next */ []));
|
|
1913
|
+
/**
|
|
1914
|
+
* Elemento resaltado: el activo de este gráfico o, en un grupo, el del mismo
|
|
1915
|
+
* valor de X activo en otra gráfica.
|
|
1916
|
+
*/
|
|
1917
|
+
this.shown = computed(() => {
|
|
1918
|
+
const local = this.active();
|
|
1919
|
+
if (local !== null)
|
|
1920
|
+
return local;
|
|
1921
|
+
const key = this.group?.key();
|
|
1922
|
+
if (key === null || key === undefined)
|
|
1923
|
+
return null;
|
|
1924
|
+
const i = this.scene().items.findIndex((item) => item.key === key);
|
|
1925
|
+
return i < 0 ? null : i;
|
|
1926
|
+
}, /* @ts-ignore */
|
|
1927
|
+
...(ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []));
|
|
1928
|
+
this.activeItem = computed(() => {
|
|
1929
|
+
const i = this.shown();
|
|
1930
|
+
return i === null ? null : (this.scene().items[i] ?? null);
|
|
1931
|
+
}, /* @ts-ignore */
|
|
1932
|
+
...(ngDevMode ? [{ debugName: "activeItem" }] : /* istanbul ignore next */ []));
|
|
1933
|
+
this.tooltip = computed(() => {
|
|
1934
|
+
const item = this.activeItem();
|
|
1935
|
+
if (!item)
|
|
1936
|
+
return null;
|
|
1937
|
+
return {
|
|
1938
|
+
title: item.title,
|
|
1939
|
+
rows: item.rows,
|
|
1940
|
+
left: clamp((item.x / this.width()) * 100, 12, 88),
|
|
1941
|
+
top: (item.y / this.height()) * 100,
|
|
1942
|
+
};
|
|
1943
|
+
}, /* @ts-ignore */
|
|
1944
|
+
...(ngDevMode ? [{ debugName: "tooltip" }] : /* istanbul ignore next */ []));
|
|
1945
|
+
// --- Zoom (modo composable) ----------------------------------------------------------
|
|
1946
|
+
/** Ventana elegida con zoom: la del grupo si lo hay, si no la propia. */
|
|
1947
|
+
this.window = computed(() => (this.group ? this.group.window() : this.localWindow()), /* @ts-ignore */
|
|
1948
|
+
...(ngDevMode ? [{ debugName: "window" }] : /* istanbul ignore next */ []));
|
|
1949
|
+
/** Lo que anuncia el lector de pantalla al recorrer el gráfico con el teclado (solo el propio). */
|
|
1950
|
+
this.announcement = computed(() => {
|
|
1951
|
+
const i = this.active();
|
|
1952
|
+
const item = i === null ? null : this.scene().items[i];
|
|
1953
|
+
if (!item)
|
|
1954
|
+
return '';
|
|
1955
|
+
return `${item.title}: ${item.rows.map((r) => (r.label && r.value ? `${r.label} ${r.value}` : r.label || r.value)).join(', ')}`;
|
|
1956
|
+
}, /* @ts-ignore */
|
|
1957
|
+
...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
|
|
1958
|
+
const destroyRef = inject(DestroyRef);
|
|
1959
|
+
afterNextRender(() => {
|
|
1960
|
+
if (typeof ResizeObserver === 'undefined')
|
|
1961
|
+
return;
|
|
1962
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
1963
|
+
const w = Math.round(entry.contentRect.width);
|
|
1964
|
+
if (w > 0)
|
|
1965
|
+
this.width.set(w);
|
|
1966
|
+
});
|
|
1967
|
+
observer.observe(this.plotRef().nativeElement);
|
|
1968
|
+
destroyRef.onDestroy(() => observer.disconnect());
|
|
1969
|
+
});
|
|
1970
|
+
// En un grupo, el valor de X activo aquí se comparte con las demás gráficas.
|
|
1971
|
+
effect(() => {
|
|
1972
|
+
const i = this.active();
|
|
1973
|
+
const group = this.group;
|
|
1974
|
+
if (!group)
|
|
1975
|
+
return;
|
|
1976
|
+
untracked(() => group.key.set(i === null ? null : (this.scene().items[i]?.key ?? null)));
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
formatter(vf) {
|
|
1980
|
+
return typeof vf === 'function' ? vf : new Intl.NumberFormat(this.locale(), vf ?? { maximumFractionDigits: 2 }).format;
|
|
1981
|
+
}
|
|
1982
|
+
setWindow(window) {
|
|
1983
|
+
this.active.set(null);
|
|
1984
|
+
if (this.group)
|
|
1985
|
+
this.group.window.set(window);
|
|
1986
|
+
else
|
|
1987
|
+
this.localWindow.set(window);
|
|
1988
|
+
}
|
|
1989
|
+
/** Acerca (`factor` < 1) o aleja la ventana alrededor de `anchor` (0–1 dentro del área). */
|
|
1990
|
+
zoomBy(factor, anchor = 0.5) {
|
|
1991
|
+
const { domain, window, step } = this.scene();
|
|
1992
|
+
if (!this.zoomEnabled() || !domain || !window)
|
|
1993
|
+
return;
|
|
1994
|
+
const [f0, f1] = domain;
|
|
1995
|
+
const [a, b] = window;
|
|
1996
|
+
const w = b - a;
|
|
1997
|
+
const next = clamp(w * factor, Math.min(f1 - f0, (step ?? (f1 - f0) / 100) * 2), f1 - f0);
|
|
1998
|
+
const start = clamp(a + w * anchor - next * anchor, f0, f1 - next);
|
|
1999
|
+
const end = start + next;
|
|
2000
|
+
this.setWindow({ start, end, pinned: end >= f1 - (f1 - f0) * 1e-6 });
|
|
2001
|
+
}
|
|
2002
|
+
/** Vuelve a la vista inicial (todo, o los últimos `xWindow`). */
|
|
2003
|
+
resetZoom() {
|
|
2004
|
+
this.setWindow(null);
|
|
2005
|
+
}
|
|
2006
|
+
/** Posición del ratón dentro del área de dibujo (0–1). */
|
|
2007
|
+
pointerFraction(event) {
|
|
2008
|
+
const clip = this.scene().clip;
|
|
2009
|
+
const svg = event.currentTarget;
|
|
2010
|
+
const rect = svg.getBoundingClientRect();
|
|
2011
|
+
if (!clip || !rect.width)
|
|
2012
|
+
return 0.5;
|
|
2013
|
+
const x = ((event.clientX - rect.left) * this.width()) / rect.width;
|
|
2014
|
+
return clamp((x - clip.x) / clip.w, 0, 1);
|
|
2015
|
+
}
|
|
2016
|
+
onWheel(event) {
|
|
2017
|
+
if (!this.zoomEnabled() || !event.deltaY)
|
|
2018
|
+
return;
|
|
2019
|
+
event.preventDefault();
|
|
2020
|
+
this.zoomBy(event.deltaY < 0 ? 0.8 : 1.25, this.pointerFraction(event));
|
|
2021
|
+
}
|
|
2022
|
+
onPointerDown(event) {
|
|
2023
|
+
const window = this.scene().window;
|
|
2024
|
+
if (!this.zoomEnabled() || !this.scene().zoomed || !window || event.button !== 0)
|
|
2025
|
+
return;
|
|
2026
|
+
this.dragging = { x: event.clientX, window };
|
|
2027
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
2028
|
+
}
|
|
2029
|
+
onPointerMove(event) {
|
|
2030
|
+
const drag = this.dragging;
|
|
2031
|
+
const { clip, domain } = this.scene();
|
|
2032
|
+
if (!drag || !clip || !domain)
|
|
2033
|
+
return;
|
|
2034
|
+
const svg = event.currentTarget;
|
|
2035
|
+
const scale = this.width() / (svg.getBoundingClientRect().width || this.width());
|
|
2036
|
+
const [a, b] = drag.window;
|
|
2037
|
+
const w = b - a;
|
|
2038
|
+
const shift = (((drag.x - event.clientX) * scale) / clip.w) * w;
|
|
2039
|
+
const start = clamp(a + shift, domain[0], domain[1] - w);
|
|
2040
|
+
this.setWindow({ start, end: start + w, pinned: start + w >= domain[1] - w * 1e-6 });
|
|
2041
|
+
}
|
|
2042
|
+
/** Clases de una forma: atenuada si hay otro elemento activo, resaltada si es el activo. */
|
|
2043
|
+
shapeClass(s) {
|
|
2044
|
+
const active = this.shown();
|
|
2045
|
+
if (s.item === undefined || active === null)
|
|
2046
|
+
return s.cls;
|
|
2047
|
+
if (s.item === active)
|
|
2048
|
+
return `${s.cls} cb-is-active`;
|
|
2049
|
+
return s.k === 'circle' ? s.cls : `${s.cls} cb-is-dim`;
|
|
2050
|
+
}
|
|
2051
|
+
// --- Exportar ------------------------------------------------------------------------
|
|
2052
|
+
/**
|
|
2053
|
+
* El gráfico como SVG autocontenido: estilos y colores resueltos (incluido el tema),
|
|
2054
|
+
* fondo y leyenda. Solo en el navegador.
|
|
2055
|
+
*/
|
|
2056
|
+
toSvg() {
|
|
2057
|
+
const svg = this.plotRef().nativeElement.querySelector('svg');
|
|
2058
|
+
if (!svg)
|
|
2059
|
+
return '';
|
|
2060
|
+
const view = this.document.defaultView;
|
|
2061
|
+
const clone = svg.cloneNode(true);
|
|
2062
|
+
const source = [svg, ...Array.from(svg.querySelectorAll('*'))];
|
|
2063
|
+
const target = [clone, ...Array.from(clone.querySelectorAll('*'))];
|
|
2064
|
+
source.forEach((el, i) => {
|
|
2065
|
+
const style = view?.getComputedStyle(el);
|
|
2066
|
+
const out = target[i];
|
|
2067
|
+
if (!style)
|
|
2068
|
+
return;
|
|
2069
|
+
const props = EXPORT_STYLES.map((p) => `${p}:${style.getPropertyValue(p)}`).filter((d) => !d.endsWith(':'));
|
|
2070
|
+
out.setAttribute('style', `${props.join(';')};animation:none;transition:none`);
|
|
2071
|
+
out.removeAttribute('class');
|
|
2072
|
+
out.removeAttribute('_ngcontent');
|
|
2073
|
+
});
|
|
2074
|
+
clone.querySelectorAll('[data-hit]').forEach((el) => el.remove());
|
|
2075
|
+
// Leyenda y fondo: el SVG exportado se ve igual que la tarjeta.
|
|
2076
|
+
const host = this.host.nativeElement;
|
|
2077
|
+
const hostStyle = view?.getComputedStyle(host);
|
|
2078
|
+
const legend = this.showLegend() && !this.sparkline() ? this.scene().legend.filter((l) => !this.hidden().has(l.index)) : [];
|
|
2079
|
+
const width = this.width();
|
|
2080
|
+
const legendH = legend.length ? 28 : 0;
|
|
2081
|
+
const height = this.height() + legendH;
|
|
2082
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
2083
|
+
clone.setAttribute('xmlns', ns);
|
|
2084
|
+
clone.setAttribute('width', String(width));
|
|
2085
|
+
clone.setAttribute('height', String(height));
|
|
2086
|
+
clone.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
2087
|
+
clone.removeAttribute('aria-hidden');
|
|
2088
|
+
const bg = this.document.createElementNS(ns, 'rect');
|
|
2089
|
+
bg.setAttribute('width', '100%');
|
|
2090
|
+
bg.setAttribute('height', '100%');
|
|
2091
|
+
bg.setAttribute('fill', hostStyle?.backgroundColor || '#fff');
|
|
2092
|
+
clone.insertBefore(bg, clone.firstChild);
|
|
2093
|
+
const text = hostStyle?.color || '#000';
|
|
2094
|
+
const font = hostStyle?.fontFamily || 'sans-serif';
|
|
2095
|
+
let x = 8;
|
|
2096
|
+
for (const item of legend) {
|
|
2097
|
+
const swatch = this.document.createElementNS(ns, 'circle');
|
|
2098
|
+
swatch.setAttribute('cx', String(x + 5));
|
|
2099
|
+
swatch.setAttribute('cy', String(this.height() + 14));
|
|
2100
|
+
swatch.setAttribute('r', '5');
|
|
2101
|
+
swatch.setAttribute('fill', resolveColor(item.color, host, view));
|
|
2102
|
+
const label = this.document.createElementNS(ns, 'text');
|
|
2103
|
+
label.setAttribute('x', String(x + 14));
|
|
2104
|
+
label.setAttribute('y', String(this.height() + 18));
|
|
2105
|
+
label.setAttribute('style', `fill:${text};font:12px ${font}`);
|
|
2106
|
+
label.textContent = item.label;
|
|
2107
|
+
clone.append(swatch, label);
|
|
2108
|
+
x += 26 + item.label.length * 7;
|
|
2109
|
+
}
|
|
2110
|
+
const title = this.document.createElementNS(ns, 'title');
|
|
2111
|
+
title.textContent = this.name();
|
|
2112
|
+
clone.insertBefore(title, clone.firstChild);
|
|
2113
|
+
return new XMLSerializer().serializeToString(clone);
|
|
2114
|
+
}
|
|
2115
|
+
/** El gráfico como imagen PNG (`scale` multiplica la resolución). */
|
|
2116
|
+
toPng(scale = 2) {
|
|
2117
|
+
const svg = this.toSvg();
|
|
2118
|
+
const width = Number(/width="(\d+)"/.exec(svg)?.[1] ?? this.width());
|
|
2119
|
+
const height = Number(/height="(\d+)"/.exec(svg)?.[1] ?? this.height());
|
|
2120
|
+
return new Promise((resolve, reject) => {
|
|
2121
|
+
const image = new Image();
|
|
2122
|
+
const url = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }));
|
|
2123
|
+
image.onload = () => {
|
|
2124
|
+
const canvas = this.document.createElement('canvas');
|
|
2125
|
+
canvas.width = Math.round(width * scale);
|
|
2126
|
+
canvas.height = Math.round(height * scale);
|
|
2127
|
+
const ctx = canvas.getContext('2d');
|
|
2128
|
+
if (!ctx)
|
|
2129
|
+
return reject(new Error('Canvas 2D no disponible'));
|
|
2130
|
+
ctx.scale(scale, scale);
|
|
2131
|
+
ctx.drawImage(image, 0, 0, width, height);
|
|
2132
|
+
URL.revokeObjectURL(url);
|
|
2133
|
+
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('No se pudo generar el PNG'))), 'image/png');
|
|
2134
|
+
};
|
|
2135
|
+
image.onerror = () => {
|
|
2136
|
+
URL.revokeObjectURL(url);
|
|
2137
|
+
reject(new Error('No se pudo dibujar el SVG'));
|
|
2138
|
+
};
|
|
2139
|
+
image.src = url;
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
/** Descarga el gráfico como PNG o SVG. */
|
|
2143
|
+
async download(format = 'png', fileName = 'chart') {
|
|
2144
|
+
const blob = format === 'svg' ? new Blob([this.toSvg()], { type: 'image/svg+xml;charset=utf-8' }) : await this.toPng();
|
|
2145
|
+
const url = URL.createObjectURL(blob);
|
|
2146
|
+
const a = this.document.createElement('a');
|
|
2147
|
+
a.href = url;
|
|
2148
|
+
a.download = `${fileName}.${format}`;
|
|
2149
|
+
a.click();
|
|
2150
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
2151
|
+
}
|
|
2152
|
+
// --- Interacción --------------------------------------------------------------------
|
|
2153
|
+
hover(item) {
|
|
2154
|
+
if (item !== undefined)
|
|
2155
|
+
this.active.set(item);
|
|
2156
|
+
}
|
|
2157
|
+
onFocus() {
|
|
2158
|
+
if (this.active() === null && this.scene().items.length)
|
|
2159
|
+
this.active.set(0);
|
|
2160
|
+
}
|
|
2161
|
+
onKeydown(event) {
|
|
2162
|
+
const count = this.scene().items.length;
|
|
2163
|
+
if (!count)
|
|
2164
|
+
return;
|
|
2165
|
+
const cols = this.scene().columns;
|
|
2166
|
+
const cur = this.active() ?? -1;
|
|
2167
|
+
let next;
|
|
2168
|
+
switch (event.key) {
|
|
2169
|
+
case 'ArrowRight':
|
|
2170
|
+
next = (cur + 1) % count;
|
|
2171
|
+
break;
|
|
2172
|
+
case 'ArrowLeft':
|
|
2173
|
+
next = cur <= 0 ? count - 1 : cur - 1;
|
|
2174
|
+
break;
|
|
2175
|
+
case 'ArrowDown':
|
|
2176
|
+
next = cols ? Math.min(count - 1, cur < 0 ? 0 : cur + cols) : (cur + 1) % count;
|
|
2177
|
+
break;
|
|
2178
|
+
case 'ArrowUp':
|
|
2179
|
+
next = cols ? Math.max(0, cur - cols) : cur <= 0 ? count - 1 : cur - 1;
|
|
2180
|
+
break;
|
|
2181
|
+
case 'Home':
|
|
2182
|
+
next = 0;
|
|
2183
|
+
break;
|
|
2184
|
+
case 'End':
|
|
2185
|
+
next = count - 1;
|
|
2186
|
+
break;
|
|
2187
|
+
case 'Enter':
|
|
2188
|
+
if (cur >= 0)
|
|
2189
|
+
this.select(cur);
|
|
2190
|
+
return;
|
|
2191
|
+
case 'Escape':
|
|
2192
|
+
next = null;
|
|
2193
|
+
break;
|
|
2194
|
+
case '+':
|
|
2195
|
+
case '=':
|
|
2196
|
+
case '-':
|
|
2197
|
+
case '0': {
|
|
2198
|
+
if (!this.zoomEnabled())
|
|
2199
|
+
return;
|
|
2200
|
+
event.preventDefault();
|
|
2201
|
+
if (event.key === '0')
|
|
2202
|
+
return this.resetZoom();
|
|
2203
|
+
const item = this.activeItem();
|
|
2204
|
+
const clip = this.scene().clip;
|
|
2205
|
+
const anchor = item && clip ? clamp((item.x - clip.x) / clip.w, 0, 1) : 0.5;
|
|
2206
|
+
return this.zoomBy(event.key === '-' ? 1.5 : 1 / 1.5, anchor);
|
|
2207
|
+
}
|
|
2208
|
+
default:
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
event.preventDefault();
|
|
2212
|
+
this.active.set(next);
|
|
2213
|
+
}
|
|
2214
|
+
select(position) {
|
|
2215
|
+
const item = position === undefined ? undefined : this.scene().items[position];
|
|
2216
|
+
if (item)
|
|
2217
|
+
this.itemSelect.emit({ index: item.index, label: item.label });
|
|
2218
|
+
}
|
|
2219
|
+
/** Oculta o muestra una serie (o un elemento en los circulares). */
|
|
2220
|
+
toggle(index) {
|
|
2221
|
+
this.active.set(null);
|
|
2222
|
+
this.hidden.update((set) => {
|
|
2223
|
+
const next = new Set(set);
|
|
2224
|
+
if (next.has(index))
|
|
2225
|
+
next.delete(index);
|
|
2226
|
+
else
|
|
2227
|
+
next.add(index);
|
|
2228
|
+
return next;
|
|
2229
|
+
});
|
|
160
2230
|
}
|
|
161
2231
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChart, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
162
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbChart, isStandalone: true, selector: "cb-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "cb-chart" }, ngImport: i0, template: `
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
[attr.y2]="lineY"
|
|
2232
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbChart, isStandalone: true, selector: "cb-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, stacked: { classPropertyName: "stacked", publicName: "stacked", isSignal: true, isRequired: false, transformFunction: null }, sparkline: { classPropertyName: "sparkline", publicName: "sparkline", isSignal: true, isRequired: false, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, bands: { classPropertyName: "bands", publicName: "bands", isSignal: true, isRequired: false, transformFunction: null }, animate: { classPropertyName: "animate", publicName: "animate", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, xWindow: { classPropertyName: "xWindow", publicName: "xWindow", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemSelect: "itemSelect" }, host: { properties: { "class.cb-chart--sparkline": "sparkline()", "class.cb-chart--animate": "animate()" }, classAttribute: "cb-chart" }, queries: [{ propertyName: "seriesDefs", predicate: CbChartSeries, isSignal: true }, { propertyName: "axisDefs", predicate: CbChartAxis, isSignal: true }, { propertyName: "lineDefs", predicate: CbChartReferenceLine, isSignal: true }, { propertyName: "bandDefs", predicate: CbChartReferenceBand, isSignal: true }, { propertyName: "noteDefs", predicate: CbChartAnnotation, isSignal: true }], viewQueries: [{ propertyName: "plotRef", first: true, predicate: ["plot"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
2233
|
+
<ng-template #shapeTpl let-s>
|
|
2234
|
+
@switch (s.k) {
|
|
2235
|
+
@case ('rect') {
|
|
2236
|
+
<svg:rect
|
|
2237
|
+
[attr.class]="shapeClass(s)"
|
|
2238
|
+
[attr.x]="s.x"
|
|
2239
|
+
[attr.y]="s.y"
|
|
2240
|
+
[attr.width]="s.w"
|
|
2241
|
+
[attr.height]="s.h"
|
|
2242
|
+
[attr.rx]="s.rx"
|
|
2243
|
+
[attr.fill]="s.fill"
|
|
2244
|
+
(mouseenter)="hover(s.item)"
|
|
2245
|
+
(click)="select(s.item)"
|
|
177
2246
|
/>
|
|
178
2247
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
2248
|
+
@case ('path') {
|
|
2249
|
+
<svg:path
|
|
2250
|
+
[attr.class]="shapeClass(s)"
|
|
2251
|
+
[attr.d]="s.d"
|
|
2252
|
+
[attr.fill]="s.fill"
|
|
2253
|
+
[attr.stroke]="s.stroke"
|
|
2254
|
+
[attr.pathLength]="s.pathLength"
|
|
2255
|
+
(mouseenter)="hover(s.item)"
|
|
2256
|
+
(click)="select(s.item)"
|
|
2257
|
+
/>
|
|
2258
|
+
}
|
|
2259
|
+
@case ('circle') {
|
|
2260
|
+
<svg:circle
|
|
2261
|
+
[attr.class]="shapeClass(s)"
|
|
2262
|
+
[attr.cx]="s.cx"
|
|
2263
|
+
[attr.cy]="s.cy"
|
|
2264
|
+
[attr.r]="s.item !== undefined && shown() === s.item ? s.rActive : s.r"
|
|
2265
|
+
[attr.fill]="s.fill"
|
|
2266
|
+
/>
|
|
2267
|
+
}
|
|
2268
|
+
@case ('line') {
|
|
2269
|
+
<svg:line [attr.class]="s.cls" [attr.x1]="s.x1" [attr.y1]="s.y1" [attr.x2]="s.x2" [attr.y2]="s.y2" [attr.stroke]="s.stroke" />
|
|
194
2270
|
}
|
|
2271
|
+
@case ('text') {
|
|
2272
|
+
<svg:text
|
|
2273
|
+
[attr.class]="s.cls"
|
|
2274
|
+
[attr.x]="s.x"
|
|
2275
|
+
[attr.y]="s.y"
|
|
2276
|
+
[attr.text-anchor]="s.anchor"
|
|
2277
|
+
[attr.dominant-baseline]="s.baseline"
|
|
2278
|
+
[attr.fill]="s.fill"
|
|
2279
|
+
>{{ s.text }}</svg:text>
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
</ng-template>
|
|
195
2283
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
2284
|
+
<div
|
|
2285
|
+
#plot
|
|
2286
|
+
class="cb-chart__plot"
|
|
2287
|
+
tabindex="0"
|
|
2288
|
+
role="group"
|
|
2289
|
+
[attr.aria-label]="name()"
|
|
2290
|
+
[attr.aria-describedby]="instructionsId"
|
|
2291
|
+
(keydown)="onKeydown($event)"
|
|
2292
|
+
(focus)="onFocus()"
|
|
2293
|
+
(blur)="active.set(null)"
|
|
2294
|
+
>
|
|
2295
|
+
<svg
|
|
2296
|
+
class="cb-chart__svg"
|
|
2297
|
+
[class.cb-chart__svg--pannable]="scene().zoomed"
|
|
2298
|
+
[attr.viewBox]="'0 0 ' + width() + ' ' + height()"
|
|
2299
|
+
aria-hidden="true"
|
|
2300
|
+
(mouseleave)="active.set(null)"
|
|
2301
|
+
(wheel)="onWheel($event)"
|
|
2302
|
+
(pointerdown)="onPointerDown($event)"
|
|
2303
|
+
(pointermove)="onPointerMove($event)"
|
|
2304
|
+
(pointerup)="dragging = null"
|
|
2305
|
+
(pointercancel)="dragging = null"
|
|
2306
|
+
(dblclick)="resetZoom()"
|
|
2307
|
+
>
|
|
2308
|
+
@if (scene().clip; as clip) {
|
|
2309
|
+
<defs>
|
|
2310
|
+
<clipPath [attr.id]="clipId">
|
|
2311
|
+
<rect [attr.x]="clip.x" [attr.y]="clip.y" [attr.width]="clip.w" [attr.height]="clip.h" />
|
|
2312
|
+
</clipPath>
|
|
2313
|
+
</defs>
|
|
2314
|
+
}
|
|
2315
|
+
@if (isEmpty()) {
|
|
2316
|
+
<text class="cb-chart__empty" [attr.x]="width() / 2" [attr.y]="height() / 2" text-anchor="middle">{{ t().noData }}</text>
|
|
2317
|
+
} @else {
|
|
2318
|
+
@for (s of scene().shapes; track $index) {
|
|
2319
|
+
<ng-container *ngTemplateOutlet="shapeTpl; context: { $implicit: s }" />
|
|
2320
|
+
}
|
|
2321
|
+
<g [attr.clip-path]="scene().clip ? 'url(#' + clipId + ')' : null">
|
|
2322
|
+
@for (s of scene().data ?? []; track $index) {
|
|
2323
|
+
<ng-container *ngTemplateOutlet="shapeTpl; context: { $implicit: s }" />
|
|
2324
|
+
}
|
|
2325
|
+
@if (activeItem()?.cursor; as c) {
|
|
2326
|
+
<line class="cb-chart__cursor" [attr.x1]="c.x" [attr.y1]="c.y1" [attr.x2]="c.x" [attr.y2]="c.y2" />
|
|
2327
|
+
}
|
|
2328
|
+
@for (s of scene().front ?? []; track $index) {
|
|
2329
|
+
<ng-container *ngTemplateOutlet="shapeTpl; context: { $implicit: s }" />
|
|
2330
|
+
}
|
|
2331
|
+
</g>
|
|
2332
|
+
@if (scene().center; as center) {
|
|
2333
|
+
<text class="cb-chart__center" [attr.x]="center.x" [attr.y]="center.y" text-anchor="middle" dominant-baseline="central">
|
|
2334
|
+
{{ center.text }}
|
|
2335
|
+
</text>
|
|
2336
|
+
@if (center.sub) {
|
|
2337
|
+
<text class="cb-chart__center-sub" [attr.x]="center.x" [attr.y]="center.y + 22" text-anchor="middle" dominant-baseline="central">
|
|
2338
|
+
{{ center.sub }}
|
|
2339
|
+
</text>
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
<!-- Zonas invisibles por elemento: reciben el ratón y el clic. -->
|
|
2343
|
+
@for (hit of scene().hits; track $index) {
|
|
2344
|
+
@if (hit.k === 'rect') {
|
|
2345
|
+
<rect
|
|
2346
|
+
data-hit
|
|
2347
|
+
class="cb-chart__hit"
|
|
2348
|
+
[attr.x]="hit.x"
|
|
2349
|
+
[attr.y]="hit.y"
|
|
2350
|
+
[attr.width]="hit.w"
|
|
2351
|
+
[attr.height]="hit.h"
|
|
2352
|
+
(mouseenter)="hover(hit.item)"
|
|
2353
|
+
(click)="select(hit.item)"
|
|
2354
|
+
/>
|
|
2355
|
+
} @else {
|
|
2356
|
+
<path data-hit class="cb-chart__hit" [attr.d]="hit.d" (mouseenter)="hover(hit.item)" (click)="select(hit.item)" />
|
|
215
2357
|
}
|
|
216
2358
|
}
|
|
217
2359
|
}
|
|
2360
|
+
</svg>
|
|
218
2361
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
[attr.cy]="height() / 2"
|
|
234
|
-
[attr.r]="circularRadius()"
|
|
235
|
-
fill="transparent"
|
|
236
|
-
[attr.stroke]="seg.color"
|
|
237
|
-
[attr.stroke-width]="type() === 'pie' ? circularRadius() * 2 : 34"
|
|
238
|
-
[attr.stroke-dasharray]="seg.dashArray"
|
|
239
|
-
[attr.stroke-dashoffset]="seg.dashOffset"
|
|
240
|
-
[attr.transform]="'rotate(-90 260 ' + height() / 2 + ')'"
|
|
241
|
-
>
|
|
242
|
-
<title>{{ seg.title }}</title>
|
|
243
|
-
</circle>
|
|
244
|
-
}
|
|
2362
|
+
@if (tooltip(); as tip) {
|
|
2363
|
+
<div class="cb-chart__tooltip" aria-hidden="true" [style.left.%]="tip.left" [style.top.%]="tip.top">
|
|
2364
|
+
<strong class="cb-chart__tooltip-title">{{ tip.title }}</strong>
|
|
2365
|
+
@for (row of tip.rows; track $index) {
|
|
2366
|
+
<span class="cb-chart__tooltip-row">
|
|
2367
|
+
<span class="cb-chart__swatch" [style.background]="row.color"></span>
|
|
2368
|
+
{{ row.label }}
|
|
2369
|
+
<strong class="cb-chart__tooltip-value">{{ row.value }}</strong>
|
|
2370
|
+
</span>
|
|
2371
|
+
}
|
|
2372
|
+
</div>
|
|
2373
|
+
}
|
|
2374
|
+
@if (zoomEnabled() && window()) {
|
|
2375
|
+
<button type="button" class="cb-chart__reset" (click)="resetZoom()">{{ t().chartResetZoom }}</button>
|
|
245
2376
|
}
|
|
246
|
-
</
|
|
2377
|
+
</div>
|
|
247
2378
|
|
|
248
|
-
@if (showLegend() &&
|
|
249
|
-
<ul class="cb-chart__legend" aria-label="
|
|
250
|
-
@for (item of
|
|
251
|
-
<li
|
|
252
|
-
<
|
|
253
|
-
|
|
2379
|
+
@if (showLegend() && !sparkline() && scene().legend.length) {
|
|
2380
|
+
<ul class="cb-chart__legend" [attr.aria-label]="t().chartLegend">
|
|
2381
|
+
@for (item of scene().legend; track item.index) {
|
|
2382
|
+
<li>
|
|
2383
|
+
<button
|
|
2384
|
+
type="button"
|
|
2385
|
+
class="cb-chart__legend-item"
|
|
2386
|
+
[class.cb-is-hidden]="hidden().has(item.index)"
|
|
2387
|
+
[attr.aria-pressed]="!hidden().has(item.index)"
|
|
2388
|
+
(click)="toggle(item.index)"
|
|
2389
|
+
>
|
|
2390
|
+
<span class="cb-chart__swatch" [style.background]="item.color"></span>
|
|
2391
|
+
{{ item.label }}
|
|
2392
|
+
</button>
|
|
254
2393
|
</li>
|
|
255
2394
|
}
|
|
256
2395
|
</ul>
|
|
257
2396
|
}
|
|
258
|
-
|
|
2397
|
+
|
|
2398
|
+
<span class="cb-chart__sr" [id]="instructionsId">{{ t().chartInstructions }} {{ zoomEnabled() ? t().chartZoomInstructions : '' }}</span>
|
|
2399
|
+
<span class="cb-chart__sr" aria-live="polite" aria-atomic="true">{{ announcement() }}</span>
|
|
2400
|
+
<!-- Los mismos datos como tabla, para lectores de pantalla. -->
|
|
2401
|
+
<table class="cb-chart__sr">
|
|
2402
|
+
<caption>{{ name() }}</caption>
|
|
2403
|
+
@if (scene().table.head; as head) {
|
|
2404
|
+
<thead>
|
|
2405
|
+
<tr>
|
|
2406
|
+
<td></td>
|
|
2407
|
+
@for (col of head; track $index) {
|
|
2408
|
+
<th scope="col">{{ col }}</th>
|
|
2409
|
+
}
|
|
2410
|
+
</tr>
|
|
2411
|
+
</thead>
|
|
2412
|
+
}
|
|
2413
|
+
<tbody>
|
|
2414
|
+
@for (row of scene().table.rows; track $index) {
|
|
2415
|
+
<tr>
|
|
2416
|
+
<th scope="row">{{ row.label }}</th>
|
|
2417
|
+
@for (cell of row.cells; track $index) {
|
|
2418
|
+
<td>{{ cell }}</td>
|
|
2419
|
+
}
|
|
2420
|
+
</tr>
|
|
2421
|
+
}
|
|
2422
|
+
</tbody>
|
|
2423
|
+
</table>
|
|
2424
|
+
`, isInline: true, styles: [":host{display:flex;flex-direction:column;gap:var(--cb-space-3, .75rem);box-sizing:border-box;width:100%;padding:var(--cb-space-4, 1rem);font-family:var(--cb-font-sans, inherit);color:var(--cb-color-text);background:var(--cb-color-surface-raised);border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-lg, .75rem)}:host(.cb-chart--sparkline){padding:0;background:transparent;border:0}.cb-chart__plot{position:relative;border-radius:var(--cb-radius-md, .5rem);outline:none}.cb-chart__plot:focus-visible{box-shadow:0 0 0 2px var(--cb-color-focus-ring)}.cb-chart__svg{display:block;width:100%;height:auto;overflow:visible}.cb-chart__grid{stroke:var(--cb-color-border);stroke-dasharray:3 3}.cb-chart__grid--zero{stroke:var(--cb-color-border-strong);stroke-dasharray:none}.cb-chart__axis{fill:var(--cb-color-text-muted);font-size:11px;font-variant-numeric:tabular-nums}.cb-chart__empty{fill:var(--cb-color-text-muted);font-size:13px}.cb-chart__bar,.cb-chart__slice{transition:opacity var(--cb-duration-fast, .12s) ease}.cb-is-dim{opacity:.35}.cb-chart__line{fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}:host(.cb-chart--sparkline) .cb-chart__line{stroke-width:2}.cb-chart__area{opacity:.22}.cb-chart__point{stroke:var(--cb-color-surface-raised);stroke-width:2}.cb-chart__cursor{stroke:var(--cb-color-border-strong);stroke-dasharray:4 3}.cb-chart__track{fill:var(--cb-color-surface-subtle)}.cb-chart__center{fill:var(--cb-color-text);font-size:20px;font-weight:var(--cb-font-weight-semibold, 600);font-variant-numeric:tabular-nums}.cb-chart__center-sub{fill:var(--cb-color-text-muted);font-size:12px}.cb-chart__hit{fill:transparent;cursor:pointer}.cb-chart__axis--tick{font-size:9px}.cb-chart__baseline,.cb-chart__tick{stroke:var(--cb-color-border-strong)}.cb-chart__axis-title{font-weight:var(--cb-font-weight-semibold, 600)}.cb-chart__ref-line{stroke:var(--cb-color-text-muted);stroke-width:1.25;stroke-dasharray:5 4}.cb-chart__ref-label{fill:var(--cb-color-text-muted);font-size:11px;font-weight:var(--cb-font-weight-medium, 500);paint-order:stroke;stroke:var(--cb-color-surface-raised);stroke-width:3px;stroke-linejoin:round}.cb-chart__ref-band{opacity:.1}.cb-chart__line--dashed{stroke-dasharray:6 4}.cb-chart__line--thin{stroke-width:1.25}.cb-chart__area--range{opacity:.18}.cb-chart__point--scatter{fill-opacity:.75;stroke-width:1;cursor:pointer}.cb-chart__wick{stroke-width:1.25}.cb-chart__candle{transition:opacity var(--cb-duration-fast, .12s) ease}.cb-chart__svg--pannable{cursor:grab;touch-action:pan-y}.cb-chart__svg--pannable:active{cursor:grabbing}.cb-chart__reset{position:absolute;top:0;right:0;padding:.2rem .55rem;font:inherit;font-size:var(--cb-text-xs, .75rem);color:var(--cb-color-text);background:var(--cb-color-surface-raised);border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-sm, .25rem);cursor:pointer}.cb-chart__reset:hover{background:var(--cb-color-surface-hover)}.cb-chart__reset:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-chart__note-dot{stroke:var(--cb-color-surface-raised);stroke-width:2}.cb-chart__note{fill:var(--cb-color-text);font-size:11px;font-weight:var(--cb-font-weight-semibold, 600);paint-order:stroke;stroke:var(--cb-color-surface-raised);stroke-width:4px;stroke-linejoin:round;pointer-events:none}.cb-chart__connector{stroke:var(--cb-color-border-strong);stroke-dasharray:3 3}.cb-chart__band{opacity:.85}.cb-chart__ring{fill:none;stroke:var(--cb-color-surface-raised);stroke-opacity:.7;pointer-events:none}.cb-chart__slice--polar{stroke:var(--cb-color-surface-raised);stroke-width:1.5}.cb-chart__cell{cursor:pointer;transition:opacity var(--cb-duration-fast, .12s) ease}.cb-chart__cell--tree{stroke:var(--cb-color-surface-raised);stroke-width:2}.cb-chart__cell.cb-is-active{stroke:var(--cb-color-text);stroke-width:2}.cb-chart__label{fill:var(--cb-color-text);font-size:11px;font-variant-numeric:tabular-nums;pointer-events:none}.cb-chart__label--inverse{fill:#fff;paint-order:stroke;stroke:#00000059;stroke-width:3px;stroke-linejoin:round}.cb-chart__label--strong{font-size:12px;font-weight:var(--cb-font-weight-semibold, 600)}@media(forced-colors:active){.cb-chart__cell,.cb-chart__bar,.cb-chart__slice{stroke:CanvasText;stroke-width:1}.cb-chart__label,.cb-chart__label--inverse{fill:CanvasText}}.cb-chart__slice{cursor:pointer}.cb-chart__tooltip{position:absolute;z-index:1;display:grid;gap:.15rem;min-width:7rem;padding:var(--cb-space-2, .5rem) var(--cb-space-3, .75rem);font-size:var(--cb-text-xs, .75rem);color:var(--cb-color-text);white-space:nowrap;pointer-events:none;background:var(--cb-color-surface-raised);border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md, .5rem);box-shadow:var(--cb-shadow-md);transform:translate(-50%,calc(-100% - 10px))}.cb-chart__tooltip-title{font-weight:var(--cb-font-weight-semibold, 600)}.cb-chart__tooltip-row{display:flex;align-items:center;gap:.4rem}.cb-chart__tooltip-value{margin-left:auto;padding-left:var(--cb-space-3, .75rem);font-variant-numeric:tabular-nums}.cb-chart__legend{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--cb-space-2, .5rem) var(--cb-space-3, .75rem);margin:0;padding:0;list-style:none}.cb-chart__legend-item{display:inline-flex;align-items:center;gap:.4rem;padding:.15rem .4rem;font:inherit;font-size:var(--cb-text-xs, .75rem);color:var(--cb-color-text-muted);background:none;border:none;border-radius:var(--cb-radius-sm, .25rem);cursor:pointer}.cb-chart__legend-item:hover{background-color:var(--cb-color-surface-hover)}.cb-chart__legend-item:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-chart__legend-item.cb-is-hidden{text-decoration:line-through;opacity:.55}.cb-chart__swatch{flex:none;width:.65rem;height:.65rem;border-radius:999px}.cb-chart__sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}:host(.cb-chart--animate) .cb-chart__bar{transform-box:fill-box;transform-origin:bottom;animation:cb-chart-grow-y .5s var(--cb-ease, ease) both}:host(.cb-chart--animate) .cb-chart__bar--horizontal{transform-origin:left;animation-name:cb-chart-grow-x}:host(.cb-chart--animate) .cb-chart__line{stroke-dasharray:1;animation:cb-chart-draw .7s var(--cb-ease, ease) both}:host(.cb-chart--animate) .cb-chart__area,:host(.cb-chart--animate) .cb-chart__slice,:host(.cb-chart--animate) .cb-chart__cell{animation:cb-chart-fade .5s var(--cb-ease, ease) both}@keyframes cb-chart-grow-y{0%{transform:scaleY(0)}}@keyframes cb-chart-grow-x{0%{transform:scaleX(0)}}@keyframes cb-chart-draw{0%{stroke-dashoffset:1}to{stroke-dashoffset:0}}@keyframes cb-chart-fade{0%{opacity:0}}@media(prefers-reduced-motion:reduce){:host(.cb-chart--animate) .cb-chart__bar,:host(.cb-chart--animate) .cb-chart__line,:host(.cb-chart--animate) .cb-chart__area,:host(.cb-chart--animate) .cb-chart__slice,:host(.cb-chart--animate) .cb-chart__cell{animation:none}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
259
2425
|
}
|
|
260
2426
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChart, decorators: [{
|
|
261
2427
|
type: Component,
|
|
262
|
-
args: [{ selector: 'cb-chart', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
|
|
2428
|
+
args: [{ selector: 'cb-chart', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [NgTemplateOutlet], host: {
|
|
263
2429
|
class: 'cb-chart',
|
|
2430
|
+
'[class.cb-chart--sparkline]': 'sparkline()',
|
|
2431
|
+
'[class.cb-chart--animate]': 'animate()',
|
|
264
2432
|
}, template: `
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
[attr.y2]="lineY"
|
|
2433
|
+
<ng-template #shapeTpl let-s>
|
|
2434
|
+
@switch (s.k) {
|
|
2435
|
+
@case ('rect') {
|
|
2436
|
+
<svg:rect
|
|
2437
|
+
[attr.class]="shapeClass(s)"
|
|
2438
|
+
[attr.x]="s.x"
|
|
2439
|
+
[attr.y]="s.y"
|
|
2440
|
+
[attr.width]="s.w"
|
|
2441
|
+
[attr.height]="s.h"
|
|
2442
|
+
[attr.rx]="s.rx"
|
|
2443
|
+
[attr.fill]="s.fill"
|
|
2444
|
+
(mouseenter)="hover(s.item)"
|
|
2445
|
+
(click)="select(s.item)"
|
|
279
2446
|
/>
|
|
280
2447
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
2448
|
+
@case ('path') {
|
|
2449
|
+
<svg:path
|
|
2450
|
+
[attr.class]="shapeClass(s)"
|
|
2451
|
+
[attr.d]="s.d"
|
|
2452
|
+
[attr.fill]="s.fill"
|
|
2453
|
+
[attr.stroke]="s.stroke"
|
|
2454
|
+
[attr.pathLength]="s.pathLength"
|
|
2455
|
+
(mouseenter)="hover(s.item)"
|
|
2456
|
+
(click)="select(s.item)"
|
|
2457
|
+
/>
|
|
2458
|
+
}
|
|
2459
|
+
@case ('circle') {
|
|
2460
|
+
<svg:circle
|
|
2461
|
+
[attr.class]="shapeClass(s)"
|
|
2462
|
+
[attr.cx]="s.cx"
|
|
2463
|
+
[attr.cy]="s.cy"
|
|
2464
|
+
[attr.r]="s.item !== undefined && shown() === s.item ? s.rActive : s.r"
|
|
2465
|
+
[attr.fill]="s.fill"
|
|
2466
|
+
/>
|
|
2467
|
+
}
|
|
2468
|
+
@case ('line') {
|
|
2469
|
+
<svg:line [attr.class]="s.cls" [attr.x1]="s.x1" [attr.y1]="s.y1" [attr.x2]="s.x2" [attr.y2]="s.y2" [attr.stroke]="s.stroke" />
|
|
296
2470
|
}
|
|
2471
|
+
@case ('text') {
|
|
2472
|
+
<svg:text
|
|
2473
|
+
[attr.class]="s.cls"
|
|
2474
|
+
[attr.x]="s.x"
|
|
2475
|
+
[attr.y]="s.y"
|
|
2476
|
+
[attr.text-anchor]="s.anchor"
|
|
2477
|
+
[attr.dominant-baseline]="s.baseline"
|
|
2478
|
+
[attr.fill]="s.fill"
|
|
2479
|
+
>{{ s.text }}</svg:text>
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
</ng-template>
|
|
297
2483
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
2484
|
+
<div
|
|
2485
|
+
#plot
|
|
2486
|
+
class="cb-chart__plot"
|
|
2487
|
+
tabindex="0"
|
|
2488
|
+
role="group"
|
|
2489
|
+
[attr.aria-label]="name()"
|
|
2490
|
+
[attr.aria-describedby]="instructionsId"
|
|
2491
|
+
(keydown)="onKeydown($event)"
|
|
2492
|
+
(focus)="onFocus()"
|
|
2493
|
+
(blur)="active.set(null)"
|
|
2494
|
+
>
|
|
2495
|
+
<svg
|
|
2496
|
+
class="cb-chart__svg"
|
|
2497
|
+
[class.cb-chart__svg--pannable]="scene().zoomed"
|
|
2498
|
+
[attr.viewBox]="'0 0 ' + width() + ' ' + height()"
|
|
2499
|
+
aria-hidden="true"
|
|
2500
|
+
(mouseleave)="active.set(null)"
|
|
2501
|
+
(wheel)="onWheel($event)"
|
|
2502
|
+
(pointerdown)="onPointerDown($event)"
|
|
2503
|
+
(pointermove)="onPointerMove($event)"
|
|
2504
|
+
(pointerup)="dragging = null"
|
|
2505
|
+
(pointercancel)="dragging = null"
|
|
2506
|
+
(dblclick)="resetZoom()"
|
|
2507
|
+
>
|
|
2508
|
+
@if (scene().clip; as clip) {
|
|
2509
|
+
<defs>
|
|
2510
|
+
<clipPath [attr.id]="clipId">
|
|
2511
|
+
<rect [attr.x]="clip.x" [attr.y]="clip.y" [attr.width]="clip.w" [attr.height]="clip.h" />
|
|
2512
|
+
</clipPath>
|
|
2513
|
+
</defs>
|
|
2514
|
+
}
|
|
2515
|
+
@if (isEmpty()) {
|
|
2516
|
+
<text class="cb-chart__empty" [attr.x]="width() / 2" [attr.y]="height() / 2" text-anchor="middle">{{ t().noData }}</text>
|
|
2517
|
+
} @else {
|
|
2518
|
+
@for (s of scene().shapes; track $index) {
|
|
2519
|
+
<ng-container *ngTemplateOutlet="shapeTpl; context: { $implicit: s }" />
|
|
2520
|
+
}
|
|
2521
|
+
<g [attr.clip-path]="scene().clip ? 'url(#' + clipId + ')' : null">
|
|
2522
|
+
@for (s of scene().data ?? []; track $index) {
|
|
2523
|
+
<ng-container *ngTemplateOutlet="shapeTpl; context: { $implicit: s }" />
|
|
2524
|
+
}
|
|
2525
|
+
@if (activeItem()?.cursor; as c) {
|
|
2526
|
+
<line class="cb-chart__cursor" [attr.x1]="c.x" [attr.y1]="c.y1" [attr.x2]="c.x" [attr.y2]="c.y2" />
|
|
2527
|
+
}
|
|
2528
|
+
@for (s of scene().front ?? []; track $index) {
|
|
2529
|
+
<ng-container *ngTemplateOutlet="shapeTpl; context: { $implicit: s }" />
|
|
2530
|
+
}
|
|
2531
|
+
</g>
|
|
2532
|
+
@if (scene().center; as center) {
|
|
2533
|
+
<text class="cb-chart__center" [attr.x]="center.x" [attr.y]="center.y" text-anchor="middle" dominant-baseline="central">
|
|
2534
|
+
{{ center.text }}
|
|
2535
|
+
</text>
|
|
2536
|
+
@if (center.sub) {
|
|
2537
|
+
<text class="cb-chart__center-sub" [attr.x]="center.x" [attr.y]="center.y + 22" text-anchor="middle" dominant-baseline="central">
|
|
2538
|
+
{{ center.sub }}
|
|
2539
|
+
</text>
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
<!-- Zonas invisibles por elemento: reciben el ratón y el clic. -->
|
|
2543
|
+
@for (hit of scene().hits; track $index) {
|
|
2544
|
+
@if (hit.k === 'rect') {
|
|
2545
|
+
<rect
|
|
2546
|
+
data-hit
|
|
2547
|
+
class="cb-chart__hit"
|
|
2548
|
+
[attr.x]="hit.x"
|
|
2549
|
+
[attr.y]="hit.y"
|
|
2550
|
+
[attr.width]="hit.w"
|
|
2551
|
+
[attr.height]="hit.h"
|
|
2552
|
+
(mouseenter)="hover(hit.item)"
|
|
2553
|
+
(click)="select(hit.item)"
|
|
2554
|
+
/>
|
|
2555
|
+
} @else {
|
|
2556
|
+
<path data-hit class="cb-chart__hit" [attr.d]="hit.d" (mouseenter)="hover(hit.item)" (click)="select(hit.item)" />
|
|
317
2557
|
}
|
|
318
2558
|
}
|
|
319
2559
|
}
|
|
2560
|
+
</svg>
|
|
320
2561
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
[attr.cy]="height() / 2"
|
|
336
|
-
[attr.r]="circularRadius()"
|
|
337
|
-
fill="transparent"
|
|
338
|
-
[attr.stroke]="seg.color"
|
|
339
|
-
[attr.stroke-width]="type() === 'pie' ? circularRadius() * 2 : 34"
|
|
340
|
-
[attr.stroke-dasharray]="seg.dashArray"
|
|
341
|
-
[attr.stroke-dashoffset]="seg.dashOffset"
|
|
342
|
-
[attr.transform]="'rotate(-90 260 ' + height() / 2 + ')'"
|
|
343
|
-
>
|
|
344
|
-
<title>{{ seg.title }}</title>
|
|
345
|
-
</circle>
|
|
346
|
-
}
|
|
2562
|
+
@if (tooltip(); as tip) {
|
|
2563
|
+
<div class="cb-chart__tooltip" aria-hidden="true" [style.left.%]="tip.left" [style.top.%]="tip.top">
|
|
2564
|
+
<strong class="cb-chart__tooltip-title">{{ tip.title }}</strong>
|
|
2565
|
+
@for (row of tip.rows; track $index) {
|
|
2566
|
+
<span class="cb-chart__tooltip-row">
|
|
2567
|
+
<span class="cb-chart__swatch" [style.background]="row.color"></span>
|
|
2568
|
+
{{ row.label }}
|
|
2569
|
+
<strong class="cb-chart__tooltip-value">{{ row.value }}</strong>
|
|
2570
|
+
</span>
|
|
2571
|
+
}
|
|
2572
|
+
</div>
|
|
2573
|
+
}
|
|
2574
|
+
@if (zoomEnabled() && window()) {
|
|
2575
|
+
<button type="button" class="cb-chart__reset" (click)="resetZoom()">{{ t().chartResetZoom }}</button>
|
|
347
2576
|
}
|
|
348
|
-
</
|
|
2577
|
+
</div>
|
|
349
2578
|
|
|
350
|
-
@if (showLegend() &&
|
|
351
|
-
<ul class="cb-chart__legend" aria-label="
|
|
352
|
-
@for (item of
|
|
353
|
-
<li
|
|
354
|
-
<
|
|
355
|
-
|
|
2579
|
+
@if (showLegend() && !sparkline() && scene().legend.length) {
|
|
2580
|
+
<ul class="cb-chart__legend" [attr.aria-label]="t().chartLegend">
|
|
2581
|
+
@for (item of scene().legend; track item.index) {
|
|
2582
|
+
<li>
|
|
2583
|
+
<button
|
|
2584
|
+
type="button"
|
|
2585
|
+
class="cb-chart__legend-item"
|
|
2586
|
+
[class.cb-is-hidden]="hidden().has(item.index)"
|
|
2587
|
+
[attr.aria-pressed]="!hidden().has(item.index)"
|
|
2588
|
+
(click)="toggle(item.index)"
|
|
2589
|
+
>
|
|
2590
|
+
<span class="cb-chart__swatch" [style.background]="item.color"></span>
|
|
2591
|
+
{{ item.label }}
|
|
2592
|
+
</button>
|
|
356
2593
|
</li>
|
|
357
2594
|
}
|
|
358
2595
|
</ul>
|
|
359
2596
|
}
|
|
360
|
-
|
|
361
|
-
|
|
2597
|
+
|
|
2598
|
+
<span class="cb-chart__sr" [id]="instructionsId">{{ t().chartInstructions }} {{ zoomEnabled() ? t().chartZoomInstructions : '' }}</span>
|
|
2599
|
+
<span class="cb-chart__sr" aria-live="polite" aria-atomic="true">{{ announcement() }}</span>
|
|
2600
|
+
<!-- Los mismos datos como tabla, para lectores de pantalla. -->
|
|
2601
|
+
<table class="cb-chart__sr">
|
|
2602
|
+
<caption>{{ name() }}</caption>
|
|
2603
|
+
@if (scene().table.head; as head) {
|
|
2604
|
+
<thead>
|
|
2605
|
+
<tr>
|
|
2606
|
+
<td></td>
|
|
2607
|
+
@for (col of head; track $index) {
|
|
2608
|
+
<th scope="col">{{ col }}</th>
|
|
2609
|
+
}
|
|
2610
|
+
</tr>
|
|
2611
|
+
</thead>
|
|
2612
|
+
}
|
|
2613
|
+
<tbody>
|
|
2614
|
+
@for (row of scene().table.rows; track $index) {
|
|
2615
|
+
<tr>
|
|
2616
|
+
<th scope="row">{{ row.label }}</th>
|
|
2617
|
+
@for (cell of row.cells; track $index) {
|
|
2618
|
+
<td>{{ cell }}</td>
|
|
2619
|
+
}
|
|
2620
|
+
</tr>
|
|
2621
|
+
}
|
|
2622
|
+
</tbody>
|
|
2623
|
+
</table>
|
|
2624
|
+
`, styles: [":host{display:flex;flex-direction:column;gap:var(--cb-space-3, .75rem);box-sizing:border-box;width:100%;padding:var(--cb-space-4, 1rem);font-family:var(--cb-font-sans, inherit);color:var(--cb-color-text);background:var(--cb-color-surface-raised);border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-lg, .75rem)}:host(.cb-chart--sparkline){padding:0;background:transparent;border:0}.cb-chart__plot{position:relative;border-radius:var(--cb-radius-md, .5rem);outline:none}.cb-chart__plot:focus-visible{box-shadow:0 0 0 2px var(--cb-color-focus-ring)}.cb-chart__svg{display:block;width:100%;height:auto;overflow:visible}.cb-chart__grid{stroke:var(--cb-color-border);stroke-dasharray:3 3}.cb-chart__grid--zero{stroke:var(--cb-color-border-strong);stroke-dasharray:none}.cb-chart__axis{fill:var(--cb-color-text-muted);font-size:11px;font-variant-numeric:tabular-nums}.cb-chart__empty{fill:var(--cb-color-text-muted);font-size:13px}.cb-chart__bar,.cb-chart__slice{transition:opacity var(--cb-duration-fast, .12s) ease}.cb-is-dim{opacity:.35}.cb-chart__line{fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}:host(.cb-chart--sparkline) .cb-chart__line{stroke-width:2}.cb-chart__area{opacity:.22}.cb-chart__point{stroke:var(--cb-color-surface-raised);stroke-width:2}.cb-chart__cursor{stroke:var(--cb-color-border-strong);stroke-dasharray:4 3}.cb-chart__track{fill:var(--cb-color-surface-subtle)}.cb-chart__center{fill:var(--cb-color-text);font-size:20px;font-weight:var(--cb-font-weight-semibold, 600);font-variant-numeric:tabular-nums}.cb-chart__center-sub{fill:var(--cb-color-text-muted);font-size:12px}.cb-chart__hit{fill:transparent;cursor:pointer}.cb-chart__axis--tick{font-size:9px}.cb-chart__baseline,.cb-chart__tick{stroke:var(--cb-color-border-strong)}.cb-chart__axis-title{font-weight:var(--cb-font-weight-semibold, 600)}.cb-chart__ref-line{stroke:var(--cb-color-text-muted);stroke-width:1.25;stroke-dasharray:5 4}.cb-chart__ref-label{fill:var(--cb-color-text-muted);font-size:11px;font-weight:var(--cb-font-weight-medium, 500);paint-order:stroke;stroke:var(--cb-color-surface-raised);stroke-width:3px;stroke-linejoin:round}.cb-chart__ref-band{opacity:.1}.cb-chart__line--dashed{stroke-dasharray:6 4}.cb-chart__line--thin{stroke-width:1.25}.cb-chart__area--range{opacity:.18}.cb-chart__point--scatter{fill-opacity:.75;stroke-width:1;cursor:pointer}.cb-chart__wick{stroke-width:1.25}.cb-chart__candle{transition:opacity var(--cb-duration-fast, .12s) ease}.cb-chart__svg--pannable{cursor:grab;touch-action:pan-y}.cb-chart__svg--pannable:active{cursor:grabbing}.cb-chart__reset{position:absolute;top:0;right:0;padding:.2rem .55rem;font:inherit;font-size:var(--cb-text-xs, .75rem);color:var(--cb-color-text);background:var(--cb-color-surface-raised);border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-sm, .25rem);cursor:pointer}.cb-chart__reset:hover{background:var(--cb-color-surface-hover)}.cb-chart__reset:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-chart__note-dot{stroke:var(--cb-color-surface-raised);stroke-width:2}.cb-chart__note{fill:var(--cb-color-text);font-size:11px;font-weight:var(--cb-font-weight-semibold, 600);paint-order:stroke;stroke:var(--cb-color-surface-raised);stroke-width:4px;stroke-linejoin:round;pointer-events:none}.cb-chart__connector{stroke:var(--cb-color-border-strong);stroke-dasharray:3 3}.cb-chart__band{opacity:.85}.cb-chart__ring{fill:none;stroke:var(--cb-color-surface-raised);stroke-opacity:.7;pointer-events:none}.cb-chart__slice--polar{stroke:var(--cb-color-surface-raised);stroke-width:1.5}.cb-chart__cell{cursor:pointer;transition:opacity var(--cb-duration-fast, .12s) ease}.cb-chart__cell--tree{stroke:var(--cb-color-surface-raised);stroke-width:2}.cb-chart__cell.cb-is-active{stroke:var(--cb-color-text);stroke-width:2}.cb-chart__label{fill:var(--cb-color-text);font-size:11px;font-variant-numeric:tabular-nums;pointer-events:none}.cb-chart__label--inverse{fill:#fff;paint-order:stroke;stroke:#00000059;stroke-width:3px;stroke-linejoin:round}.cb-chart__label--strong{font-size:12px;font-weight:var(--cb-font-weight-semibold, 600)}@media(forced-colors:active){.cb-chart__cell,.cb-chart__bar,.cb-chart__slice{stroke:CanvasText;stroke-width:1}.cb-chart__label,.cb-chart__label--inverse{fill:CanvasText}}.cb-chart__slice{cursor:pointer}.cb-chart__tooltip{position:absolute;z-index:1;display:grid;gap:.15rem;min-width:7rem;padding:var(--cb-space-2, .5rem) var(--cb-space-3, .75rem);font-size:var(--cb-text-xs, .75rem);color:var(--cb-color-text);white-space:nowrap;pointer-events:none;background:var(--cb-color-surface-raised);border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md, .5rem);box-shadow:var(--cb-shadow-md);transform:translate(-50%,calc(-100% - 10px))}.cb-chart__tooltip-title{font-weight:var(--cb-font-weight-semibold, 600)}.cb-chart__tooltip-row{display:flex;align-items:center;gap:.4rem}.cb-chart__tooltip-value{margin-left:auto;padding-left:var(--cb-space-3, .75rem);font-variant-numeric:tabular-nums}.cb-chart__legend{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--cb-space-2, .5rem) var(--cb-space-3, .75rem);margin:0;padding:0;list-style:none}.cb-chart__legend-item{display:inline-flex;align-items:center;gap:.4rem;padding:.15rem .4rem;font:inherit;font-size:var(--cb-text-xs, .75rem);color:var(--cb-color-text-muted);background:none;border:none;border-radius:var(--cb-radius-sm, .25rem);cursor:pointer}.cb-chart__legend-item:hover{background-color:var(--cb-color-surface-hover)}.cb-chart__legend-item:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-chart__legend-item.cb-is-hidden{text-decoration:line-through;opacity:.55}.cb-chart__swatch{flex:none;width:.65rem;height:.65rem;border-radius:999px}.cb-chart__sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}:host(.cb-chart--animate) .cb-chart__bar{transform-box:fill-box;transform-origin:bottom;animation:cb-chart-grow-y .5s var(--cb-ease, ease) both}:host(.cb-chart--animate) .cb-chart__bar--horizontal{transform-origin:left;animation-name:cb-chart-grow-x}:host(.cb-chart--animate) .cb-chart__line{stroke-dasharray:1;animation:cb-chart-draw .7s var(--cb-ease, ease) both}:host(.cb-chart--animate) .cb-chart__area,:host(.cb-chart--animate) .cb-chart__slice,:host(.cb-chart--animate) .cb-chart__cell{animation:cb-chart-fade .5s var(--cb-ease, ease) both}@keyframes cb-chart-grow-y{0%{transform:scaleY(0)}}@keyframes cb-chart-grow-x{0%{transform:scaleX(0)}}@keyframes cb-chart-draw{0%{stroke-dashoffset:1}to{stroke-dashoffset:0}}@keyframes cb-chart-fade{0%{opacity:0}}@media(prefers-reduced-motion:reduce){:host(.cb-chart--animate) .cb-chart__bar,:host(.cb-chart--animate) .cb-chart__line,:host(.cb-chart--animate) .cb-chart__area,:host(.cb-chart--animate) .cb-chart__slice,:host(.cb-chart--animate) .cb-chart__cell{animation:none}}\n"] }]
|
|
2625
|
+
}], ctorParameters: () => [], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], stacked: [{ type: i0.Input, args: [{ isSignal: true, alias: "stacked", required: false }] }], sparkline: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkline", required: false }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], bands: [{ type: i0.Input, args: [{ isSignal: true, alias: "bands", required: false }] }], animate: [{ type: i0.Input, args: [{ isSignal: true, alias: "animate", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], xWindow: [{ type: i0.Input, args: [{ isSignal: true, alias: "xWindow", required: false }] }], itemSelect: [{ type: i0.Output, args: ["itemSelect"] }], seriesDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CbChartSeries), { isSignal: true }] }], axisDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CbChartAxis), { isSignal: true }] }], lineDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CbChartReferenceLine), { isSignal: true }] }], bandDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CbChartReferenceBand), { isSignal: true }] }], noteDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CbChartAnnotation), { isSignal: true }] }], plotRef: [{ type: i0.ViewChild, args: ['plot', { isSignal: true }] }] } });
|
|
2626
|
+
/** Propiedades que se copian como estilo en línea al exportar. */
|
|
2627
|
+
const EXPORT_STYLES = [
|
|
2628
|
+
'fill',
|
|
2629
|
+
'fill-opacity',
|
|
2630
|
+
'stroke',
|
|
2631
|
+
'stroke-width',
|
|
2632
|
+
'stroke-opacity',
|
|
2633
|
+
'stroke-dasharray',
|
|
2634
|
+
'stroke-linecap',
|
|
2635
|
+
'stroke-linejoin',
|
|
2636
|
+
'paint-order',
|
|
2637
|
+
'opacity',
|
|
2638
|
+
'font-family',
|
|
2639
|
+
'font-size',
|
|
2640
|
+
'font-weight',
|
|
2641
|
+
'font-variant-numeric',
|
|
2642
|
+
];
|
|
2643
|
+
/** Resuelve un color con variables CSS (`var(--x)`) al valor del tema actual. */
|
|
2644
|
+
function resolveColor(color, host, view) {
|
|
2645
|
+
if (!color.includes('var(') || !view)
|
|
2646
|
+
return color;
|
|
2647
|
+
const probe = host.ownerDocument.createElement('span');
|
|
2648
|
+
probe.style.color = color;
|
|
2649
|
+
host.append(probe);
|
|
2650
|
+
const value = view.getComputedStyle(probe).color;
|
|
2651
|
+
probe.remove();
|
|
2652
|
+
return value || color;
|
|
2653
|
+
}
|
|
2654
|
+
function clamp(n, min, max) {
|
|
2655
|
+
return Math.min(max, Math.max(min, n));
|
|
2656
|
+
}
|
|
362
2657
|
|
|
363
2658
|
/**
|
|
364
2659
|
* Generated bundle index. Do not edit.
|
|
365
2660
|
*/
|
|
366
2661
|
|
|
367
|
-
export { CbChart };
|
|
2662
|
+
export { CbChart, CbChartAnnotation, CbChartArea, CbChartAxis, CbChartBar, CbChartCandlestick, CbChartGroup, CbChartGroupState, CbChartLine, CbChartRangeArea, CbChartReferenceBand, CbChartReferenceLine, CbChartScatter, CbChartSeries };
|