@hestia-earth/ui-components 0.41.31 → 0.41.33

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,156 +5651,462 @@ 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);
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);
5804
6110
  ctx.stroke();
5805
6111
  };
5806
6112
  // UPDATED: Distance calculation using the new bounds
@@ -5828,513 +6134,207 @@ const isWithinHitArea = (x, y, bounds, threshold) => {
5828
6134
  };
5829
6135
  const checkBarProximity = (element, // Raw element
5830
6136
  index, datasetIndex, x, y, threshold, currentClosest, indexAxis) => {
5831
- const bar = element;
5832
- // V4 Safety check: Ensure the element has been drawn (has x/y)
5833
- if (bar.x == null || bar.y == null)
5834
- return;
5835
- const bounds = calculateBarBounds(bar, indexAxis);
5836
- if (!isWithinHitArea(x, y, bounds, threshold))
5837
- return;
5838
- const distance = calculateDistanceToBar(x, y, bounds);
5839
- if (distance < currentClosest.distance) {
5840
- currentClosest.distance = distance;
5841
- currentClosest.bar = {
5842
- datasetIndex,
5843
- index,
5844
- element: bar // Save the whole element
5845
- };
5846
- }
5847
- };
5848
- const findNearBar = (chart, x, y, threshold) => {
5849
- const closest = { bar: null, distance: Infinity };
5850
- // Determine orientation (horizontal bar vs vertical bar)
5851
- const indexAxis = chart.options.indexAxis || 'x';
5852
- chart.data.datasets?.forEach((dataset, datasetIndex) => {
5853
- // V4 Visibility check
5854
- if (!chart.isDatasetVisible(datasetIndex))
5855
- return;
5856
- const meta = chart.getDatasetMeta(datasetIndex);
5857
- meta.data.forEach((element, index) => {
5858
- // Skip hidden/skipped elements
5859
- if (element.hidden || element.skip)
5860
- return;
5861
- checkBarProximity(element, index, datasetIndex, x, y, threshold, closest, indexAxis);
5862
- });
5863
- });
5864
- return closest.distance <= threshold ? closest.bar : undefined;
5865
- };
5866
- const triggerBarClick = (chart, barData) => {
5867
- const activeElements = [
5868
- {
5869
- element: barData.element,
5870
- datasetIndex: barData.datasetIndex,
5871
- index: barData.index
5872
- }
5873
- ];
5874
- // 1. Highlight visually
5875
- chart.setActiveElements(activeElements);
5876
- // 2. Show Tooltip
5877
- chart.tooltip?.setActiveElements(activeElements, { x: 0, y: 0 });
5878
- // 3. Fire Click Callback
5879
- const onClickHandler = chart.options.onClick;
5880
- if (onClickHandler) {
5881
- const mockEvent = {
5882
- type: 'click',
5883
- x: barData.element.x,
5884
- y: barData.element.y,
5885
- chart: chart,
5886
- native: {
5887
- target: chart.canvas,
5888
- preventDefault: () => { },
5889
- stopPropagation: () => { }
5890
- }
5891
- };
5892
- onClickHandler.call(chart, mockEvent, activeElements, chart);
5893
- }
5894
- chart.update();
5895
- };
5896
- const handleMouseMove = (chart, nearBar, threshold, e) => {
5897
- // Return object
5898
- const newNearBar = findNearBar(chart, e.x, e.y, threshold);
5899
- const prevCursor = chart.canvas.style.cursor;
5900
- const newCursor = newNearBar ? 'pointer' : 'default';
5901
- // Optimization: Only touch DOM if necessary
5902
- if (prevCursor !== newCursor) {
5903
- chart.canvas.style.cursor = newCursor;
5904
- }
5905
- // Check if the "near bar" target has changed
5906
- const hasChanged = newNearBar?.datasetIndex !== nearBar?.datasetIndex || newNearBar?.index !== nearBar?.index;
5907
- return {
5908
- nearBar: hasChanged ? newNearBar || null : nearBar,
5909
- changed: hasChanged
5910
- };
5911
- };
5912
- const handleMouseOut = (chart, nearBar) => {
5913
- 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
6137
+ const bar = element;
6138
+ // V4 Safety check: Ensure the element has been drawn (has x/y)
6139
+ if (bar.x == null || bar.y == null)
6140
+ return;
6141
+ const bounds = calculateBarBounds(bar, indexAxis);
6142
+ if (!isWithinHitArea(x, y, bounds, threshold))
6143
+ return;
6144
+ const distance = calculateDistanceToBar(x, y, bounds);
6145
+ if (distance < currentClosest.distance) {
6146
+ currentClosest.distance = distance;
6147
+ currentClosest.bar = {
6148
+ datasetIndex,
6149
+ index,
6150
+ element: bar // Save the whole element
6151
+ };
6152
+ }
5984
6153
  };
