@hestia-earth/ui-components 0.41.31 → 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,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
|
|
@@ -5826,515 +6132,209 @@ const isWithinHitArea = (x, y, bounds, threshold) => {
|
|
|
5826
6132
|
const hitBottom = bounds.barBottom + threshold;
|
|
5827
6133
|
return x >= hitLeft && x <= hitRight && y >= hitTop && y <= hitBottom;
|
|
5828
6134
|
};
|
|
5829
|
-
const checkBarProximity = (element, // Raw element
|
|
5830
|
-
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
|
|
6135
|
+
const checkBarProximity = (element, // Raw element
|
|
6136
|
+
index, datasetIndex, x, y, threshold, currentClosest, indexAxis) => {
|
|
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
|