@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
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
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
|
-
|
|
5678
|
-
|
|
5693
|
+
removeChart() {
|
|
5694
|
+
const chart = this._chart || Chart.getChart(this._elementRef.nativeElement);
|
|
5695
|
+
chart?.destroy();
|
|
5696
|
+
this._chart = null;
|
|
5679
5697
|
}
|
|
5680
|
-
|
|
5681
|
-
|
|
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
|
-
|
|
5739
|
-
const
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
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
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
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
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
};
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
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
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
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
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
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
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
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
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
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
|
-
|
|
6158
|
-
|
|
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
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
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
|
|
6183
|
-
|
|
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
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
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(
|
|
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: (
|
|
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),
|