5985
- const lollipopChartPlugin = settings => ({
5986
- id: 'lollipopChartPlugin',
5987
- afterDatasetsDraw: (chart) => {
5988
- if (!chart.data.datasets?.length) {
6154
+ const findNearBar = (chart, x, y, threshold) => {
6155
+ const closest = { bar: null, distance: Infinity };
6156
+ // Determine orientation (horizontal bar vs vertical bar)
6157
+ const indexAxis = chart.options.indexAxis || 'x';
6158
+ chart.data.datasets?.forEach((dataset, datasetIndex) => {
6159
+ // V4 Visibility check
6160
+ if (!chart.isDatasetVisible(datasetIndex))
5989
6161
  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)
6162
+ const meta = chart.getDatasetMeta(datasetIndex);
6163
+ meta.data.forEach((element, index) => {
6164
+ // Skip hidden/skipped elements
6165
+ if (element.hidden || element.skip)
6002
6166
  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();
6167
+ checkBarProximity(element, index, datasetIndex, x, y, threshold, closest, indexAxis);
6024
6168
  });
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 parseAfterDrawBarPlugins = (afterBarDrawConfig, units) => Array.isArray(afterBarDrawConfig)
6071
- ? afterBarDrawConfig.map(afterBarDrawPlugin)
6072
- : [
6073
- afterBarDrawPlugin({
6074
- textFn: ({ data }) => [toPrecision(Array.isArray(data) ? data[0] : data), units].filter(Boolean).join(' '),
6075
- emptyValueLabel: 'No data',
6076
- ...afterBarDrawConfig
6077
- })
6078
- ];
6079
- const convertToSvg = (config, metadata = {}) => new Promise((resolve, reject) => {
6080
- const width = metadata.width || defaultChartWidth;
6081
- const height = metadata.height || defaultChartHeight;
6082
- // 1. Create the SVG Context
6083
- const svgContext = new C2S(width, height);
6084
- svgContext.setTransform = () => { };
6085
- svgContext.resetTransform = () => { };
6086
- svgContext.roundRect = function (x, y, w, h) {
6087
- this.rect(x, y, w, h);
6088
- };
6089
- // 2. Mock the Canvas Element
6090
- const mockCanvas = document.createElement('canvas');
6091
- mockCanvas.width = width;
6092
- mockCanvas.height = height;
6093
- mockCanvas.style.width = `${width}px`;
6094
- mockCanvas.style.height = `${height}px`;
6095
- mockCanvas.getBoundingClientRect = () => ({
6096
- x: 0,
6097
- y: 0,
6098
- bottom: height,
6099
- height: height,
6100
- left: 0,
6101
- right: width,
6102
- top: 0,
6103
- width: width,
6104
- toJSON: () => { }
6105
6169
  });
6106
- // Intercept getContext to return our SVG generator
6107
- mockCanvas.getContext = (type) => {
6108
- if (type === '2d') {
6109
- svgContext.canvas = mockCanvas;
6110
- return svgContext;
6170
+ return closest.distance <= threshold ? closest.bar : undefined;
6171
+ };
6172
+ const triggerBarClick = (chart, barData) => {
6173
+ const activeElements = [
6174
+ {
6175
+ element: barData.element,
6176
+ datasetIndex: barData.datasetIndex,
6177
+ index: barData.index
6111
6178
  }
6112
- return null;
6113
- };
6114
- // 3. Prepare Config
6115
- const chartConfig = extractEssentialChartConfig(config);
6116
- // eslint-disable-next-line prefer-const
6117
- let chart;
6118
- chartConfig.options.animation = {
6119
- onComplete: () => {
6120
- try {
6121
- const svgString = svgContext.getSerializedSvg(true);
6122
- const finalSvg = stretchSvg(svgString, width, height);
6123
- chart.destroy();
6124
- resolve(finalSvg);
6125
- }
6126
- catch (e) {
6127
- reject(e);
6179
+ ];
6180
+ // 1. Highlight visually
6181
+ chart.setActiveElements(activeElements);
6182
+ // 2. Show Tooltip
6183
+ chart.tooltip?.setActiveElements(activeElements, { x: 0, y: 0 });
6184
+ // 3. Fire Click Callback
6185
+ const onClickHandler = chart.options.onClick;
6186
+ if (onClickHandler) {
6187
+ const mockEvent = {
6188
+ type: 'click',
6189
+ x: barData.element.x,
6190
+ y: barData.element.y,
6191
+ chart: chart,
6192
+ native: {
6193
+ target: chart.canvas,
6194
+ preventDefault: () => { },
6195
+ stopPropagation: () => { }
6128
6196
  }
6129
- }
6130
- };
6131
- // 4. Register Export-Specific Plugins
6132
- chartConfig.plugins = [
6133
- ...parseAfterDrawBarPlugins(metadata.afterBarDrawConfig || {}, metadata.units),
6134
- metadata.lollipopConfig ? lollipopChartPlugin(metadata.lollipopConfig) : null
6135
- ]
6136
- .filter(Boolean)
6137
- .flat();
6138
- chart = new Chart(mockCanvas, chartConfig);
6139
- });
6140
- const exportAsSVG = async (config, metadata) => {
6141
- try {
6142
- const content = await convertToSvg(config, metadata);
6143
- const blob = new Blob([content], { type: 'image/svg+xml;charset=utf-8' });
6144
- const url = URL.createObjectURL(blob);
6145
- downloadFile(url, metadata.fileName || 'chart-export.svg');
6146
- }
6147
- catch (error) {
6148
- console.error('Failed to export SVG', error);
6197
+ };
6198
+ onClickHandler.call(chart, mockEvent, activeElements, chart);
6149
6199
  }
6200
+ chart.update();
6150
6201
  };
6151
- const svgToUrl = (svgElement) => {
6152
- const serializer = new XMLSerializer();
6153
- let svgString = serializer.serializeToString(svgElement);
6154
- if (!svgString.includes('xmlns')) {
6155
- svgString = svgString.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
6202
+ const handleMouseMove = (chart, nearBar, threshold, e) => {
6203
+ // Return object
6204
+ const newNearBar = findNearBar(chart, e.x, e.y, threshold);
6205
+ const prevCursor = chart.canvas.style.cursor;
6206
+ const newCursor = newNearBar ? 'pointer' : 'default';
6207
+ // Optimization: Only touch DOM if necessary
6208
+ if (prevCursor !== newCursor) {
6209
+ chart.canvas.style.cursor = newCursor;
6156
6210
  }
6157
- const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
6158
- return URL.createObjectURL(blob);
6211
+ // Check if the "near bar" target has changed
6212
+ const hasChanged = newNearBar?.datasetIndex !== nearBar?.datasetIndex || newNearBar?.index !== nearBar?.index;
6213
+ return {
6214
+ nearBar: hasChanged ? newNearBar || null : nearBar,
6215
+ changed: hasChanged
6216
+ };
6159
6217
  };
6160
- const downloadSvg = (svgElement) => downloadFile(svgToUrl(svgElement), 'chart.svg');
6161
- const downloadPng = async (svgElement, { width, height } = {}) => {
6162
- const canvas = document.createElement('canvas');
6163
- const ctx = canvas.getContext('2d');
6164
- const img = new Image();
6165
- // Set canvas dimensions based on SVG size
6166
- // (You might want to fetch getBBox() or use explicitly set width/height)
6167
- const svgSize = svgElement.getBoundingClientRect();
6168
- canvas.width = width || svgSize.width;
6169
- canvas.height = height || svgSize.height;
6170
- img.onload = () => {
6171
- if (ctx) {
6172
- // ctx.fillStyle = 'white';
6173
- ctx.fillRect(0, 0, canvas.width, canvas.height);
6174
- ctx.drawImage(img, 0, 0);
6175
- const pngUrl = canvas.toDataURL('image/png');
6176
- downloadFile(pngUrl, 'chart.png');
6218
+ const handleMouseOut = (chart, nearBar) => {
6219
+ chart.canvas.style.cursor = 'default';
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);
6177
6259
  }
6178
6260
  };
6179
- img.src = svgToUrl(svgElement);
6180
6261
  };
6181
6262
 
6182
- const registerChart = (items = []) => () => {
6183
- 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;
6184
6271
  };
6185
-
6186
- class ChartConfigurationDirective {
6187
- constructor() {
6188
- this._elementRef = inject(ElementRef);
6189
- this._observer = new ResizeObserver(() => this.resize());
6190
- /**
6191
- * The chart configuration.
6192
- * This is used to initialize the chart.
6193
- *
6194
- * @param configuration The chart configuration
6195
- */
6196
- this.chartConfiguration = input(...(ngDevMode ? [undefined, { debugName: "chartConfiguration" }] : []));
6197
- /**
6198
- * The container element of the chart.
6199
- * 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)
6200
- * If not provided, the chart will not be resized.
6201
- *
6202
- * @param container The container element of the chart
6203
- */
6204
- this.chartContainer = input(...(ngDevMode ? [undefined, { debugName: "chartContainer" }] : []));
6205
- effect(onCleanup => {
6206
- const configuration = this.chartConfiguration();
6207
- untracked(() => {
6208
- this.removeChart();
6209
- if (configuration) {
6210
- 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);
6211
6327
  }
6212
6328
  });
6213
- onCleanup(() => this.removeChart());
6214
- });
6215
- effect(onCleanup => {
6216
- if (this.chartContainer()) {
6217
- this._observer?.observe(this.chartContainer());
6218
- }
6219
- else {
6220
- this._observer?.disconnect();
6221
- }
6222
- onCleanup(() => this._observer?.disconnect());
6329
+ ctx.restore();
6223
6330
  });
6224
- }
6225
- removeChart() {
6226
- const chart = this._chart || Chart.getChart(this._elementRef.nativeElement);
6227
- chart?.destroy();
6228
- this._chart = null;
6229
- }
6230
- get chart() {
6231
- return this._chart;
6232
- }
6233
- resize() {
6234
- this._chart?.resize();
6235
- }
6236
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartConfigurationDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
6237
- 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 }); }
6238
- }
6239
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartConfigurationDirective, decorators: [{
6240
- type: Directive,
6241
- args: [{
6242
- selector: '[chartConfiguration]',
6243
- exportAs: 'chart'
6244
- }]
6245
- }], ctorParameters: () => [], propDecorators: { chartConfiguration: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartConfiguration", required: false }] }], chartContainer: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartContainer", required: false }] }] } });
6246
-
6247
- var ChartExportFormat;
6248
- (function (ChartExportFormat) {
6249
- ChartExportFormat["png"] = "Image";
6250
- ChartExportFormat["svg"] = "Vector image";
6251
- })(ChartExportFormat || (ChartExportFormat = {}));
6252
- const exportFormats = Object.entries(ChartExportFormat).map(([extension, label]) => ({ extension, label }));
6253
- class ChartExportButtonComponent {
6254
- constructor() {
6255
- this.modalService = inject(NgbModal);
6256
- this.modal = viewChild('modal', ...(ngDevMode ? [{ debugName: "modal" }] : []));
6257
- this.buttonClass = input('button is-small is-ghost is-p-2', ...(ngDevMode ? [{ debugName: "buttonClass" }] : []));
6258
- this.chart = input(...(ngDevMode ? [undefined, { debugName: "chart" }] : []));
6259
- this.config = input(...(ngDevMode ? [undefined, { debugName: "config" }] : []));
6260
- this.exportFormats = input(exportFormats, ...(ngDevMode ? [{ debugName: "exportFormats" }] : []));
6261
- this.chartExportFn = input(...(ngDevMode ? [undefined, { debugName: "chartExportFn" }] : []));
6262
- this.exportFormat = signal(undefined, ...(ngDevMode ? [{ debugName: "exportFormat" }] : []));
6263
- }
6264
- async defaultDownload(format) {
6265
- return format === 'svg' ? await this.chart().exportAsSvg(this.config()) : this.chart().exportAsPng();
6266
- }
6267
- async download() {
6268
- const downloadFn = this.chartExportFn() || this.defaultDownload.bind(this);
6269
- await downloadFn(this.exportFormat(), this.chart());
6270
- this.close();
6271
- }
6272
- open() {
6273
- this.modalService.open(this.modal());
6274
- }
6275
- close() {
6276
- this.exportFormat.set(undefined);
6277
- this.modalService.dismissAll();
6278
- }
6279
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartExportButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6280
- 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 }); }
6281
- }
6282
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartExportButtonComponent, decorators: [{
6283
- type: Component$1,
6284
- 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" }]
6285
- }], 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 }] }] } });
6286
-
6287
- const defaultSettings$3 = Object.freeze({
6288
- options: {
6289
- responsive: true,
6290
- maintainAspectRatio: false,
6291
- plugins: {
6292
- legend: {
6293
- display: false
6294
- }
6295
- }
6331
+ isMouseInsideLollipopFn((x, y) => clickableDictionary.findIndex(fn => fn(x, y)));
6296
6332
  }
6297
6333
  });
