@hestia-earth/ui-components 0.41.30 → 0.41.32

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.
@@ -33,11 +33,11 @@ import { GoogleMap, MapMarker, MapPolygon } from '@angular/google-maps';
33
33
  import { Meta, DomSanitizer } from '@angular/platform-browser';
34
34
  import removeMd from 'remove-markdown';
35
35
  import orderBy from 'lodash.orderby';
36
+ import merge$1 from 'lodash.merge';
36
37
  import { Chart, BarController, LineController, CategoryScale, LinearScale, PointElement, BarElement, LineElement, Title, Tooltip, Legend, TimeScale } from 'chart.js';
37
38
  import C2S from 'canvas-to-svg';
38
39
  import 'chartjs-adapter-date-fns';
39
40
  import annotationPlugin from 'chartjs-plugin-annotation';
40
- import merge$1 from 'lodash.merge';
41
41
  import { headersFromCsv, toCsv as toCsv$1, toJson, toCsvPivot, ErrorKeys } from '@hestia-earth/schema-convert';
42
42
  import { isCSVIncluded, isDefaultCSVSelected } from '@hestia-earth/json-schema/schema-utils';
43
43
  import { recommendedProperties, loadSchemas } from '@hestia-earth/json-schema';
@@ -5651,161 +5651,467 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
5651
5651
  args: [{ selector: 'he-bibliographies-search-confirm', changeDetection: ChangeDetectionStrategy.OnPush, imports: [FormsModule, ReactiveFormsModule, NgbHighlight, NgTemplateOutlet, HESvgIconComponent], template: "<div class=\"modal is-large is-active\">\n <div class=\"modal-background\"></div>\n <div class=\"modal-card\">\n <header class=\"modal-card-head\">\n <p class=\"modal-card-title\">Search Bibliographies</p>\n <button class=\"delete is-small\" aria-label=\"close\" type=\"button\" (click)=\"cancel()\"></button>\n </header>\n <section class=\"modal-card-body\">\n <form [formGroup]=\"formGroup\" novalidate>\n <div class=\"field has-addons\">\n <div class=\"control is-expanded has-icons-right\">\n <input\n class=\"input search-input\"\n [attr.placeholder]=\"'Search bibliography by ' + searchBy()\"\n formControlName=\"search\"\n name=\"bibliography\"\n autocomplete=\"off\"\n (focus)=\"searchFocus($event)\" />\n <a class=\"icon is-small is-right\" [class.is-hidden]=\"loading()\" (click)=\"resetSearch()\">\n <he-svg-icon name=\"xmark\" />\n </a>\n\n <span class=\"icon is-right has-text-grey-dark\" [class.is-hidden]=\"!loading()\">\n <he-svg-icon name=\"loading\" animation=\"spin\" size=\"20\" />\n </span>\n </div>\n </div>\n </form>\n\n <div class=\"is-mt-2\">\n <span [class.is-hidden]=\"loading() || !searchControl?.value || hasResults()\">\n No bibliographies found matching query.\n </span>\n\n <ul>\n @for (result of results(); track result) {\n <li>\n <a\n class=\"is-block p-1 search-result\"\n (click)=\"selectedResult.set(result)\"\n [class.is-active]=\"selectedResult() === result\">\n <ngb-highlight [result]=\"result.bibliography?.title ?? result.title\" [term]=\"searchControl?.value\" />\n <span class=\"px-1\">-</span>\n <span class=\"px-1\">\n <i>{{ result.name }}</i>\n </span>\n @if (result.bibliography?.documentDOI || result.documentDOI) {\n <span class=\"px-1\">\n -\n <b>documentDOI:</b>\n </span>\n }\n <ngb-highlight\n [result]=\"result.bibliography?.documentDOI ?? result.documentDOI\"\n [term]=\"searchControl?.value\" />\n @if (result.bibliography?.scopus || result.scopus) {\n <span class=\"px-1\">\n -\n <b>scopus:</b>\n </span>\n }\n <ngb-highlight [result]=\"result.bibliography?.scopus ?? result.scopus\" [term]=\"searchControl?.value\" />\n <span>\n <ng-container\n *ngTemplateOutlet=\"\n mendeleyLink;\n context: { $implicit: result.bibliography?.mendeleyID ?? result.mendeleyID }\n \" />\n </span>\n </a>\n </li>\n }\n </ul>\n </div>\n </section>\n <footer class=\"modal-card-foot\">\n <button class=\"button is-primary\" (click)=\"confirm()\" [disabled]=\"!selectedResult()\">\n <span>Confirm</span>\n </button>\n <button class=\"button is-ghost\" (click)=\"cancel()\">\n <span>Close</span>\n </button>\n </footer>\n </div>\n</div>\n\n<ng-template #mendeleyLink let-id>\n @if (id) {\n <a [href]=\"'https://www.mendeley.com/catalogue/' + id\" target=\"_blank\" (click)=\"$event.stopPropagation()\">\n <he-svg-icon name=\"external-link\" class=\"ml-2\" />\n </a>\n }\n</ng-template>\n", styles: ["ngb-highlight,span{vertical-align:middle;white-space:normal;width:auto}\n"] }]
5652
5652
  }], ctorParameters: () => [], propDecorators: { search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }, { type: i0.Output, args: ["searchChange"] }], searchSources: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchSources", required: false }] }, { type: i0.Output, args: ["searchSourcesChange"] }], searchBibliographies: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchBibliographies", required: false }] }, { type: i0.Output, args: ["searchBibliographiesChange"] }], searchBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchBy", required: false }] }, { type: i0.Output, args: ["searchByChange"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
5653
5653
 
5654
- // show label 8px from bar
5655
- const gapX = 8;
5656
- const placementGap = (placement) => (placement === 'right' ? +gapX : -gapX);
5657
- const placementX = (x, chart, data, placement) => placement === 'right' ? (!Array.isArray(data) && data < 0 ? positionAtZero(chart) : x) : x;
5658
- const positionAtZero = (chart) => chart.scales.x.getPixelForValue(0);
5659
- const defaultBarDrawSettings = {
5660
- placement: 'right',
5661
- xPosFn: (x, index, width, chart, data, placement) => (isEmpty(data) ? positionAtZero(chart) : placementX(x, chart, data, placement)) + placementGap(placement),
5662
- yPosFn: y => y + 3,
5663
- colorFn: (m, index, chart, data) => (isUndefined(data) ? '#b5b5b5' : '#4a4a4a'),
5664
- textFn: ({ data }) => `${data}`,
5665
- font: '14px Lato',
5666
- maxWidth: 90,
5667
- emptyValueLabel: 'No data'
5668
- };
5669
- const drawText = (ctx, { text, font, fillStyle, placement, xPos, yPos, maxWidth }) => {
5670
- ctx.font = font;
5671
- ctx.fillStyle = fillStyle;
5672
- ctx.textAlign = placement === 'left' ? 'right' : 'left';
5673
- if (Array.isArray(text)) {
5674
- ctx.fillText(text[0], xPos, yPos - 5, maxWidth);
5675
- ctx.fillText(text[1], xPos, yPos + 5, maxWidth);
5654
+ class ChartConfigurationDirective {
5655
+ constructor() {
5656
+ this._elementRef = inject(ElementRef);
5657
+ this._observer = new ResizeObserver(() => this.resize());
5658
+ /**
5659
+ * The chart configuration.
5660
+ * This is used to initialize the chart.
5661
+ *
5662
+ * @param configuration The chart configuration
5663
+ */
5664
+ this.chartConfiguration = input(...(ngDevMode ? [undefined, { debugName: "chartConfiguration" }] : []));
5665
+ /**
5666
+ * The container element of the chart.
5667
+ * This is used to observe the size of the container and resize the chart accordingly. (chart.js update charts only on the window resize event)
5668
+ * If not provided, the chart will not be resized.
5669
+ *
5670
+ * @param container The container element of the chart
5671
+ */
5672
+ this.chartContainer = input(...(ngDevMode ? [undefined, { debugName: "chartContainer" }] : []));
5673
+ effect(onCleanup => {
5674
+ const configuration = this.chartConfiguration();
5675
+ untracked(() => {
5676
+ this.removeChart();
5677
+ if (configuration) {
5678
+ this._chart = new Chart(this._elementRef.nativeElement, configuration);
5679
+ }
5680
+ });
5681
+ onCleanup(() => this.removeChart());
5682
+ });
5683
+ effect(onCleanup => {
5684
+ if (this.chartContainer()) {
5685
+ this._observer?.observe(this.chartContainer());
5686
+ }
5687
+ else {
5688
+ this._observer?.disconnect();
5689
+ }
5690
+ onCleanup(() => this._observer?.disconnect());
5691
+ });
5676
5692
  }
5677
- else {
5678
- ctx.fillText(text, xPos, yPos, maxWidth);
5693
+ removeChart() {
5694
+ const chart = this._chart || Chart.getChart(this._elementRef.nativeElement);
5695
+ chart?.destroy();
5696
+ this._chart = null;
5679
5697
  }
5680
- };
5681
- const afterBarDrawPlugin = settings => ({
5682
- id: ['afterBarDrawPlugin', settings?.placement].filter(Boolean).join('-'),
5683
- afterDatasetsDraw: (chart) => {
5684
- if (!chart.data.datasets?.length) {
5685
- return;
5686
- }
5687
- const { placement, xPosFn, yPosFn, colorFn, textFn, maxWidth, font, emptyValueLabel } = {
5688
- ...defaultBarDrawSettings,
5689
- ...(settings ?? {})
5690
- };
5691
- const { ctx, width, height } = chart;
5692
- ctx.save();
5693
- const meta = chart.getDatasetMeta(0);
5694
- const dataset = chart.data.datasets[0];
5695
- const elements = meta.data;
5696
- elements
5697
- .filter(element => !element.skip)
5698
- .forEach((element, index) => {
5699
- const { x, y, base } = element;
5700
- const label = chart.data.labels?.[index] ?? '';
5701
- const data = dataset.data[index];
5702
- // use min/max as both negative values would inverse the positions
5703
- const anchorX = placement === 'left' ? Math.min(base, x) : Math.max(base, x);
5704
- const xPos = xPosFn(anchorX, index, width, chart, data, placement);
5705
- const yPos = yPosFn(y, index, height, chart, data, placement);
5706
- const text = isUndefined(data) ? emptyValueLabel : textFn({ label, data }, index, chart);
5707
- text &&
5708
- drawText(ctx, {
5709
- text,
5710
- font,
5711
- fillStyle: colorFn(element.options, index, chart, data),
5712
- placement,
5713
- xPos,
5714
- yPos,
5715
- maxWidth
5716
- });
5717
- });
5718
- ctx.restore();
5698
+ get chart() {
5699
+ return this._chart;
5719
5700
  }
5720
- });
5721
-
5722
- const axisHoverPlugin = {
5723
- id: 'axisHover',
5724
- afterEvent(chart, args) {
5725
- const { event } = args;
5726
- const { maxDistance = 15, onHoverLabel } = (chart.config.options.plugins.axisHover ||
5727
- {});
5728
- const yScale = chart.scales.y;
5729
- const isOverYAxis = event.x >= yScale.left && event.x <= yScale.right;
5730
- const closestTick = isOverYAxis
5731
- ? yScale.getTicks().find((tick, index) => Math.abs(event.y - yScale.getPixelForTick(index)) < maxDistance)
5732
- : null;
5733
- const label = closestTick ? chart.data.labels[closestTick.value] : '';
5734
- onHoverLabel({ label, event });
5701
+ resize() {
5702
+ this._chart?.resize();
5735
5703
  }
5736
- };
5704
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartConfigurationDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
5705
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.6", type: ChartConfigurationDirective, isStandalone: true, selector: "[chartConfiguration]", inputs: { chartConfiguration: { classPropertyName: "chartConfiguration", publicName: "chartConfiguration", isSignal: true, isRequired: false, transformFunction: null }, chartContainer: { classPropertyName: "chartContainer", publicName: "chartContainer", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["chart"], ngImport: i0 }); }
5706
+ }
5707
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartConfigurationDirective, decorators: [{
5708
+ type: Directive,
5709
+ args: [{
5710
+ selector: '[chartConfiguration]',
5711
+ exportAs: 'chart'
5712
+ }]
5713
+ }], ctorParameters: () => [], propDecorators: { chartConfiguration: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartConfiguration", required: false }] }], chartContainer: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartContainer", required: false }] }] } });
5737
5714
 
