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