6298
- const defaultTicksFont = {
6299
- family: 'Lato',
6300
- size: 13
6334
+
6335
+ const registerChart = (items = []) => () => {
6336
+ Chart.register(BarController, LineController, CategoryScale, LinearScale, PointElement, BarElement, LineElement, Title, Tooltip, Legend, TimeScale, annotationPlugin, ...items);
6301
6337
  };
6302
- class ChartComponent {
6303
- constructor() {
6304
- this.data = input(undefined, ...(ngDevMode ? [{ debugName: "data" }] : []));
6305
- this.config = input({}, ...(ngDevMode ? [{ debugName: "config" }] : []));
6306
- this.showExportButton = input(true, ...(ngDevMode ? [{ debugName: "showExportButton" }] : []));
6307
- this.exporting = signal(false, ...(ngDevMode ? [{ debugName: "exporting" }] : []));
6308
- this.chartRef = viewChild('chartRef', ...(ngDevMode ? [{ debugName: "chartRef" }] : []));
6309
- this.configuration = computed(() => ({
6310
- ...merge$1({}, defaultSettings$3, this.config()),
6311
- data: this.data()
6312
- }), ...(ngDevMode ? [{ debugName: "configuration" }] : []));
6313
- }
6314
- async exportAsSvg(config = {}) {
6315
- this.exporting.set(true);
6316
- await exportAsSVG(this.configuration(), {
6317
- width: 600,
6318
- height: 400,
6319
- fileName: 'chart.svg',
6320
- ...config
6321
- });
6322
- this.exporting.set(false);
6323
- }
6324
- exportAsPng() {
6325
- this.exporting.set(true);
6326
- const chart = this.chartRef();
6327
- const url = chart?.chart?.toBase64Image();
6328
- downloadFile(url, 'chart.png');
6329
- this.exporting.set(false);
6330
- }
6331
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6332
- 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 }); }
6333
- }
6334
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChartComponent, decorators: [{
6335
- type: Component$1,
6336
- 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"] }]
6337
- }], 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 }] }] } });
6338
6338
 
