diff --git a/projects/x-framework-components/src/lib/x-chart/x-chart.component.ts b/projects/x-framework-components/src/lib/x-chart/x-chart.component.ts index 78e575f..b0cc797 100644 --- a/projects/x-framework-components/src/lib/x-chart/x-chart.component.ts +++ b/projects/x-framework-components/src/lib/x-chart/x-chart.component.ts @@ -2,8 +2,8 @@ import { Component, ViewChild, ElementRef, - ChangeDetectionStrategy, HostListener, + ChangeDetectionStrategy, } from '@angular/core'; import { nameof, @@ -122,6 +122,40 @@ export class XChartComponent extends XChartContainerComponent { this.registerActionProvider(); } + // + // Available Action Bar Items Changed ... + const isAvailableActionBarItemsChanged = changeKeys.includes( + nameof('availableActionBarItems') + ); + if (isAvailableActionBarItemsChanged) { + // + if (isNullOrUndefined(this.availableActionBarItems)) { + this.availableActionBarItems = this.propertyProvider.availableActionBarItems; + } + + // + await this.prepareActionBar(); + } + + // + // Zoom Step ... + const isZoomStepChanged = changeKeys.includes( + nameof('zoomStep') + ); + if (isZoomStepChanged) { + // + let zoomStep = await this.getValueAsync(this.zoomStep); + if (isNullOrUndefined(zoomStep) || + zoomStep < 0 || + zoomStep > 1 + ) { + this.zoomStep = 0.125; + } + + // + zoomStep = await this.getValueAsync(this.zoomStep); + } + // // Check Auto Update ... var autoUpdate = await this.getValueAsync(this.autoUpdate); @@ -207,47 +241,82 @@ export class XChartComponent extends XChartContainerComponent { } // - switch ( - model.action + switch (model.action) { // - // case this.ActionBarItems.ChangeMode: - // this.handleActionModeFired(model.event); - // break; + // Print ... + case XChartActionBarItem.Print: + await this.handlePrint(); + break; - // // - // case this.ActionBarItems.ChangeTheme: - // this.handleActionThemFired(model.event); - // break; + // + // Export As CSV ... + case XChartActionBarItem.ExportAsCSV: + await this.handleExportAsCSV(); + break; - // // - // case this.ActionBarItems.Close: - // this.closeTab(this.currentTab); - // break; + // + // Export As PNG ... + case XChartActionBarItem.ExportAsPNG: + await this.handleExportAsPng(); + break; - // // - // case this.ActionBarItems.Undo: - // this.undo(); - // break; + // + // Export As SVG ... + case XChartActionBarItem.ExportAsSVG: + await this.handleExportAsSVG(); + break; - // // - // case this.ActionBarItems.Redo: - // this.redo(); - // break; + // + // Zoom In ... + case XChartActionBarItem.ZoomIn: + await this.handleZoomIn(); + break; - // // - // case this.ActionBarItems.Beautify: - // await this.beautifyCode(); - // break; + // + // Zoom Out ... + case XChartActionBarItem.ZoomOut: + await this.handleZoomOut(); + break; - // // - // case this.ActionBarItems.Save: - // // - // this.updateCurrentTabDescriptor({ - // canSave: false, - // }); - // this.prepareActionBar(); - // break; - ) { + // + // Zoom Selection ... + case XChartActionBarItem.ZoomSelection: + break; + + // + // X Selection ... + case XChartActionBarItem.XSelection: + break; + + // + // Y Selection ... + case XChartActionBarItem.YSelection: + break; + + // + // XY Selection ... + case XChartActionBarItem.XYSelection: + break; + + // + // Pan ... + case XChartActionBarItem.Pan: + await this.handlePan(); + break; + + // + // ResetChart ... + case XChartActionBarItem.ResetChart: + await this.handleResetChart(); + break; + + // + // ToggleFullScreen ... + case XChartActionBarItem.ToggleFullScreen: + await this.handleToggleFullscreen(); + break; + + // } // @@ -308,6 +377,222 @@ export class XChartComponent extends XChartContainerComponent { } //#endregion + // + //#region Toolbar Actions ... + async handleExportAsPng() { + // + if (isNullOrUndefined(this.chartInstance)) { + return; + } + + // + await this.export('png'); + } + + async handleExportAsSVG() { + // + if (isNullOrUndefined(this.chartInstance)) { + return; + } + + // + await this.export('svg'); + } + + async handleExportAsCSV() { + // + if (isNullOrUndefined(this.chartInstance)) { + return; + } + + // + await this.export('csv'); + } + + async handleZoomIn() { + // + if (isNullOrUndefined(this.chartInstance)) { + return; + } + + // + var zoomStep = await this.getValueAsync(this.zoomStep); + + // + const state = this.chartInstance.getState(); + const minX = state.minX; + const maxX = state.maxX; + const rangeIn = maxX - minX; + + // + await this.chartInstance.zoomX(minX + rangeIn * zoomStep, maxX - rangeIn * zoomStep); + } + + async handleZoomOut() { + // + if (isNullOrUndefined(this.chartInstance)) { + return; + } + + // + var zoomStep = await this.getValueAsync(this.zoomStep); + + // + const state = this.chartInstance.getState(); + const minX = state.minX; + const maxX = state.maxX; + const rangeIn = maxX - minX; + + // + await this.chartInstance.zoomX(minX - rangeIn * zoomStep, maxX + rangeIn * zoomStep); + } + + async handlePan() { + // + if (isNullOrUndefined(this.chartInstance)) { + return; + } + + // + await this.chartInstance.updateOptions({ + chart: { + zoom: { + enabled: false + }, + selection: { + type: 'x', + enabled: true, + }, + } + }); + } + + async handleResetChart() { + // + if (isNullOrUndefined(this.chartInstance)) { + return; + } + + // + this.chartInstance.resetSeries(true, true); + } + + async handlePrint() { + // + if (isNullOrUndefined(this.chartInstance)) { + return; + } + + // + try { + // + const result = await this.chartInstance.dataURI(); + if ('imgURI' in result) { + // + const imgURI = result.imgURI; + const printWindow = window.open('', '_blank'); + if (printWindow) { + // + // Write Data Uri on a Page for Printing ... + printWindow.document.write(` + + + + Print Chart + + + + Chart + + + + `); + + // + printWindow.document.close(); + } + } + } catch (error) { + } + } + + async handleToggleFullscreen() { + // + if (isNullOrUndefined(this.chartInstance) || + isNullOrUndefined(this.chartContainer) || + isNullOrUndefined(this.chartContainer.nativeElement) + ) { + return; + } + + // + const chartWrapper = this.chartContainer.nativeElement; + const doc = document as any; + + // + // Check if the chart wrapper is currently in fullscreen mode + const isFullscreen = + doc.fullscreenElement === chartWrapper || + doc.msFullscreenElement === chartWrapper || + doc.mozFullScreenElement === chartWrapper || + doc.webkitFullscreenElement === chartWrapper; + if (!isFullscreen) { + // + // Enter Fullscreen + if (chartWrapper.requestFullscreen) { + await chartWrapper.requestFullscreen(); + } else if (chartWrapper.webkitRequestFullscreen) { + await chartWrapper.webkitRequestFullscreen(); + } else if (chartWrapper.mozRequestFullScreen) { + await chartWrapper.mozRequestFullScreen(); + } else if (chartWrapper.msRequestFullscreen) { + await chartWrapper.msRequestFullscreen(); + } + } else { + // + // Exit Fullscreen + if (doc.exitFullscreen) { + await doc.exitFullscreen(); + } else if (doc.webkitExitFullscreen) { + await doc.webkitExitFullscreen(); + } else if (doc.mozCancelFullScreen) { + await doc.mozCancelFullScreen(); + } else if (doc.msExitFullscreen) { + await doc.msExitFullscreen(); + } + } + } + //#endregion + // //#region Private ... private async initializeComponent() { diff --git a/projects/x-framework-components/src/lib/x-chart/x-chart.typings.ts b/projects/x-framework-components/src/lib/x-chart/x-chart.typings.ts index 66c7c0a..73fc3da 100644 --- a/projects/x-framework-components/src/lib/x-chart/x-chart.typings.ts +++ b/projects/x-framework-components/src/lib/x-chart/x-chart.typings.ts @@ -22,27 +22,26 @@ import { XCardActionModel, XComponentCardContainer, IXComponentCardContainer, - XComponentCardContainerComponent, } from '../x-card/x-card.typings'; -import { concatMap, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; +import { + XBaseContainer, + XBaseContainerComponent, +} from '../classes/i-x-base.container'; +import { + XActionBarItem, + XActionBarActionModel, + XActionBarActionFiredModel, + XComponentActionBarContainer, + IXComponentActionBarContainer, +} from '../x-action-bar/x-action-bar.typings'; import { forkJoin, Observable, Subject } from 'rxjs'; import { ViewportRuler } from '@angular/cdk/overlay'; import { XManagerService } from 'x-framework-services'; import { XBaseActionModel } from '../classes/x-base-action.model'; import { X_FRAMEWORK_COMPONENTS_CONFIG } from '../tokens/x-injectable-tokens'; -import { XFrameworkComponentsConfig } from '../config/x-framework-components.config'; -import { - IXComponentActionBarContainer, - XActionBarActionFiredModel, - XActionBarActionModel, - XActionBarItem, - XComponentActionBarContainer, -} from '../x-action-bar/x-action-bar.typings'; import { XPopoverPresentOptions } from '../dialogs/x-popover/x-popover.typings'; -import { - XBaseContainer, - XBaseContainerComponent, -} from '../classes/i-x-base.container'; +import { XFrameworkComponentsConfig } from '../config/x-framework-components.config'; // //#region Chart ... @@ -371,71 +370,71 @@ export type XChartAxisChartSeries = { zIndex?: number; parsing?: XChartParsing; data: - | (number | null)[] - | { - /** - * A category label, a timestamp, or a `Date`. On a `datetime` axis all - * three keep millisecond resolution. - */ - x: string | number | Date; - /** - * A plain value for most charts. For `candlestick`/`boxPlot`, the - * summary array (`[O,H,L,C]` / `[min,Q1,median,Q3,max]`). For `violin`, an - * object carrying the precomputed density profile (`[value, weight]` pairs) - * plus the raw observations rendered as jitter. For a `scatter` strip plot - * (`plotOptions.scatter.jitter`), the array of observations in this category. - */ - y: - | number - | null - | number[] - | { density: [number, number][]; points?: number[] }; - /** - * Optional raw observations for a `boxPlot` data point, rendered as jitter - * dots when `plotOptions.boxPlot.points.show` is enabled. - */ - points?: number[]; - fill?: XChartFill; - fillColor?: string; - strokeColor?: string; - meta?: unknown; - /** - * A second metric that drives this point's COLOUR, independent of `y` - * which drives its size (`treemap`). See - * `plotOptions.treemap.colorScale.colorValue` to read a different key. - */ - colorValue?: number; - /** - * Nested hierarchy: this point is a branch containing these children, - * to any depth (`treemap`, `sunburst`). A branch normally omits `y` and - * takes the sum of its children instead. - */ - children?: XChartHierarchyNode[]; - /** - * Drilldown target: the `id` of a `drilldown.series` entry. Clicking this - * point drills into that level. Requires the Drilldown feature. - */ - drilldown?: string | number; - goals?: { - name?: string; - value: number; - strokeHeight?: number; - strokeWidth?: number; - strokeColor?: string; - strokeDashArray?: number; - strokeLineCap?: 'butt' | 'square' | 'round'; - }[]; - barHeightOffset?: number; - columnWidthOffset?: number; - }[] - | [number, number | null][] - | [number, (number | null)[]][] - | number[][] - // A `children` hierarchy for the partition charts, where a branch carries no - // value of its own. Listed before the catch-all so authors get completion on - // the node shape instead of falling through to `Record`. - | XChartHierarchyNode[] - | Record[]; + | (number | null)[] + | { + /** + * A category label, a timestamp, or a `Date`. On a `datetime` axis all + * three keep millisecond resolution. + */ + x: string | number | Date; + /** + * A plain value for most charts. For `candlestick`/`boxPlot`, the + * summary array (`[O,H,L,C]` / `[min,Q1,median,Q3,max]`). For `violin`, an + * object carrying the precomputed density profile (`[value, weight]` pairs) + * plus the raw observations rendered as jitter. For a `scatter` strip plot + * (`plotOptions.scatter.jitter`), the array of observations in this category. + */ + y: + | number + | null + | number[] + | { density: [number, number][]; points?: number[] }; + /** + * Optional raw observations for a `boxPlot` data point, rendered as jitter + * dots when `plotOptions.boxPlot.points.show` is enabled. + */ + points?: number[]; + fill?: XChartFill; + fillColor?: string; + strokeColor?: string; + meta?: unknown; + /** + * A second metric that drives this point's COLOUR, independent of `y` + * which drives its size (`treemap`). See + * `plotOptions.treemap.colorScale.colorValue` to read a different key. + */ + colorValue?: number; + /** + * Nested hierarchy: this point is a branch containing these children, + * to any depth (`treemap`, `sunburst`). A branch normally omits `y` and + * takes the sum of its children instead. + */ + children?: XChartHierarchyNode[]; + /** + * Drilldown target: the `id` of a `drilldown.series` entry. Clicking this + * point drills into that level. Requires the Drilldown feature. + */ + drilldown?: string | number; + goals?: { + name?: string; + value: number; + strokeHeight?: number; + strokeWidth?: number; + strokeColor?: string; + strokeDashArray?: number; + strokeLineCap?: 'butt' | 'square' | 'round'; + }[]; + barHeightOffset?: number; + columnWidthOffset?: number; + }[] + | [number, number | null][] + | [number, (number | null)[]][] + | number[][] + // A `children` hierarchy for the partition charts, where a branch carries no + // value of its own. Listed before the catch-all so authors get completion on + // the node shape instead of falling through to `Record`. + | XChartHierarchyNode[] + | Record[]; }[]; export type XChartNonAxisChartSeries = number[] | XChartAxisChartSeries; @@ -932,11 +931,11 @@ export type XChartPlotOptions = { * property, or an accessor to compute one. */ colorValue?: - | string - | (( - datum: any, - opts: { seriesIndex: number; dataPointIndex: number; w: any }, - ) => number); + | string + | (( + datum: any, + opts: { seriesIndex: number; dataPointIndex: number; w: any }, + ) => number); /** * Continuous interpolation between colour stops, for the metric above. * Active as soon as any datum carries a colour metric; `enabled: false` @@ -1069,13 +1068,13 @@ export type XChartPlotOptions = { * 'custom': positions come from `positions`. */ layout?: - | 'grouped' - | 'packed' - | 'columns' - | 'grid' - | 'scatter' - | 'arc' - | 'custom'; + | 'grouped' + | 'packed' + | 'columns' + | 'grid' + | 'scatter' + | 'arc' + | 'custom'; /** * `layout: 'custom'` only. The layout provider: either a function returning * plot-pixel positions, or the name of one registered with @@ -2103,19 +2102,19 @@ export interface XChartDrilldown { /** Inline child levels referenced by data-point `drilldown` ids. */ series?: XChartDrilldownSeries[]; breadcrumb?: - | false - | { - show?: boolean; - position?: 'top-left' | 'top-right'; - separator?: string; - rootLabel?: string; - offsetX?: number; - offsetY?: number; - formatter?( - label: string, - opts: { index: number; depth: number }, - ): string; - }; + | false + | { + show?: boolean; + position?: 'top-left' | 'top-right'; + separator?: string; + rootLabel?: string; + offsetX?: number; + offsetY?: number; + formatter?( + label: string, + opts: { index: number; depth: number }, + ): string; + }; animation?: { enabled?: boolean; /** @@ -2164,11 +2163,11 @@ export interface XChartDrilldown { * name, so the default ships no user-visible English. */ loading?: - | false - | { - show?: boolean; - text?: string; - }; + | false + | { + show?: boolean; + text?: string; + }; /** * Cache levels resolved by `onDrillDown`, keyed by id, so drilling back down * a branch does not re-fetch. Default true. Clear it with the drilldown @@ -2274,12 +2273,12 @@ export type XChartStates = { export type XChartStroke = { show?: boolean; curve?: - | 'smooth' - | 'straight' - | 'stepline' - | 'linestep' - | 'monotoneCubic' - | ('smooth' | 'straight' | 'stepline' | 'linestep' | 'monotoneCubic')[]; + | 'smooth' + | 'straight' + | 'stepline' + | 'linestep' + | 'monotoneCubic' + | ('smooth' | 'straight' | 'stepline' | 'linestep' | 'monotoneCubic')[]; lineCap?: 'butt' | 'square' | 'round'; colors?: string[]; width?: number | number[]; @@ -2330,11 +2329,11 @@ export type XChartTheme = { }; accessibility?: { colorBlindMode?: - | 'deuteranopia' - | 'protanopia' - | 'tritanopia' - | 'highContrast' - | ''; + | 'deuteranopia' + | 'protanopia' + | 'tritanopia' + | 'highContrast' + | ''; }; }; @@ -2363,14 +2362,14 @@ export type XChartTooltip = { inverseOrder?: boolean; arrow?: boolean; custom?: - | (( - opts: XChartTooltipCustomOpts, - ) => string | number | Element | { nodeName: string }) - | Array< - ( - opts: XChartTooltipCustomOpts, - ) => string | number | Element | { nodeName: string } - >; + | (( + opts: XChartTooltipCustomOpts, + ) => string | number | Element | { nodeName: string }) + | Array< + ( + opts: XChartTooltipCustomOpts, + ) => string | number | Element | { nodeName: string } + >; fillSeriesColor?: boolean; theme?: 'light' | 'dark'; cssClass?: string; @@ -2447,30 +2446,30 @@ export type XChart = { width?: string | number; height?: string | number; type?: - | 'line' - | 'area' - | 'bar' - | 'pie' - | 'donut' - | 'radialBar' - | 'scatter' - | 'bubble' - | 'heatmap' - | 'candlestick' - | 'boxPlot' - | 'violin' - | 'histogram' - | 'radar' - | 'polarArea' - | 'rangeBar' - | 'rangeArea' - | 'treemap' - | 'unit' - | 'waffle' - | 'sunburst' - | 'funnel' - | 'pyramid' - | 'gauge'; + | 'line' + | 'area' + | 'bar' + | 'pie' + | 'donut' + | 'radialBar' + | 'scatter' + | 'bubble' + | 'heatmap' + | 'candlestick' + | 'boxPlot' + | 'violin' + | 'histogram' + | 'radar' + | 'polarArea' + | 'rangeBar' + | 'rangeArea' + | 'treemap' + | 'unit' + | 'waffle' + | 'sunburst' + | 'funnel' + | 'pyramid' + | 'gauge'; /** * Internal — populated when `type` is a first-class alias (`'funnel'`, * `'pyramid'`, `'gauge'`, `'waffle'`, `'histogram'`). The original requested @@ -2753,9 +2752,9 @@ export type XChart = { dimension?: (row: any) => any; /** FILTER mode: reduction over a bucket's rows. @default 'count' */ reduce?: - | 'count' - | { sum?: string; avg?: string; min?: string; max?: string } - | ((rows: any[]) => number); + | 'count' + | { sum?: string; avg?: string; min?: string; max?: string } + | ((rows: any[]) => number); /** * FILTER mode: bucket kind. Else inferred: `bins` present => 'range', a * heatmap chart => 'matrix' (2D), otherwise 'category'. @@ -2873,21 +2872,21 @@ export type XChart = { | 'yline' | 'measure' | { - id?: string; - label?: string; - icon?: string; - onClick?: ( - chart: ApexCharts, - context: { - x: any; - y: any; - seriesIndex: number | null; - dataPointIndex: number | null; - clientX: number; - clientY: number; - }, - ) => void; - } + id?: string; + label?: string; + icon?: string; + onClick?: ( + chart: ApexCharts, + context: { + x: any; + y: any; + seriesIndex: number | null; + dataPointIndex: number | null; + clientX: number; + clientY: number; + }, + ) => void; + } >; /** Override the built-in item labels. */ labels?: { @@ -3226,7 +3225,7 @@ export enum XChartAction { export type XChartActionIdentifier = XChartAction | string; -export interface XChartActionModel extends XBaseActionModel {} +export interface XChartActionModel extends XBaseActionModel { } export type XChartSeriesType = XChartAxisChartSeries | XChartNonAxisChartSeries; @@ -3300,8 +3299,7 @@ export interface IXChartContainer extends TXChartContainer { autoUpdate: XStandardType; options: XStandardType; series: XStandardType; - - // + zoomStep: XStandardType; type: XStandardType; // @@ -3324,14 +3322,16 @@ export interface IXChartContainer extends TXChartContainer { }) export class XChartContainer extends XBaseContainer - implements Readonly -{ + implements Readonly { + // + readonly actionProvider = new Subject(); + readonly cssClass: XStandardType>; + // readonly options: XStandardType; + readonly zoomStep: XStandardType = 0.125; readonly series: XStandardType; readonly autoUpdate: XStandardType = true; - readonly cssClass: XStandardType>; - readonly actionProvider = new Subject(); readonly type: XStandardType = XChartType.Line; // @@ -3403,8 +3403,7 @@ export class XChartContainer @Component({ template: `` }) export abstract class XChartContainerComponent extends XBaseContainerComponent - implements IXChartContainer -{ + implements IXChartContainer { // //#region Props ... @Input() @@ -3427,6 +3426,9 @@ export abstract class XChartContainerComponent @Input() series: XStandardType = this.propertyProvider.series; + @Input() + zoomStep: XStandardType = this.propertyProvider.zoomStep; + @Input() autoUpdate: XStandardType = this.propertyProvider.autoUpdate;