5738
- // ... createHoverGradient remains the same ...
5739
- const createHoverGradient = (ctx, chartArea, color) => {
5740
- const gradient = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0);
5741
- gradient.addColorStop(0, colorToRgba(color, 0.05));
5742
- gradient.addColorStop(0.5, colorToRgba(color, 0.12));
5743
- gradient.addColorStop(1, colorToRgba(color, 0.05));
5744
- return gradient;
5745
- };
5746
- // UPDATED: Use element properties instead of _model
5747
- const calculateBarBounds = (element, indexAxis = 'y') => {
5748
- const { x, y, base, width, height } = element;
5749
- // Handle Horizontal Bars (indexAxis: 'y')
5750
- if (indexAxis === 'y') {
5751
- const barHeight = height; // Thickness of the bar
5752
- const barTop = y - barHeight / 2;
5753
- const barBottom = y + barHeight / 2;
5754
- const barLeft = Math.min(base, x);
5755
- const barRight = Math.max(base, x);
5756
- return { barLeft, barRight, barTop, barBottom, thickness: barHeight };
5757
- }
5758
- // Handle Vertical Bars (indexAxis: 'x')
5759
- else {
5760
- const barWidth = width; // Thickness of the bar
5761
- const barLeft = x - barWidth / 2;
5762
- const barRight = x + barWidth / 2;
5763
- const barTop = Math.min(base, y);
5764
- const barBottom = Math.max(base, y);
5765
- return { barLeft, barRight, barTop, barBottom, thickness: barWidth };
5715
+ const defaultChartWidth = 800;
5716
+ const defaultChartHeight = 600;
5717
+ const extractEssentialChartConfig = (config) => ({
5718
+ type: config.type || 'bar',
5719
+ data: config.data,
5720
+ options: {
5721
+ indexAxis: config.options?.indexAxis,
5722
+ responsive: false,
5723
+ maintainAspectRatio: false,
5724
+ devicePixelRatio: 1,
5725
+ animation: false,
5726
+ animations: {
5727
+ colors: false,
5728
+ x: false,
5729
+ y: false
5730
+ },
5731
+ transitions: {
5732
+ active: {
5733
+ animation: {
5734
+ duration: 0
5735
+ }
5736
+ }
5737
+ },
5738
+ plugins: {
5739
+ legend: { display: false },
5740
+ title: config.options.plugins?.title || { display: false }
5741
+ },
5742
+ scales: config.options.scales
5766
5743
  }
5744
+ });
5745
+ const stretchSvg = (svgString, width, height) => {
5746
+ const parser = new DOMParser();
5747
+ const doc = parser.parseFromString(svgString, 'image/svg+xml');
5748
+ const svgElement = doc.documentElement;
5749
+ // Add 100px padding to ViewBox for overflow labels
5750
+ svgElement.setAttribute('viewBox', `0 0 ${width + 100} ${height}`);
5751
+ svgElement.setAttribute('width', '100%');
5752
+ svgElement.setAttribute('height', '100%');
5753
+ svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet');
5754
+ return svgElement.outerHTML;
5767
5755
  };
5768
- // UPDATED: Calculate shadow bounds based on the bounds we just found
5769
- const calculateShadowBounds = (element, bounds, threshold) => {
5770
- return {
5771
- top: bounds.barTop - threshold,
5772
- bottom: bounds.barBottom + threshold,
5773
- left: bounds.barLeft, // Usually we expand Y (thickness), but keep X (length) capped at value
5774
- right: bounds.barRight,
5775
- width: bounds.barRight - bounds.barLeft,
5776
- height: bounds.barBottom + threshold - (bounds.barTop - threshold)
5756
+ const parseAfterDrawBarPlugins = (afterBarDrawConfig, units) => Array.isArray(afterBarDrawConfig)
5757
+ ? afterBarDrawConfig.map(afterBarDrawPlugin)
5758
+ : [
5759
+ afterBarDrawPlugin({
5760
+ textFn: ({ data }) => [toPrecision(Array.isArray(data) ? data[0] : data), units].filter(Boolean).join(' '),
5761
+ emptyValueLabel: 'No data',
5762
+ ...afterBarDrawConfig
5763
+ })
5764
+ ];
5765
+ const convertToSvg = (config, metadata = {}) => new Promise((resolve, reject) => {
5766
+ const width = metadata.width || defaultChartWidth;
5767
+ const height = metadata.height || defaultChartHeight;
5768
+ // 1. Create the SVG Context
5769
+ const svgContext = new C2S(width, height);
5770
+ svgContext.setTransform = () => { };
5771
+ svgContext.resetTransform = () => { };
5772
+ svgContext.roundRect = function (x, y, w, h) {
5773
+ this.rect(x, y, w, h);
5777
5774
  };
5778
- };
5779
- const drawHoverEffect = (ctx, chartArea, element, threshold, indexAxis) => {
5780
- // Access colors via options
5781
- const opts = element.options || null;
5782
- const barColor = (opts?.backgroundColor || opts?.borderColor || '#000000');
5783
- const color = parseColor(barColor);
5784
- const bounds = calculateBarBounds(element, indexAxis);
5785
- const shadow = calculateShadowBounds(element, bounds, threshold);
5786
- ctx.save();
5787
- // Draw Gradient Background
5788
- const gradient = createHoverGradient(ctx, { left: shadow.left, right: shadow.right }, color);
5789
- ctx.fillStyle = gradient;
5790
- ctx.fillRect(shadow.left, shadow.top, shadow.width, shadow.height);
5791
- // Draw Dashed Lines
5792
- drawHorizontalLines(ctx, { left: shadow.left, right: shadow.right }, shadow.top, shadow.bottom, color);
5793
- ctx.restore();
5794
- };
5795
- const drawHorizontalLines = (ctx, barArea, topY, bottomY, color) => {
5796
- ctx.strokeStyle = colorToRgba(color, 0.25);
5797
- ctx.lineWidth = 1;
5798
- ctx.setLineDash([5, 5]);
5799
- ctx.beginPath();
5800
- ctx.moveTo(barArea.left, topY);
5801
- ctx.lineTo(barArea.right, topY);
5802
- ctx.moveTo(barArea.left, bottomY);
5803
- ctx.lineTo(barArea.right, bottomY);
5804
- ctx.stroke();
5805
- };
5806
- // UPDATED: Distance calculation using the new bounds
5807
- const calculateDistanceToBar = (x, y, bounds) => {
5808
- let distance = 0;
5775
+ // 2. Mock the Canvas Element
5776
+ const mockCanvas = document.createElement('canvas');
5777
+ mockCanvas.width = width;
5778
+ mockCanvas.height = height;
5779
+ mockCanvas.style.width = `${width}px`;
5780
+ mockCanvas.style.height = `${height}px`;
5781
+ mockCanvas.getBoundingClientRect = () => ({
5782
+ x: 0,
5783
+ y: 0,
5784
+ bottom: height,
5785
+ height: height,
5786
+ left: 0,
5787
+ right: width,
5788
+ top: 0,
5789
+ width: width,
5790
+ toJSON: () => { }
5791
+ });
5792
+ // Intercept getContext to return our SVG generator
5793
+ mockCanvas.getContext = (type) => {
5794
+ if (type === '2d') {
5795
+ svgContext.canvas = mockCanvas;
5796
+ return svgContext;
5797
+ }
5798
+ return null;
5799
+ };
5800
+ // 3. Prepare Config
5801
+ const chartConfig = extractEssentialChartConfig(config);
5802
+ // eslint-disable-next-line prefer-const
5803
+ let chart;
5804
+ chartConfig.options.animation = {
5805
+ onComplete: () => {
5806
+ try {
5807
+ const svgString = svgContext.getSerializedSvg(true);
5808
+ const finalSvg = stretchSvg(svgString, width, height);
5809
+ chart.destroy();
5810
+ resolve(finalSvg);
5811
+ }
5812
+ catch (e) {
5813
+ reject(e);
5814
+ }
5815
+ }
5816
+ };
5817
+ // 4. Register Export-Specific Plugins
5818
+ chartConfig.plugins = [
5819
+ ...parseAfterDrawBarPlugins(metadata.afterBarDrawConfig || {}, metadata.units),
5820
+ metadata.lollipopConfig ? lollipopChartPlugin(metadata.lollipopConfig) : null
5821
+ ]
5822
+ .filter(Boolean)
5823
+ .flat();
5824
+ chart = new Chart(mockCanvas, chartConfig);
5825
+ });
5826
+ const exportAsSVG = async (config, metadata) => {
5827
+ try {
5828
+ const content = await convertToSvg(config, metadata);
5829
+ const blob = new Blob([content], { type: 'image/svg+xml;charset=utf-8' });
5830
+ const url = URL.createObjectURL(blob);
5831
+ downloadFile(url, metadata.fileName || 'chart-export.svg');
5832
+ }
5833
+ catch (error) {
5834
+ console.error('Failed to export SVG', error);
5835
+ }
5836
+ };
5837
+ const svgToUrl = (svgElement) => {
5838
+ const serializer = new XMLSerializer();
5839
+ let svgString = serializer.serializeToString(svgElement);
5840
+ if (!svgString.includes('xmlns')) {
5841
+ svgString = svgString.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
5842
+ }
5843
+ const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
5844
+ return URL.createObjectURL(blob);
5845
+ };
5846
+ const downloadSvg = (svgElement) => downloadFile(svgToUrl(svgElement), 'chart.svg');
5847
+ const downloadPng = async (svgElement, { width, height } = {}) => {
5848
+ const canvas = document.createElement('canvas');
5849
+ const ctx = canvas.getContext('2d');
5850
+ const img = new Image();
5851
+ // Set canvas dimensions based on SVG size
5852
+ // (You might want to fetch getBBox() or use explicitly set width/height)
5853
+ const svgSize = svgElement.getBoundingClientRect();
5854
+ canvas.width = width || svgSize.width;
5855
+ canvas.height = height || svgSize.height;
5856
+ img.onload = () => {
5857
+ if (ctx) {
5858
+ // ctx.fillStyle = 'white';
5859
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
5860
+ ctx.drawImage(img, 0, 0);
5861
+ const pngUrl = canvas.toDataURL('image/png');
5862
+ downloadFile(pngUrl, 'chart.png');
5863
+ }
5864
+ };
5865
+ img.src = svgToUrl(svgElement);
5866
+ };
5867
+
5868
+ var ChartExportFormat;
5869
+ (function (ChartExportFormat) {
5870
+ ChartExportFormat["png"] = "Image";
5871
+ ChartExportFormat["svg"] = "Vector image";
5872
+ })(ChartExportFormat || (ChartExportFormat = {}));
5873
+ const exportFormats = Object.entries(ChartExportFormat).map(([extension, label]) => ({ extension, label }));
5874
+ class ChartExportButtonComponent {
5875
+ constructor() {
5876
+ this.modalService = inject(NgbModal);
5877
+ this.modal = viewChild('modal', ...(ngDevMode ? [{ debugName: "modal" }] : []));
5878
+ this.buttonClass = input('button is-small is-ghost is-p-2', ...(ngDevMode ? [{ debugName: "buttonClass" }] : []));
5879
+ this.chart = input(...(ngDevMode ? [undefined, { debugName: "chart" }] : []));
5880
+ this.config = input(...(ngDevMode ? [undefined, { debugName: "config" }] : []));
5881
+ this.exportFormats = input(exportFormats, ...(ngDevMode ? [{ debugName: "exportFormats" }] : []));
5882
+ this.chartExportFn = input(...(ngDevMode ? [undefined, { debugName: "chartExportFn" }] : []));
5883
+ this.exportFormat = signal(undefined, ...(ngDevMode ? [{ debugName: "exportFormat" }] : []));
5884
+ }
5885
+ async defaultDownload(format) {
5886
+ return format === 'svg' ? await this.chart().exportAsSvg(this.config()) : this.chart().exportAsPng();
5887
+ }
5888
+ async download() {
5889
+ const downloadFn = this.chartExportFn() || this.defaultDownload.bind(this);
5890
+ await downloadFn(this.exportFormat(), this.chart());
5891
+ this.close();
5892
+ }
5893
+ open() {
5894
+ this.modalService.open(this.modal());
5895
+ }
5896
+ close() {
5897
+ this.exportFormat.set(undefined);
5898
+ this.modalService.dismissAll();
5899
+ }
5900
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartExportButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5901
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ChartExportButtonComponent, isStandalone: true, selector: "he-chart-export-button", inputs: { buttonClass: { classPropertyName: "buttonClass", publicName: "buttonClass", isSignal: true, isRequired: false, transformFunction: null }, chart: { classPropertyName: "chart", publicName: "chart", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, exportFormats: { classPropertyName: "exportFormats", publicName: "exportFormats", isSignal: true, isRequired: false, transformFunction: null }, chartExportFn: { classPropertyName: "chartExportFn", publicName: "chartExportFn", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "modal", first: true, predicate: ["modal"], descendants: true, isSignal: true }], ngImport: i0, template: "<button\n class=\"{{ buttonClass() }}\"\n type=\"button\"\n [ngbTooltip]=\"chart()?.exporting() ? '' : 'Download'\"\n placement=\"bottom\"\n (click)=\"open()\">\n <he-svg-icon name=\"download\" />\n</button>\n\n<ng-template #modal>\n <div class=\"modal is-active\">\n <div class=\"modal-background\"></div>\n <div class=\"modal-card\">\n <header class=\"modal-card-head\">\n <p class=\"modal-card-title\">Download Chart</p>\n <button class=\"delete is-small\" aria-label=\"close\" type=\"button\" (click)=\"close()\"></button>\n </header>\n <section class=\"modal-card-body\">\n <p class=\"has-text-secondary is-mb-2\">Download chart as:</p>\n <div class=\"is-flex is-flex-direction-column is-gap-4\">\n @for (format of exportFormats(); track format.extension) {\n <div class=\"is-flex is-gap-4\">\n <div class=\"field is-mb-0\">\n <input\n type=\"checkbox\"\n [id]=\"format.extension\"\n [checked]=\"exportFormat() === format.extension\"\n (change)=\"exportFormat.set(format.extension)\" />\n </div>\n <he-svg-icon name=\"image\" />\n <label class=\"has-text-grey-dark is-clickable\" [for]=\"format.extension\">\n {{ format.label }} (.{{ format.extension }})\n </label>\n </div>\n }\n </div>\n </section>\n <footer class=\"modal-card-foot\">\n <button class=\"button is-primary\" [disabled]=\"!exportFormat()\" (click)=\"download()\">\n @if (chart()?.exporting()) {\n <he-svg-icon name=\"loading\" animation=\"spin\" />\n } @else {\n <he-svg-icon name=\"download\" />\n }\n <span class=\"is-pl-2\">Download</span>\n </button>\n </footer>\n </div>\n </div>\n</ng-template>\n", styles: [""], dependencies: [{ kind: "component", type: HESvgIconComponent, selector: "he-svg-icon", inputs: ["name", "size", "animation"] }, { kind: "directive", type: NgbTooltip, selector: "[ngbTooltip]", inputs: ["animation", "autoClose", "placement", "popperOptions", "triggers", "positionTarget", "container", "disableTooltip", "tooltipClass", "tooltipContext", "openDelay", "closeDelay", "ngbTooltip"], outputs: ["shown", "hidden"], exportAs: ["ngbTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5902
+ }
5903
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartExportButtonComponent, decorators: [{
5904
+ type: Component$1,
5905
+ args: [{ selector: 'he-chart-export-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HESvgIconComponent, NgbTooltip], template: "<button\n class=\"{{ buttonClass() }}\"\n type=\"button\"\n [ngbTooltip]=\"chart()?.exporting() ? '' : 'Download'\"\n placement=\"bottom\"\n (click)=\"open()\">\n <he-svg-icon name=\"download\" />\n</button>\n\n<ng-template #modal>\n <div class=\"modal is-active\">\n <div class=\"modal-background\"></div>\n <div class=\"modal-card\">\n <header class=\"modal-card-head\">\n <p class=\"modal-card-title\">Download Chart</p>\n <button class=\"delete is-small\" aria-label=\"close\" type=\"button\" (click)=\"close()\"></button>\n </header>\n <section class=\"modal-card-body\">\n <p class=\"has-text-secondary is-mb-2\">Download chart as:</p>\n <div class=\"is-flex is-flex-direction-column is-gap-4\">\n @for (format of exportFormats(); track format.extension) {\n <div class=\"is-flex is-gap-4\">\n <div class=\"field is-mb-0\">\n <input\n type=\"checkbox\"\n [id]=\"format.extension\"\n [checked]=\"exportFormat() === format.extension\"\n (change)=\"exportFormat.set(format.extension)\" />\n </div>\n <he-svg-icon name=\"image\" />\n <label class=\"has-text-grey-dark is-clickable\" [for]=\"format.extension\">\n {{ format.label }} (.{{ format.extension }})\n </label>\n </div>\n }\n </div>\n </section>\n <footer class=\"modal-card-foot\">\n <button class=\"button is-primary\" [disabled]=\"!exportFormat()\" (click)=\"download()\">\n @if (chart()?.exporting()) {\n <he-svg-icon name=\"loading\" animation=\"spin\" />\n } @else {\n <he-svg-icon name=\"download\" />\n }\n <span class=\"is-pl-2\">Download</span>\n </button>\n </footer>\n </div>\n </div>\n</ng-template>\n" }]
5906
+ }], propDecorators: { modal: [{ type: i0.ViewChild, args: ['modal', { isSignal: true }] }], buttonClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonClass", required: false }] }], chart: [{ type: i0.Input, args: [{ isSignal: true, alias: "chart", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], exportFormats: [{ type: i0.Input, args: [{ isSignal: true, alias: "exportFormats", required: false }] }], chartExportFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartExportFn", required: false }] }] } });
5907
+
5908
+ const defaultSettings$4 = Object.freeze({
5909
+ options: {
5910
+ responsive: true,
5911
+ maintainAspectRatio: false,
5912
+ plugins: {
5913
+ legend: {
5914
+ display: false
5915
+ }
5916
+ }
5917
+ }
5918
+ });
5919
+ const defaultTicksFont = {
5920
+ family: 'Lato',
5921
+ size: 13
5922
+ };
5923
+ class ChartComponent {
5924
+ constructor() {
5925
+ this.data = input(undefined, ...(ngDevMode ? [{ debugName: "data" }] : []));
5926
+ this.config = input({}, ...(ngDevMode ? [{ debugName: "config" }] : []));
5927
+ this.showExportButton = input(true, ...(ngDevMode ? [{ debugName: "showExportButton" }] : []));
5928
+ this.exporting = signal(false, ...(ngDevMode ? [{ debugName: "exporting" }] : []));
5929
+ this.chartRef = viewChild('chartRef', ...(ngDevMode ? [{ debugName: "chartRef" }] : []));
5930
+ this.configuration = computed(() => ({
5931
+ ...merge$1({}, defaultSettings$4, this.config()),
5932
+ data: this.data()
5933
+ }), ...(ngDevMode ? [{ debugName: "configuration" }] : []));
5934
+ }
5935
+ async exportAsSvg(config = {}) {
5936
+ this.exporting.set(true);
5937
+ await exportAsSVG(this.configuration(), {
5938
+ width: 600,
5939
+ height: 400,
5940
+ fileName: 'chart.svg',
5941
+ ...config
5942
+ });
5943
+ this.exporting.set(false);
5944
+ }
5945
+ exportAsPng() {
5946
+ this.exporting.set(true);
5947
+ const chart = this.chartRef();
5948
+ const url = chart?.chart?.toBase64Image();
5949
+ downloadFile(url, 'chart.png');
5950
+ this.exporting.set(false);
5951
+ }
5952
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5953
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ChartComponent, isStandalone: true, selector: "he-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, showExportButton: { classPropertyName: "showExportButton", publicName: "showExportButton", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "chartRef", first: true, predicate: ["chartRef"], descendants: true, isSignal: true }], exportAs: ["chart"], ngImport: i0, template: "<div class=\"is-relative h-100 | chart-container\" #container>\n @if (showExportButton()) {\n <div class=\"is-absolute | download\">\n <he-chart-export-button [chart]=\"this\" />\n </div>\n }\n\n <ng-content />\n\n <canvas #chartRef=\"chart\" [chartConfiguration]=\"configuration()\" [chartContainer]=\"container\"></canvas>\n</div>\n", styles: [":host{display:block;height:100%;overflow:visible}.chart-container{min-height:50px}.download{top:-12px;right:-10px}\n"], dependencies: [{ kind: "directive", type: ChartConfigurationDirective, selector: "[chartConfiguration]", inputs: ["chartConfiguration", "chartContainer"], exportAs: ["chart"] }, { kind: "component", type: ChartExportButtonComponent, selector: "he-chart-export-button", inputs: ["buttonClass", "chart", "config", "exportFormats", "chartExportFn"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5954
+ }
5955
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartComponent, decorators: [{
5956
+ type: Component$1,
5957
+ args: [{ selector: 'he-chart', exportAs: 'chart', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ChartConfigurationDirective, ChartExportButtonComponent], template: "<div class=\"is-relative h-100 | chart-container\" #container>\n @if (showExportButton()) {\n <div class=\"is-absolute | download\">\n <he-chart-export-button [chart]=\"this\" />\n </div>\n }\n\n <ng-content />\n\n <canvas #chartRef=\"chart\" [chartConfiguration]=\"configuration()\" [chartContainer]=\"container\"></canvas>\n</div>\n", styles: [":host{display:block;height:100%;overflow:visible}.chart-container{min-height:50px}.download{top:-12px;right:-10px}\n"] }]
5958
+ }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], showExportButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExportButton", required: false }] }], chartRef: [{ type: i0.ViewChild, args: ['chartRef', { isSignal: true }] }] } });
5959
+
5960
+ // show label 8px from bar
5961
+ const gapX = 8;
5962
+ const placementGap = (placement) => (placement === 'right' ? +gapX : -gapX);
5963
+ const placementX = (x, chart, data, placement) => placement === 'right' ? (!Array.isArray(data) && data < 0 ? positionAtZero(chart) : x) : x;
5964
+ const positionAtZero = (chart) => chart.scales.x.getPixelForValue(0);
5965
+ const defaultBarDrawSettings = {
5966
+ placement: 'right',
5967
+ xPosFn: (x, index, width, chart, data, placement) => (isEmpty(data) ? positionAtZero(chart) : placementX(x, chart, data, placement)) + placementGap(placement),
5968
+ yPosFn: y => y + 3,
5969
+ colorFn: (m, index, chart, data) => (isUndefined(data) ? '#b5b5b5' : '#4a4a4a'),
5970
+ textFn: ({ data }) => `${data}`,
5971
+ font: `${defaultTicksFont.size}px ${defaultTicksFont.family}`,
5972
+ maxWidth: 90,
5973
+ emptyValueLabel: 'No data'
5974
+ };
5975
+ const drawText = (ctx, { text, font, fillStyle, placement, xPos, yPos, maxWidth }) => {
5976
+ ctx.font = font;
5977
+ ctx.fillStyle = fillStyle;
5978
+ ctx.textAlign = placement === 'left' ? 'right' : 'left';
5979
+ if (Array.isArray(text)) {
5980
+ ctx.fillText(text[0], xPos, yPos - 5, maxWidth);
5981
+ ctx.fillText(text[1], xPos, yPos + 5, maxWidth);
5982
+ }
5983
+ else {
5984
+ ctx.fillText(text, xPos, yPos, maxWidth);
5985
+ }
5986
+ };
5987
+ const afterBarDrawPlugin = settings => ({
5988
+ id: ['afterBarDrawPlugin', settings?.placement].filter(Boolean).join('-'),
5989
+ afterDatasetsDraw: (chart) => {
5990
+ if (!chart.data.datasets?.length) {
5991
+ return;
5992
+ }
5993
+ const { placement, xPosFn, yPosFn, colorFn, textFn, maxWidth, font, emptyValueLabel } = {
5994
+ ...defaultBarDrawSettings,
5995
+ ...(settings ?? {})
5996
+ };
5997
+ const { ctx, width, height } = chart;
5998
+ ctx.save();
5999
+ const meta = chart.getDatasetMeta(0);
6000
+ const dataset = chart.data.datasets[0];
6001
+ const elements = meta.data;
6002
+ elements
6003
+ .filter(element => !element.skip)
6004
+ .forEach((element, index) => {
6005
+ const { x, y, base } = element;
6006
+ const label = chart.data.labels?.[index] ?? '';
6007
+ const data = dataset.data[index];
6008
+ // use min/max as both negative values would inverse the positions
6009
+ const anchorX = placement === 'left' ? Math.min(base, x) : Math.max(base, x);
6010
+ const xPos = xPosFn(anchorX, index, width, chart, data, placement);
6011
+ const yPos = yPosFn(y, index, height, chart, data, placement);
6012
+ const text = isUndefined(data) ? emptyValueLabel : textFn({ label, data }, index, chart);
6013
+ text &&
6014
+ drawText(ctx, {
6015
+ text,
6016
+ font,
6017
+ fillStyle: colorFn(element.options, index, chart, data),
6018
+ placement,
6019
+ xPos,
6020
+ yPos,
6021
+ maxWidth
6022
+ });
6023
+ });
6024
+ ctx.restore();
6025
+ }
6026
+ });
6027
+
6028
+ const axisHoverPlugin = {
6029
+ id: 'axisHover',
6030
+ afterEvent(chart, args) {
6031
+ const { event } = args;
6032
+ const { maxDistance = 15, onHoverLabel } = (chart.config.options.plugins.axisHover ||
6033
+ {});
6034
+ const yScale = chart.scales.y;
6035
+ const isOverYAxis = event.x >= yScale.left && event.x <= yScale.right;
6036
+ const closestTick = isOverYAxis
6037
+ ? yScale.getTicks().find((tick, index) => Math.abs(event.y - yScale.getPixelForTick(index)) < maxDistance)
6038
+ : null;
6039
+ const label = closestTick ? chart.data.labels[closestTick.value] : '';
6040
+ onHoverLabel({ label, event });
6041
+ }
6042
+ };
6043
+
6044
+ // ... createHoverGradient remains the same ...
6045
+ const createHoverGradient = (ctx, chartArea, color) => {
6046
+ const gradient = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0);
6047
+ gradient.addColorStop(0, colorToRgba(color, 0.05));
6048
+ gradient.addColorStop(0.5, colorToRgba(color, 0.12));
6049
+ gradient.addColorStop(1, colorToRgba(color, 0.05));
6050
+ return gradient;
6051
+ };
6052
+ // UPDATED: Use element properties instead of _model
6053
+ const calculateBarBounds = (element, indexAxis = 'y') => {
6054
+ const { x, y, base, width, height } = element;
6055
+ // Handle Horizontal Bars (indexAxis: 'y')
6056
+ if (indexAxis === 'y') {
6057
+ const barHeight = height; // Thickness of the bar
6058
+ const barTop = y - barHeight / 2;
6059
+ const barBottom = y + barHeight / 2;
6060
+ const barLeft = Math.min(base, x);
6061
+ const barRight = Math.max(base, x);
6062
+ return { barLeft, barRight, barTop, barBottom, thickness: barHeight };
6063
+ }
6064
+ // Handle Vertical Bars (indexAxis: 'x')
6065
+ else {
6066
+ const barWidth = width; // Thickness of the bar
6067
+ const barLeft = x - barWidth / 2;
6068
+ const barRight = x + barWidth / 2;
6069
+ const barTop = Math.min(base, y);
6070
+ const barBottom = Math.max(base, y);
6071
+ return { barLeft, barRight, barTop, barBottom, thickness: barWidth };
6072
+ }
6073
+ };
6074
+ // UPDATED: Calculate shadow bounds based on the bounds we just found
6075
+ const calculateShadowBounds = (element, bounds, threshold) => {
6076
+ return {
6077
+ top: bounds.barTop - threshold,
6078
+ bottom: bounds.barBottom + threshold,
6079
+ left: bounds.barLeft, // Usually we expand Y (thickness), but keep X (length) capped at value
6080
+ right: bounds.barRight,
6081
+ width: bounds.barRight - bounds.barLeft,
6082
+ height: bounds.barBottom + threshold - (bounds.barTop - threshold)
6083
+ };
6084
+ };
6085
+ const drawHoverEffect = (ctx, chartArea, element, threshold, indexAxis) => {
6086
+ // Access colors via options
6087
+ const opts = element.options || null;
6088
+ const barColor = (opts?.backgroundColor || opts?.borderColor || '#000000');
6089
+ const color = parseColor(barColor);
6090
+ const bounds = calculateBarBounds(element, indexAxis);
6091
+ const shadow = calculateShadowBounds(element, bounds, threshold);
6092
+ ctx.save();
6093
+ // Draw Gradient Background
6094
+ const gradient = createHoverGradient(ctx, { left: shadow.left, right: shadow.right }, color);
6095
+ ctx.fillStyle = gradient;
6096
+ ctx.fillRect(shadow.left, shadow.top, shadow.width, shadow.height);
6097
+ // Draw Dashed Lines
6098
+ drawHorizontalLines(ctx, { left: shadow.left, right: shadow.right }, shadow.top, shadow.bottom, color);
6099
+ ctx.restore();
6100
+ };
6101
+ const drawHorizontalLines = (ctx, barArea, topY, bottomY, color) => {
6102
+ ctx.strokeStyle = colorToRgba(color, 0.25);
6103
+ ctx.lineWidth = 1;
6104
+ ctx.setLineDash([5, 5]);
6105
+ ctx.beginPath();
6106
+ ctx.moveTo(barArea.left, topY);
6107
+ ctx.lineTo(barArea.right, topY);
6108
+ ctx.moveTo(barArea.left, bottomY);
6109
+ ctx.lineTo(barArea.right, bottomY);
6110
+ ctx.stroke();
6111
+ };
6112
+ // UPDATED: Distance calculation using the new bounds
6113
+ const calculateDistanceToBar = (x, y, bounds) => {
6114
+ let distance = 0;
5809
6115
  // Horizontal distance
5810
6116
  if (x < bounds.barLeft)
5811
6117
  distance += Math.pow(bounds.barLeft - x, 2);
@@ -5911,423 +6217,124 @@ const handleMouseMove = (chart, nearBar, threshold, e) => {
5911
6217
  };
5912
6218
  const handleMouseOut = (chart, nearBar) => {
5913
6219
  chart.canvas.style.cursor = 'default';
5914
- return !!nearBar;
5915
- };
5916
- const handleClick = (chart, nearBar) => {
5917
- if (nearBar)
5918
- triggerBarClick(chart, nearBar);
5919
- };
5920
- const getActiveBar = (chart) => {
5921
- const activeElements = chart.tooltip?.getActiveElements();
5922
- return activeElements?.[0]?.element;
5923
- };
5924
- const defaultSettings$4 = { threshold: 10 };
5925
- const backgroundHoverPlugin = (settings = {}) => {
5926
- let nearBar = null;
5927
- const { threshold } = { ...defaultSettings$4, ...settings };
5928
- return {
5929
- id: 'backgroundHover',
5930
- afterEvent: (chart, args) => {
5931
- const e = args.event;
5932
- let changed = false;
5933
- if (e.type === 'mousemove') {
5934
- const result = handleMouseMove(chart, nearBar, threshold || 10, e);
5935
- nearBar = result.nearBar;
5936
- changed = result.changed;
5937
- }
5938
- else if (e.type === 'mouseout') {
5939
- changed = handleMouseOut(chart, nearBar);
5940
- nearBar = null;
5941
- }
5942
- else if (e.type === 'click') {
5943
- handleClick(chart, nearBar);
5944
- // Clicks usually trigger their own updates, but if you need one:
5945
- // changed = true;
5946
- }
5947
- args.changed = changed;
5948
- },
5949
- beforeDraw: (chart) => {
5950
- const indexAxis = chart.options.indexAxis || 'x';
5951
- const activeBar = nearBar ? nearBar.element : getActiveBar(chart);
5952
- activeBar && drawHoverEffect(chart.ctx, chart.chartArea, activeBar, threshold || 10, indexAxis);
5953
- }
5954
- };
5955
- };
5956
-
5957
- const isMouseInsideCircle = (event, circleCenter, circleRadius) => {
5958
- const { clientX, clientY } = event;
5959
- const circleX = circleCenter.x;
5960
- const circleY = circleCenter.y;
5961
- // Calculate the distance between the click point and the circle center
5962
- const distance = Math.sqrt((clientX - circleX) ** 2 + (clientY - circleY) ** 2);
5963
- // Check if the distance is less than the circle radius
5964
- return distance <= circleRadius;
5965
- };
5966
- const circle = (ctx, x, y, color, radius) => {
5967
- ctx.beginPath();
5968
- ctx.arc(x, y, radius, 0, Math.PI * 2);
5969
- ctx.fillStyle = color;
5970
- ctx.fill();
5971
- };
5972
- const line = (ctx, startX, endX, y, color, lineWidth) => {
5973
- ctx.beginPath();
5974
- ctx.lineWidth = lineWidth;
5975
- ctx.strokeStyle = color;
5976
- ctx.moveTo(startX, y);
5977
- ctx.lineTo(endX, y);
5978
- ctx.stroke();
5979
- };
5980
- const defaultLollipopSettings = {
5981
- circleRadius: 4,
5982
- colorFn: m => m.backgroundColor,
5983
- isMouseInsideLollipopFn: () => null
5984
- };
5985
- const lollipopChartPlugin = settings => ({
5986
- id: 'lollipopChartPlugin',
5987
- afterDatasetsDraw: (chart) => {
5988
- if (!chart.data.datasets?.length) {
5989
- return;
5990
- }
5991
- const { circleRadius, lineWidth, colorFn, isMouseInsideLollipopFn, valueFn } = {
5992
- ...defaultLollipopSettings,
5993
- ...(settings ?? {})
5994
- };
5995
- const { ctx } = chart;
5996
- ctx.save();
5997
- const clickableDictionary = [];
5998
- chart.data.datasets.forEach((dataset, datasetIndex) => {
5999
- const meta = chart.getDatasetMeta(datasetIndex);
6000
- // Skip hidden datasets
6001
- if (meta.hidden)
6002
- return;
6003
- ctx.save();
6004
- const elements = meta.data;
6005
- elements
6006
- .filter(element => !element.skip)
6007
- .forEach((element, index) => {
6008
- const { base, x, y } = element;
6009
- const color = colorFn(element.options, index);
6010
- const data = dataset.data[index];
6011
- const overrideValue = valueFn?.(dataset, index, chart);
6012
- const xValue = isUndefined(overrideValue) ? x : chart.scales.x.getPixelForValue(overrideValue);
6013
- clickableDictionary.push((clientX, clientY) => isMouseInsideCircle({ clientX, clientY }, { x: xValue, y }, circleRadius));
6014
- circle(ctx, xValue, y, color, circleRadius);
6015
- // draw the line between the 2 cricles if necessary
6016
- [Array.isArray(data), !!lineWidth].every(Boolean) && line(ctx, base, x, y, color, lineWidth);
6017
- // when drawing from the data, draw both ends if its an array
6018
- if (isUndefined(overrideValue)) {
6019
- clickableDictionary.push((clientX, clientY) => isMouseInsideCircle({ clientX, clientY }, { x: base, y }, circleRadius));
6020
- Array.isArray(data) && circle(ctx, base, y, color, circleRadius);
6021
- }
6022
- });
6023
- ctx.restore();
6024
- });
6025
- isMouseInsideLollipopFn((x, y) => clickableDictionary.findIndex(fn => fn(x, y)));
6026
- }
6027
- });
6028
-
6029
- const defaultChartWidth = 800;
6030
- const defaultChartHeight = 600;
6031
- const extractEssentialChartConfig = (config) => ({
6032
- type: config.type || 'bar',
6033
- data: config.data,
6034
- options: {
6035
- indexAxis: config.options?.indexAxis,
6036
- responsive: false,
6037
- maintainAspectRatio: false,
6038
- devicePixelRatio: 1,
6039
- animation: false,
6040
- animations: {
6041
- colors: false,
6042
- x: false,
6043
- y: false
6044
- },
6045
- transitions: {
6046
- active: {
6047
- animation: {
6048
- duration: 0
6049
- }
6050
- }
6051
- },
6052
- plugins: {
6053
- legend: { display: false },
6054
- title: config.options.plugins?.title || { display: false }
6055
- },
6056
- scales: config.options.scales
6057
- }
6058
- });
6059
- const stretchSvg = (svgString, width, height) => {
6060
- const parser = new DOMParser();
6061
- const doc = parser.parseFromString(svgString, 'image/svg+xml');
6062
- const svgElement = doc.documentElement;
6063
- // Add 100px padding to ViewBox for overflow labels
6064
- svgElement.setAttribute('viewBox', `0 0 ${width + 100} ${height}`);
6065
- svgElement.setAttribute('width', '100%');
6066
- svgElement.setAttribute('height', '100%');
6067
- svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet');
6068
- return svgElement.outerHTML;
6069
- };
6070
- const convertToSvg = (config, metadata = {}) => new Promise((resolve, reject) => {
6071
- const width = metadata.width || defaultChartWidth;
6072
- const height = metadata.height || defaultChartHeight;
6073
- // 1. Create the SVG Context
6074
- const svgContext = new C2S(width, height);
6075
- svgContext.setTransform = () => { };
6076
- svgContext.resetTransform = () => { };
6077
- svgContext.roundRect = function (x, y, w, h) {
6078
- this.rect(x, y, w, h);
6079
- };
6080
- // 2. Mock the Canvas Element
6081
- const mockCanvas = document.createElement('canvas');
6082
- mockCanvas.width = width;
6083
- mockCanvas.height = height;
6084
- mockCanvas.style.width = `${width}px`;
6085
- mockCanvas.style.height = `${height}px`;
6086
- mockCanvas.getBoundingClientRect = () => ({
6087
- x: 0,
6088
- y: 0,
6089
- bottom: height,
6090
- height: height,
6091
- left: 0,
6092
- right: width,
6093
- top: 0,
6094
- width: width,
6095
- toJSON: () => { }
6096
- });
6097
- // Intercept getContext to return our SVG generator
6098
- mockCanvas.getContext = (type) => {
6099
- if (type === '2d') {
6100
- svgContext.canvas = mockCanvas;
6101
- return svgContext;
6102
- }
6103
- return null;
6104
- };
6105
- // 3. Prepare Config
6106
- const chartConfig = extractEssentialChartConfig(config);
6107
- // eslint-disable-next-line prefer-const
6108
- let chart;
6109
- chartConfig.options.animation = {
6110
- onComplete: () => {
6111
- try {
6112
- const svgString = svgContext.getSerializedSvg(true);
6113
- const finalSvg = stretchSvg(svgString, width, height);
6114
- chart.destroy();
6115
- resolve(finalSvg);
6116
- }
6117
- catch (e) {
6118
- reject(e);
6119
- }
6120
- }
6121
- };
6122
- // 4. Register Export-Specific Plugins
6123
- chartConfig.plugins = [
6124
- afterBarDrawPlugin({
6125
- textFn: ({ data }) => [toPrecision(Array.isArray(data) ? data[0] : data), metadata.units].filter(Boolean).join(' '),
6126
- emptyValueLabel: 'No data',
6127
- ...(metadata?.afterBarDrawConfig || {})
6128
- }),
6129
- metadata.lollipopConfig ? lollipopChartPlugin(metadata.lollipopConfig) : null
6130
- ].filter(Boolean);
6131
- chart = new Chart(mockCanvas, chartConfig);
6132
- });
6133
- const exportAsSVG = async (config, metadata) => {
6134
- try {
6135
- const content = await convertToSvg(config, metadata);
6136
- const blob = new Blob([content], { type: 'image/svg+xml;charset=utf-8' });
6137
- const url = URL.createObjectURL(blob);
6138
- downloadFile(url, metadata.fileName || 'chart-export.svg');
6139
- }
6140
- catch (error) {
6141
- console.error('Failed to export SVG', error);
6142
- }
6143
- };
6144
- const svgToUrl = (svgElement) => {
6145
- const serializer = new XMLSerializer();
6146
- let svgString = serializer.serializeToString(svgElement);
6147
- if (!svgString.includes('xmlns')) {
6148
- svgString = svgString.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
6149
- }
6150
- const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
6151
- return URL.createObjectURL(blob);
6152
- };
6153
- const downloadSvg = (svgElement) => downloadFile(svgToUrl(svgElement), 'chart.svg');
6154
- const downloadPng = async (svgElement, { width, height } = {}) => {
6155
- const canvas = document.createElement('canvas');
6156
- const ctx = canvas.getContext('2d');
6157
- const img = new Image();
6158
- // Set canvas dimensions based on SVG size
6159
- // (You might want to fetch getBBox() or use explicitly set width/height)
6160
- const svgSize = svgElement.getBoundingClientRect();
6161
- canvas.width = width || svgSize.width;
6162
- canvas.height = height || svgSize.height;
6163
- img.onload = () => {
6164
- if (ctx) {
6165
- // ctx.fillStyle = 'white';
6166
- ctx.fillRect(0, 0, canvas.width, canvas.height);
6167
- ctx.drawImage(img, 0, 0);
6168
- const pngUrl = canvas.toDataURL('image/png');
6169
- downloadFile(pngUrl, 'chart.png');
6220
+ return !!nearBar;
6221
+ };
6222
+ const handleClick = (chart, nearBar) => {
6223
+ if (nearBar)
6224
+ triggerBarClick(chart, nearBar);
6225
+ };
6226
+ const getActiveBar = (chart) => {
6227
+ const activeElements = chart.tooltip?.getActiveElements();
6228
+ return activeElements?.[0]?.element;
6229
+ };
6230
+ const defaultSettings$3 = { threshold: 10 };
6231
+ const backgroundHoverPlugin = (settings = {}) => {
6232
+ let nearBar = null;
6233
+ const { threshold } = { ...defaultSettings$3, ...settings };
6234
+ return {
6235
+ id: 'backgroundHover',
6236
+ afterEvent: (chart, args) => {
6237
+ const e = args.event;
6238
+ let changed = false;
6239
+ if (e.type === 'mousemove') {
6240
+ const result = handleMouseMove(chart, nearBar, threshold || 10, e);
6241
+ nearBar = result.nearBar;
6242
+ changed = result.changed;
6243
+ }
6244
+ else if (e.type === 'mouseout') {
6245
+ changed = handleMouseOut(chart, nearBar);
6246
+ nearBar = null;
6247
+ }
6248
+ else if (e.type === 'click') {
6249
+ handleClick(chart, nearBar);
6250
+ // Clicks usually trigger their own updates, but if you need one:
6251
+ // changed = true;
6252
+ }
6253
+ args.changed = changed;
6254
+ },
6255
+ beforeDraw: (chart) => {
6256
+ const indexAxis = chart.options.indexAxis || 'x';
6257
+ const activeBar = nearBar ? nearBar.element : getActiveBar(chart);
6258
+ activeBar && drawHoverEffect(chart.ctx, chart.chartArea, activeBar, threshold || 10, indexAxis);
6170
6259
  }
6171
6260
  };
6172
- img.src = svgToUrl(svgElement);
6173
6261
  };
6174
6262
 
6175
- const registerChart = (items = []) => () => {
6176
- Chart.register(BarController, LineController, CategoryScale, LinearScale, PointElement, BarElement, LineElement, Title, Tooltip, Legend, TimeScale, annotationPlugin, ...items);
6263
+ const isMouseInsideCircle = (event, circleCenter, circleRadius) => {
6264
+ const { clientX, clientY } = event;
6265
+ const circleX = circleCenter.x;
6266
+ const circleY = circleCenter.y;
6267
+ // Calculate the distance between the click point and the circle center
6268
+ const distance = Math.sqrt((clientX - circleX) ** 2 + (clientY - circleY) ** 2);
6269
+ // Check if the distance is less than the circle radius
6270
+ return distance <= circleRadius;
6177
6271
  };
6178
-
6179
- class ChartConfigurationDirective {
6180
- constructor() {
6181
- this._elementRef = inject(ElementRef);
6182
- this._observer = new ResizeObserver(() => this.resize());
6183
- /**
6184
- * The chart configuration.
6185
- * This is used to initialize the chart.
6186
- *
6187
- * @param configuration The chart configuration
6188
- */
6189
- this.chartConfiguration = input(...(ngDevMode ? [undefined, { debugName: "chartConfiguration" }] : []));
6190
- /**
6191
- * The container element of the chart.
6192
- * This is used to observe the size of the container and resize the chart accordingly. (chart.js update charts only on the window resize event)
6193
- * If not provided, the chart will not be resized.
6194
- *
6195
- * @param container The container element of the chart
6196
- */
6197
- this.chartContainer = input(...(ngDevMode ? [undefined, { debugName: "chartContainer" }] : []));
6198
- effect(onCleanup => {
6199
- const configuration = this.chartConfiguration();
6200
- untracked(() => {
6201
- this.removeChart();
6202
- if (configuration) {
6203
- this._chart = new Chart(this._elementRef.nativeElement, configuration);
6272
+ const circle = (ctx, x, y, color, radius) => {
6273
+ ctx.beginPath();
6274
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
6275
+ ctx.fillStyle = color;
6276
+ ctx.fill();
6277
+ };
6278
+ const line = (ctx, startX, endX, y, color, lineWidth) => {
6279
+ ctx.beginPath();
6280
+ ctx.lineWidth = lineWidth;
6281
+ ctx.strokeStyle = color;
6282
+ ctx.moveTo(startX, y);
6283
+ ctx.lineTo(endX, y);
6284
+ ctx.stroke();
6285
+ };
6286
+ const defaultLollipopSettings = {
6287
+ circleRadius: 4,
6288
+ colorFn: m => m.backgroundColor,
6289
+ isMouseInsideLollipopFn: () => null
6290
+ };
6291
+ const lollipopChartPlugin = settings => ({
6292
+ id: 'lollipopChartPlugin',
6293
+ afterDatasetsDraw: (chart) => {
6294
+ if (!chart.data.datasets?.length) {
6295
+ return;
6296
+ }
6297
+ const { circleRadius, lineWidth, colorFn, isMouseInsideLollipopFn, valueFn } = {
6298
+ ...defaultLollipopSettings,
6299
+ ...(settings ?? {})
6300
+ };
6301
+ const { ctx } = chart;
6302
+ ctx.save();
6303
+ const clickableDictionary = [];
6304
+ chart.data.datasets.forEach((dataset, datasetIndex) => {
6305
+ const meta = chart.getDatasetMeta(datasetIndex);
6306
+ // Skip hidden datasets
6307
+ if (meta.hidden)
6308
+ return;
6309
+ ctx.save();
6310
+ const elements = meta.data;
6311
+ elements
6312
+ .filter(element => !element.skip)
6313
+ .forEach((element, index) => {
6314
+ const { base, x, y } = element;
6315
+ const color = colorFn(element.options, index);
6316
+ const data = dataset.data[index];
6317
+ const overrideValue = valueFn?.(dataset, index, chart);
6318
+ const xValue = isUndefined(overrideValue) ? x : chart.scales.x.getPixelForValue(overrideValue);
6319
+ clickableDictionary.push((clientX, clientY) => isMouseInsideCircle({ clientX, clientY }, { x: xValue, y }, circleRadius));
6320
+ circle(ctx, xValue, y, color, circleRadius);
6321
+ // draw the line between the 2 cricles if necessary
6322
+ [Array.isArray(data), !!lineWidth].every(Boolean) && line(ctx, base, x, y, color, lineWidth);
6323
+ // when drawing from the data, draw both ends if its an array
6324
+ if (isUndefined(overrideValue)) {
6325
+ clickableDictionary.push((clientX, clientY) => isMouseInsideCircle({ clientX, clientY }, { x: base, y }, circleRadius));
6326
+ Array.isArray(data) && circle(ctx, base, y, color, circleRadius);
6204
6327
  }
6205
6328
  });
6206
- onCleanup(() => this.removeChart());
6207
- });
6208
- effect(onCleanup => {
6209
- if (this.chartContainer()) {
6210
- this._observer?.observe(this.chartContainer());
6211
- }
6212
- else {
6213
- this._observer?.disconnect();
6214
- }
6215
- onCleanup(() => this._observer?.disconnect());
6329
+ ctx.restore();
6216
6330
  });
6217
- }
6218
- removeChart() {
6219
- const chart = this._chart || Chart.getChart(this._elementRef.nativeElement);
6220
- chart?.destroy();
6221
- this._chart = null;
6222
- }
6223
- get chart() {
6224
- return this._chart;
6225
- }
6226
- resize() {
6227
- this._chart?.resize();
6228
- }
6229
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartConfigurationDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
6230
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.6", type: ChartConfigurationDirective, isStandalone: true, selector: "[chartConfiguration]", inputs: { chartConfiguration: { classPropertyName: "chartConfiguration", publicName: "chartConfiguration", isSignal: true, isRequired: false, transformFunction: null }, chartContainer: { classPropertyName: "chartContainer", publicName: "chartContainer", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["chart"], ngImport: i0 }); }
6231
- }
6232
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartConfigurationDirective, decorators: [{
6233
- type: Directive,
6234
- args: [{
6235
- selector: '[chartConfiguration]',
6236
- exportAs: 'chart'
6237
- }]
6238
- }], ctorParameters: () => [], propDecorators: { chartConfiguration: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartConfiguration", required: false }] }], chartContainer: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartContainer", required: false }] }] } });
6239
-
6240
- var ChartExportFormat;
6241
- (function (ChartExportFormat) {
6242
- ChartExportFormat["png"] = "Image";
6243
- ChartExportFormat["svg"] = "Vector image";
6244
- })(ChartExportFormat || (ChartExportFormat = {}));
6245
- const exportFormats = Object.entries(ChartExportFormat).map(([extension, label]) => ({ extension, label }));
6246
- class ChartExportButtonComponent {
6247
- constructor() {
6248
- this.modalService = inject(NgbModal);
6249
- this.modal = viewChild('modal', ...(ngDevMode ? [{ debugName: "modal" }] : []));
6250
- this.buttonClass = input('button is-small is-ghost is-p-2', ...(ngDevMode ? [{ debugName: "buttonClass" }] : []));
6251
- this.chart = input(...(ngDevMode ? [undefined, { debugName: "chart" }] : []));
6252
- this.config = input(...(ngDevMode ? [undefined, { debugName: "config" }] : []));
6253
- this.exportFormats = input(exportFormats, ...(ngDevMode ? [{ debugName: "exportFormats" }] : []));
6254
- this.chartExportFn = input(...(ngDevMode ? [undefined, { debugName: "chartExportFn" }] : []));
6255
- this.exportFormat = signal(undefined, ...(ngDevMode ? [{ debugName: "exportFormat" }] : []));
6256
- }
6257
- async defaultDownload(format) {
6258
- return format === 'svg' ? await this.chart().exportAsSvg(this.config()) : this.chart().exportAsPng();
6259
- }
6260
- async download() {
6261
- const downloadFn = this.chartExportFn() || this.defaultDownload.bind(this);
6262
- await downloadFn(this.exportFormat(), this.chart());
6263
- this.close();
6264
- }
6265
- open() {
6266
- this.modalService.open(this.modal());
6267
- }
6268
- close() {
6269
- this.exportFormat.set(undefined);
6270
- this.modalService.dismissAll();
6271
- }
6272
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartExportButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6273
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ChartExportButtonComponent, isStandalone: true, selector: "he-chart-export-button", inputs: { buttonClass: { classPropertyName: "buttonClass", publicName: "buttonClass", isSignal: true, isRequired: false, transformFunction: null }, chart: { classPropertyName: "chart", publicName: "chart", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, exportFormats: { classPropertyName: "exportFormats", publicName: "exportFormats", isSignal: true, isRequired: false, transformFunction: null }, chartExportFn: { classPropertyName: "chartExportFn", publicName: "chartExportFn", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "modal", first: true, predicate: ["modal"], descendants: true, isSignal: true }], ngImport: i0, template: "<button\n class=\"{{ buttonClass() }}\"\n type=\"button\"\n [ngbTooltip]=\"chart()?.exporting() ? '' : 'Download'\"\n placement=\"bottom\"\n (click)=\"open()\">\n <he-svg-icon name=\"download\" />\n</button>\n\n<ng-template #modal>\n <div class=\"modal is-active\">\n <div class=\"modal-background\"></div>\n <div class=\"modal-card\">\n <header class=\"modal-card-head\">\n <p class=\"modal-card-title\">Download Chart</p>\n <button class=\"delete is-small\" aria-label=\"close\" type=\"button\" (click)=\"close()\"></button>\n </header>\n <section class=\"modal-card-body\">\n <p class=\"has-text-secondary is-mb-2\">Download chart as:</p>\n <div class=\"is-flex is-flex-direction-column is-gap-4\">\n @for (format of exportFormats(); track format.extension) {\n <div class=\"is-flex is-gap-4\">\n <div class=\"field is-mb-0\">\n <input\n type=\"checkbox\"\n [id]=\"format.extension\"\n [checked]=\"exportFormat() === format.extension\"\n (change)=\"exportFormat.set(format.extension)\" />\n </div>\n <he-svg-icon name=\"image\" />\n <label class=\"has-text-grey-dark is-clickable\" [for]=\"format.extension\">\n {{ format.label }} (.{{ format.extension }})\n </label>\n </div>\n }\n </div>\n </section>\n <footer class=\"modal-card-foot\">\n <button class=\"button is-primary\" [disabled]=\"!exportFormat()\" (click)=\"download()\">\n @if (chart()?.exporting()) {\n <he-svg-icon name=\"loading\" animation=\"spin\" />\n } @else {\n <he-svg-icon name=\"download\" />\n }\n <span class=\"is-pl-2\">Download</span>\n </button>\n </footer>\n </div>\n </div>\n</ng-template>\n", styles: [""], dependencies: [{ kind: "component", type: HESvgIconComponent, selector: "he-svg-icon", inputs: ["name", "size", "animation"] }, { kind: "directive", type: NgbTooltip, selector: "[ngbTooltip]", inputs: ["animation", "autoClose", "placement", "popperOptions", "triggers", "positionTarget", "container", "disableTooltip", "tooltipClass", "tooltipContext", "openDelay", "closeDelay", "ngbTooltip"], outputs: ["shown", "hidden"], exportAs: ["ngbTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6274
- }
6275
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartExportButtonComponent, decorators: [{
6276
- type: Component$1,
6277
- args: [{ selector: 'he-chart-export-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HESvgIconComponent, NgbTooltip], template: "<button\n class=\"{{ buttonClass() }}\"\n type=\"button\"\n [ngbTooltip]=\"chart()?.exporting() ? '' : 'Download'\"\n placement=\"bottom\"\n (click)=\"open()\">\n <he-svg-icon name=\"download\" />\n</button>\n\n<ng-template #modal>\n <div class=\"modal is-active\">\n <div class=\"modal-background\"></div>\n <div class=\"modal-card\">\n <header class=\"modal-card-head\">\n <p class=\"modal-card-title\">Download Chart</p>\n <button class=\"delete is-small\" aria-label=\"close\" type=\"button\" (click)=\"close()\"></button>\n </header>\n <section class=\"modal-card-body\">\n <p class=\"has-text-secondary is-mb-2\">Download chart as:</p>\n <div class=\"is-flex is-flex-direction-column is-gap-4\">\n @for (format of exportFormats(); track format.extension) {\n <div class=\"is-flex is-gap-4\">\n <div class=\"field is-mb-0\">\n <input\n type=\"checkbox\"\n [id]=\"format.extension\"\n [checked]=\"exportFormat() === format.extension\"\n (change)=\"exportFormat.set(format.extension)\" />\n </div>\n <he-svg-icon name=\"image\" />\n <label class=\"has-text-grey-dark is-clickable\" [for]=\"format.extension\">\n {{ format.label }} (.{{ format.extension }})\n </label>\n </div>\n }\n </div>\n </section>\n <footer class=\"modal-card-foot\">\n <button class=\"button is-primary\" [disabled]=\"!exportFormat()\" (click)=\"download()\">\n @if (chart()?.exporting()) {\n <he-svg-icon name=\"loading\" animation=\"spin\" />\n } @else {\n <he-svg-icon name=\"download\" />\n }\n <span class=\"is-pl-2\">Download</span>\n </button>\n </footer>\n </div>\n </div>\n</ng-template>\n" }]
6278
- }], propDecorators: { modal: [{ type: i0.ViewChild, args: ['modal', { isSignal: true }] }], buttonClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonClass", required: false }] }], chart: [{ type: i0.Input, args: [{ isSignal: true, alias: "chart", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], exportFormats: [{ type: i0.Input, args: [{ isSignal: true, alias: "exportFormats", required: false }] }], chartExportFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartExportFn", required: false }] }] } });
6279
-
6280
- const defaultSettings$3 = Object.freeze({
6281
- options: {
6282
- responsive: true,
6283
- maintainAspectRatio: false,
6284
- plugins: {
6285
- legend: {
6286
- display: false
6287
- }
6288
- }
6331
+ isMouseInsideLollipopFn((x, y) => clickableDictionary.findIndex(fn => fn(x, y)));
6289
6332
  }
6290
6333
  });
6291
- const defaultTicksFont = {
6292
- family: 'Lato',
6293
- size: 13
6334
+
6335
+ const registerChart = (items = []) => () => {
6336
+ Chart.register(BarController, LineController, CategoryScale, LinearScale, PointElement, BarElement, LineElement, Title, Tooltip, Legend, TimeScale, annotationPlugin, ...items);
6294
6337
  };
6295
- class ChartComponent {
6296
- constructor() {
6297
- this.data = input(undefined, ...(ngDevMode ? [{ debugName: "data" }] : []));
6298
- this.config = input({}, ...(ngDevMode ? [{ debugName: "config" }] : []));
6299
- this.showExportButton = input(true, ...(ngDevMode ? [{ debugName: "showExportButton" }] : []));
6300
- this.exporting = signal(false, ...(ngDevMode ? [{ debugName: "exporting" }] : []));
6301
- this.chartRef = viewChild('chartRef', ...(ngDevMode ? [{ debugName: "chartRef" }] : []));
6302
- this.configuration = computed(() => ({
6303
- ...merge$1({}, defaultSettings$3, this.config()),
6304
- data: this.data()
6305
- }), ...(ngDevMode ? [{ debugName: "configuration" }] : []));
6306
- }
6307
- async exportAsSvg(config = {}) {
6308
- this.exporting.set(true);
6309
- await exportAsSVG(this.configuration(), {
6310
- width: 600,
6311
- height: 400,
6312
- fileName: 'chart.svg',
6313
- ...config
6314
- });
6315
- this.exporting.set(false);
6316
- }
6317
- exportAsPng() {
6318
- this.exporting.set(true);
6319
- const chart = this.chartRef();
6320
- const url = chart?.chart?.toBase64Image();
6321
- downloadFile(url, 'chart.png');
6322
- this.exporting.set(false);
6323
- }
6324
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6325
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ChartComponent, isStandalone: true, selector: "he-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, showExportButton: { classPropertyName: "showExportButton", publicName: "showExportButton", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "chartRef", first: true, predicate: ["chartRef"], descendants: true, isSignal: true }], exportAs: ["chart"], ngImport: i0, template: "<div class=\"is-relative h-100 | chart-container\" #container>\n @if (showExportButton()) {\n <div class=\"is-absolute | download\">\n <he-chart-export-button [chart]=\"this\" />\n </div>\n }\n\n <ng-content />\n\n <canvas #chartRef=\"chart\" [chartConfiguration]=\"configuration()\" [chartContainer]=\"container\"></canvas>\n</div>\n", styles: [":host{display:block;height:100%;overflow:visible}.chart-container{min-height:50px}.download{top:-12px;right:-10px}\n"], dependencies: [{ kind: "directive", type: ChartConfigurationDirective, selector: "[chartConfiguration]", inputs: ["chartConfiguration", "chartContainer"], exportAs: ["chart"] }, { kind: "component", type: ChartExportButtonComponent, selector: "he-chart-export-button", inputs: ["buttonClass", "chart", "config", "exportFormats", "chartExportFn"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6326
- }
6327
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartComponent, decorators: [{
6328
- type: Component$1,
6329
- args: [{ selector: 'he-chart', exportAs: 'chart', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ChartConfigurationDirective, ChartExportButtonComponent], template: "<div class=\"is-relative h-100 | chart-container\" #container>\n @if (showExportButton()) {\n <div class=\"is-absolute | download\">\n <he-chart-export-button [chart]=\"this\" />\n </div>\n }\n\n <ng-content />\n\n <canvas #chartRef=\"chart\" [chartConfiguration]=\"configuration()\" [chartContainer]=\"container\"></canvas>\n</div>\n", styles: [":host{display:block;height:100%;overflow:visible}.chart-container{min-height:50px}.download{top:-12px;right:-10px}\n"] }]
6330
- }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], showExportButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExportButton", required: false }] }], chartRef: [{ type: i0.ViewChild, args: ['chartRef', { isSignal: true }] }] } });
6331
6338
 
6332
6339
  class BarChartLegendComponent {
6333
6340
  constructor() {
@@ -6366,10 +6373,7 @@ const defaultSettings$2 = Object.freeze({
6366
6373
  title: {
6367
6374
  display: false,
6368
6375
  color: grey,
6369
- font: {
6370
- family: defaultTicksFont.family,
6371
- size: 14
6372
- }
6376
+ font: defaultTicksFont
6373
6377
  },
6374
6378
  ticks: {
6375
6379
  font: defaultTicksFont