6339
6339
  class BarChartLegendComponent {
6340
6340
  constructor() {
@@ -6373,10 +6373,7 @@ const defaultSettings$2 = Object.freeze({
6373
6373
  title: {
6374
6374
  display: false,
6375
6375
  color: grey,
6376
- font: {
6377
- family: defaultTicksFont.family,
6378
- size: 14
6379
- }
6376
+ font: defaultTicksFont
6380
6377
  },
6381
6378
  ticks: {
6382
6379
  font: defaultTicksFont
@@ -11131,6 +11128,9 @@ const customErrorMessage = {
11131
11128
  : ''}`,
11132
11129
  'invalid water salinity': ({ params }) => `The water type ${params?.current} is not consistent with the Water salinity you specified.
11133
11130
  Make sure the Water salinity value is correct and either fix it or update the water type accordingly.`,
11131
+ 'measurements for the same termType must be consistent': ({ params }) => `The information you have provided for ${code(params.termType)} Measurements is not consistent across all Measurements.
11132
+ Please ensure that all your ${code(params.termType)} Measurements have a ${code('dates')} field if at least one ${code(params.termType)} Measurement has a ${code('dates')} field specified.
11133
+ Please also ensure that all your ${code(params.termType)} Measurements have ${code('depthUpper')} and ${code('depthLower')} fields if at least one ${code(params.termType)} Measurement has ${code('depthUpper')} and ${code('depthLower')} fields specified.`,
11134
11134
  'must add substrate inputs': () => 'The substrate must be specified when a substrate-based protected cropping system has been added.',
11135
11135
  'should be equal to cycleDuration for crop': ({ params }) => `For temporary crop production Cycles, ${schemaLink('Cycle#siteDuration', 'siteDuration')} must represent the period from harvest of the previous crop to harvest of the current crop.
11136
11136
  Here, you have stated that ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} represents the period from
@@ -14350,6 +14350,10 @@ class FilterAccordionComponent extends ControlValueAccessor {
14350
14350
  this.data = input(...(ngDevMode ? [undefined, { debugName: "data" }] : []));
14351
14351
  this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
14352
14352
  this.showGroupCount = input(true, ...(ngDevMode ? [{ debugName: "showGroupCount" }] : []));
14353
+ /**
14354
+ * Expand by default the first group in the data, if relevant.
14355
+ */
14356
+ this.expandFirstGroup = input(false, ...(ngDevMode ? [{ debugName: "expandFirstGroup" }] : []));
14353
14357
  this.selectionChanged = output();
14354
14358
  this.selectControl = new FormControl([]);
14355
14359
  this.panelStates = {};
@@ -14367,11 +14371,15 @@ class FilterAccordionComponent extends ControlValueAccessor {
14367
14371
  this.group = computed(() => ({ options: this.filterStore.filteredData() }), ...(ngDevMode ? [{ debugName: "group" }] : []));
14368
14372
  this.disabledValues = computed(() => disabledValues(this.data()), ...(ngDevMode ? [{ debugName: "disabledValues" }] : []));
14369
14373
  effect(() => this.data() && this.filterStore.setData(this.data()));
14370
- effect(() => (this.panelStates = Object.fromEntries(allGroups(this.data()).map(({ label }) => [
14374
+ effect(() => (this.panelStates = Object.fromEntries(
14375
+ /* eslint-disable-next-line complexity */
14376
+ allGroups(this.data()).map(({ label, type }, index) => [
14371
14377
  label,
14372
14378
  {
14373
14379
  id: label,
14374
- expanded: (this.maintainPanelStates() && this.panelStates[label]?.expanded) || false,
14380
+ expanded: (type === 'group' && index === 0 && this.expandFirstGroup()) ||
14381
+ (this.maintainPanelStates() && this.panelStates[label]?.expanded) ||
14382
+ false,
14375
14383
  searchTerm: (this.maintainPanelStates() && this.panelStates[label]?.searchTerm) || ''
14376
14384
  }
14377
14385
  ]))));
@@ -14454,7 +14462,7 @@ class FilterAccordionComponent extends ControlValueAccessor {
14454
14462
  return this.disabled() || item.disabled || parentDisabled;
14455
14463
  }
14456
14464
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: FilterAccordionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
14457
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: FilterAccordionComponent, isStandalone: true, selector: "he-filter-accordion", inputs: { showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, showGlobalSearch: { classPropertyName: "showGlobalSearch", publicName: "showGlobalSearch", isSignal: true, isRequired: false, transformFunction: null }, globalSearchPlaceholder: { classPropertyName: "globalSearchPlaceholder", publicName: "globalSearchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, showClearAll: { classPropertyName: "showClearAll", publicName: "showClearAll", isSignal: true, isRequired: false, transformFunction: null }, preserveOptionsOnSelection: { classPropertyName: "preserveOptionsOnSelection", publicName: "preserveOptionsOnSelection", isSignal: true, isRequired: false, transformFunction: null }, maintainPanelStates: { classPropertyName: "maintainPanelStates", publicName: "maintainPanelStates", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showGroupCount: { classPropertyName: "showGroupCount", publicName: "showGroupCount", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChanged: "selectionChanged" }, providers: [
14465
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: FilterAccordionComponent, isStandalone: true, selector: "he-filter-accordion", inputs: { showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, showGlobalSearch: { classPropertyName: "showGlobalSearch", publicName: "showGlobalSearch", isSignal: true, isRequired: false, transformFunction: null }, globalSearchPlaceholder: { classPropertyName: "globalSearchPlaceholder", publicName: "globalSearchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, showClearAll: { classPropertyName: "showClearAll", publicName: "showClearAll", isSignal: true, isRequired: false, transformFunction: null }, preserveOptionsOnSelection: { classPropertyName: "preserveOptionsOnSelection", publicName: "preserveOptionsOnSelection", isSignal: true, isRequired: false, transformFunction: null }, maintainPanelStates: { classPropertyName: "maintainPanelStates", publicName: "maintainPanelStates", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showGroupCount: { classPropertyName: "showGroupCount", publicName: "showGroupCount", isSignal: true, isRequired: false, transformFunction: null }, expandFirstGroup: { classPropertyName: "expandFirstGroup", publicName: "expandFirstGroup", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChanged: "selectionChanged" }, providers: [
14458
14466
  {
14459
14467
  provide: NG_VALUE_ACCESSOR,
14460
14468
  useExisting: forwardRef(() => FilterAccordionComponent),
@@ -14497,7 +14505,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
14497
14505
  transition(':leave', [animate('200ms ease-out', style({ maxHeight: '0', opacity: 0 }))])
14498
14506
  ])
14499
14507
  ], template: "<div class=\"is-flex is-flex-direction-column is-gap-12 w-100\">\n @if (showHeader()) {\n <div class=\"is-flex is-flex-direction-column is-gap-12 is-justify-content-space-between is-align-items-flex-start\">\n @if (title()) {\n <div class=\"is-flex is-align-items-center\">\n <span class=\"has-text-secondary has-text-weight-semibold\">{{ title() }}</span>\n @if (tooltip()) {\n <he-svg-icon\n name=\"info-circle\"\n class=\"is-ml-1 is-mb-1 has-text-grey-light is-clickable\"\n [ngbTooltip]=\"tooltip()\"\n placement=\"right\"\n triggers=\"hover\"\n size=\"16\"\n container=\"body\" />\n }\n </div>\n }\n\n <ng-content select=\"[header-content]\" />\n\n @if (showClearAll()) {\n <span\n class=\"is-size-7 has-text-weight-normal is-italic | clear-button\"\n [class.is-clickable]=\"hasSelectedValues()\"\n (click)=\"hasSelectedValues() && clearAll()\">\n Clear all\n </span>\n }\n\n @if (showGlobalSearch()) {\n <div class=\"field is-mb-0 w-100\">\n <div class=\"control is-expanded has-icons-right\">\n <input\n type=\"text\"\n class=\"input is-secondary is-small search-input\"\n [placeholder]=\"globalSearchPlaceholder()\"\n [(ngModel)]=\"search\"\n [disabled]=\"disabled()\"\n (input)=\"onSearch($event.target.value, group())\" />\n <a class=\"icon has-text-secondary is-small is-right\" [class.is-hidden]=\"!search()\" (click)=\"clearSearch()\">\n <he-svg-icon name=\"xmark\" />\n </a>\n <a class=\"icon has-text-secondary is-small is-right\" [class.is-hidden]=\"search()\">\n <he-svg-icon name=\"search\" />\n </a>\n </div>\n </div>\n }\n </div>\n }\n\n <div class=\"has-border-top has-border-bottom\">\n <ng-container\n *ngTemplateOutlet=\"\n itemsList;\n context: { group: group(), parentSearch: search(), showNoResults: showGlobalSearch() }\n \" />\n </div>\n</div>\n\n<ng-template #itemsList let-group=\"group\" let-parentSearch=\"parentSearch\" let-showNoResults=\"showNoResults\">\n @for (item of group | filterAccordionGroup: parentSearch; track item.trackId || item.label; let lastItem = $last) {\n @if (item.type === 'group') {\n @let groupState = panelStates[item.label];\n @if (groupState) {\n <div [class.has-border-bottom]=\"!lastItem\" [class.is-active]=\"groupState.expanded\">\n <div\n class=\"is-flex is-align-items-center is-clickable has-background-hover is-py-1 | accordion-row\"\n (click)=\"groupState.expanded = !groupState.expanded\">\n <ng-container *ngTemplateOutlet=\"groupOptionLabel; context: { item, groupState }\" />\n </div>\n\n @if (groupState.expanded) {\n <div class=\"is-overflow-hidden\" [@slideDownUp]=\"groupState.expanded\">\n @if (!showGlobalSearch() && getDirectOptionsCount(item) >= 5) {\n <div class=\"field is-mb-0 pb-2 has-border-bottom\">\n <div class=\"control is-expanded has-icons-right pl-5\">\n <input\n type=\"text\"\n class=\"input is-secondary is-small search-input pl-2\"\n placeholder=\"Search {{ item.label }}\"\n [value]=\"groupState.searchTerm || ''\"\n [disabled]=\"isItemEffectivelyDisabled(item)\"\n (input)=\"groupState.searchTerm = $event.target.value; onSearch($event.target.value, item)\" />\n <a\n class=\"icon has-text-secondary is-small is-right\"\n [class.is-hidden]=\"!groupState.searchTerm\"\n (click)=\"groupState.searchTerm = ''\">\n <he-svg-icon name=\"xmark\" />\n </a>\n <a class=\"icon is-small has-text-secondary is-right\" [class.is-hidden]=\"groupState.searchTerm\">\n <he-svg-icon name=\"search\" />\n </a>\n </div>\n </div>\n }\n\n <div class=\"pl-5\">\n <ng-container\n *ngTemplateOutlet=\"\n itemsList;\n context: {\n group: item,\n parentSearch: parentSearch || groupState.searchTerm,\n showNoResults: !showGlobalSearch()\n }\n \" />\n </div>\n </div>\n }\n </div>\n }\n } @else {\n <div\n class=\"is-flex is-justify-content-space-between is-py-1 | accordion-row\"\n [class.has-border-bottom]=\"!lastItem\">\n <ng-container *ngTemplateOutlet=\"optionLabel; context: { item }\" />\n </div>\n }\n } @empty {\n @if (parentSearch && showNoResults) {\n <div class=\"px-6 py-4 has-text-grey is-size-7 has-text-centered is-italic\">\n No results found for \"{{ parentSearch }}\"\n </div>\n }\n }\n</ng-template>\n\n<ng-template #itemLabel let-item=\"item\" let-count=\"count\">\n <span class=\"is-flex is-gap-4 is-flex-wrap-wrap is-flex-grow-1 is-size-7 has-text-grey-dark has-text-weight-medium\">\n <span>{{ item.label }}</span>\n\n @if (item.tooltip) {\n <he-svg-icon\n name=\"info-circle\"\n class=\"has-text-grey-light\"\n [ngbTooltip]=\"item.tooltip\"\n placement=\"top\"\n triggers=\"hover\"\n size=\"16\"\n container=\"body\" />\n }\n\n @if (isNumber(count)) {\n <span class=\"has-text-grey-light is-size-7\">({{ count }})</span>\n }\n </span>\n</ng-template>\n\n<ng-template #groupOptionLabel let-item=\"item\" let-groupState=\"groupState\">\n @let options = optionsFromGroup(item);\n\n <label\n class=\"checkbox is-flex is-justify-content-center is-align-items-center is-fullwidth\"\n (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n class=\"mr-3 is-flex-shrink-0\"\n [checked]=\"isGroupFullySelected(options)\"\n [indeterminate]=\"isGroupPartiallySelected(options)\"\n (change)=\"toggleGroup(options)\"\n [disabled]=\"isItemEffectivelyDisabled(item)\" />\n </label>\n\n <ng-container *ngTemplateOutlet=\"itemLabel; context: { item, count: showGroupCount() ? item.count : undefined }\" />\n\n <he-svg-icon\n class=\"has-text-secondary transition-transform\"\n [name]=\"groupState.expanded ? 'chevron-up' : 'chevron-down'\" />\n</ng-template>\n\n<ng-template #optionLabel let-item=\"item\" let-parentDisabled=\"parentDisabled\">\n <label\n class=\"checkbox is-flex is-justify-content-center is-align-items-center is-fullwidth\"\n (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n class=\"mr-3 is-flex-shrink-0\"\n [checked]=\"isOptionSelected(item.value)\"\n (change)=\"toggleOption(item.value)\"\n [disabled]=\"isItemEffectivelyDisabled(item, parentDisabled)\" />\n\n <ng-container *ngTemplateOutlet=\"itemLabel; context: { item, count: item.count }\" />\n </label>\n</ng-template>\n", styles: [".clear-button{color:#b5b5b5}.clear-button.is-clickable{color:#4c7194}.accordion-row{min-height:25px}.control{height:28px}.control.has-icons-left .icon,.control.has-icons-right .icon{height:28px!important}.search-input{height:28px;border:1px solid #dbe3ea;border-radius:3px;font-weight:400;line-height:17px;box-shadow:none!important}.has-background-hover-light:hover{background-color:#fafafa}.has-border-top{border-top:1px solid #dbe3ea}.has-border-bottom{border-bottom:1px solid #dbe3ea}.transition-transform{transition:transform .2s ease-out}input[type=checkbox]{height:14px;width:14px;border:1px solid #b5b5b5;border-radius:3px;appearance:none;-webkit-appearance:none;background-color:transparent;accent-color:transparent}input[type=checkbox]:disabled{background-color:#f5f5f5}input[type=checkbox]:checked{background-color:#4c7194;accent-color:#4c7194;appearance:auto;-webkit-appearance:auto}\n"] }]
14500
- }], ctorParameters: () => [], propDecorators: { showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], showGlobalSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGlobalSearch", required: false }] }], globalSearchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalSearchPlaceholder", required: false }] }], showClearAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearAll", required: false }] }], preserveOptionsOnSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "preserveOptionsOnSelection", required: false }] }], maintainPanelStates: [{ type: i0.Input, args: [{ isSignal: true, alias: "maintainPanelStates", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showGroupCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGroupCount", required: false }] }], selectionChanged: [{ type: i0.Output, args: ["selectionChanged"] }] } });
14508
+ }], ctorParameters: () => [], propDecorators: { showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], showGlobalSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGlobalSearch", required: false }] }], globalSearchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalSearchPlaceholder", required: false }] }], showClearAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearAll", required: false }] }], preserveOptionsOnSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "preserveOptionsOnSelection", required: false }] }], maintainPanelStates: [{ type: i0.Input, args: [{ isSignal: true, alias: "maintainPanelStates", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showGroupCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGroupCount", required: false }] }], expandFirstGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandFirstGroup", required: false }] }], selectionChanged: [{ type: i0.Output, args: ["selectionChanged"] }] } });
14501
14509
 
14502
14510
  const termTypeName = (select) => [
14503
14511
  keyToLabel(select?.termType),