@fivem-nui/svelte-components 0.0.0-stage → 1.0.0
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.
- package/README.md +101 -2
- package/dist/components/actions/NuiButton.svelte +292 -0
- package/dist/components/actions/NuiButton.svelte.d.ts +60 -0
- package/dist/components/data/NuiBadge.svelte +277 -0
- package/dist/components/data/NuiBadge.svelte.d.ts +74 -0
- package/dist/components/data/NuiCodeBlock.svelte +256 -0
- package/dist/components/data/NuiCodeBlock.svelte.d.ts +49 -0
- package/dist/components/data/NuiList.svelte +341 -0
- package/dist/components/data/NuiList.svelte.d.ts +52 -0
- package/dist/components/data/NuiProgressBar.svelte +264 -0
- package/dist/components/data/NuiProgressBar.svelte.d.ts +51 -0
- package/dist/components/data/NuiRichText.svelte +260 -0
- package/dist/components/data/NuiRichText.svelte.d.ts +33 -0
- package/dist/components/data/NuiTable.svelte +470 -0
- package/dist/components/data/NuiTable.svelte.d.ts +53 -0
- package/dist/components/data/chart/NuiChart.svelte +384 -0
- package/dist/components/data/chart/NuiChart.svelte.d.ts +49 -0
- package/dist/components/data/chart/color.d.ts +9 -0
- package/dist/components/data/chart/color.js +78 -0
- package/dist/components/data/chart/index.d.ts +10 -0
- package/dist/components/data/chart/index.js +10 -0
- package/dist/components/data/chart/registry.d.ts +14 -0
- package/dist/components/data/chart/registry.js +53 -0
- package/dist/components/data/chart/renderers/bar.d.ts +8 -0
- package/dist/components/data/chart/renderers/bar.js +79 -0
- package/dist/components/data/chart/renderers/base.d.ts +18 -0
- package/dist/components/data/chart/renderers/base.js +10 -0
- package/dist/components/data/chart/renderers/line.d.ts +8 -0
- package/dist/components/data/chart/renderers/line.js +89 -0
- package/dist/components/data/chart/renderers/pie.d.ts +10 -0
- package/dist/components/data/chart/renderers/pie.js +55 -0
- package/dist/components/data/chart/scale.d.ts +22 -0
- package/dist/components/data/chart/scale.js +101 -0
- package/dist/components/data/chart/types.d.ts +84 -0
- package/dist/components/data/chart/types.js +1 -0
- package/dist/components/data/chart/validator.d.ts +10 -0
- package/dist/components/data/chart/validator.js +82 -0
- package/dist/components/forms/NuiCheckbox.svelte +218 -0
- package/dist/components/forms/NuiCheckbox.svelte.d.ts +37 -0
- package/dist/components/forms/NuiDatepicker.svelte +755 -0
- package/dist/components/forms/NuiDatepicker.svelte.d.ts +34 -0
- package/dist/components/forms/NuiFileInput.svelte +1047 -0
- package/dist/components/forms/NuiFileInput.svelte.d.ts +93 -0
- package/dist/components/forms/NuiFileInput.types.d.ts +28 -0
- package/dist/components/forms/NuiFileInput.types.js +1 -0
- package/dist/components/forms/NuiForm.svelte +391 -0
- package/dist/components/forms/NuiForm.svelte.d.ts +69 -0
- package/dist/components/forms/NuiInput.svelte +296 -0
- package/dist/components/forms/NuiInput.svelte.d.ts +62 -0
- package/dist/components/forms/NuiRadioGroup.svelte +238 -0
- package/dist/components/forms/NuiRadioGroup.svelte.d.ts +47 -0
- package/dist/components/forms/NuiSelect.svelte +819 -0
- package/dist/components/forms/NuiSelect.svelte.d.ts +49 -0
- package/dist/components/layout/NuiCard.svelte +314 -0
- package/dist/components/layout/NuiCard.svelte.d.ts +48 -0
- package/dist/components/layout/NuiCardGrid.svelte +98 -0
- package/dist/components/layout/NuiCardGrid.svelte.d.ts +22 -0
- package/dist/components/layout/NuiFlex.svelte +121 -0
- package/dist/components/layout/NuiFlex.svelte.d.ts +23 -0
- package/dist/components/layout/NuiGrid.svelte +66 -0
- package/dist/components/layout/NuiGrid.svelte.d.ts +14 -0
- package/dist/components/layout/NuiStack.svelte +138 -0
- package/dist/components/layout/NuiStack.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiAccordion.svelte +323 -0
- package/dist/components/navigation/NuiAccordion.svelte.d.ts +38 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte.d.ts +19 -0
- package/dist/components/navigation/NuiMenu.svelte +596 -0
- package/dist/components/navigation/NuiMenu.svelte.d.ts +28 -0
- package/dist/components/navigation/NuiPagination.svelte +306 -0
- package/dist/components/navigation/NuiPagination.svelte.d.ts +40 -0
- package/dist/components/navigation/NuiSidebar.svelte +186 -0
- package/dist/components/navigation/NuiSidebar.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiTabs.svelte +249 -0
- package/dist/components/navigation/NuiTabs.svelte.d.ts +31 -0
- package/dist/components/overlay/NuiDialog.svelte +118 -0
- package/dist/components/overlay/NuiDialog.svelte.d.ts +34 -0
- package/dist/components/overlay/NuiModal.svelte +992 -0
- package/dist/components/overlay/NuiModal.svelte.d.ts +72 -0
- package/dist/components/overlay/NuiTooltip.svelte +159 -0
- package/dist/components/overlay/NuiTooltip.svelte.d.ts +13 -0
- package/dist/composables/useClickOutside.d.ts +43 -0
- package/dist/composables/useClickOutside.js +73 -0
- package/dist/composables/useFormValidation.d.ts +23 -0
- package/dist/composables/useFormValidation.js +72 -0
- package/dist/composables/useIdle.svelte.d.ts +18 -0
- package/dist/composables/useIdle.svelte.js +144 -0
- package/dist/composables/useKeyboardShortcut.svelte.d.ts +114 -0
- package/dist/composables/useKeyboardShortcut.svelte.js +153 -0
- package/dist/composables/useLocalStorage.svelte.d.ts +22 -0
- package/dist/composables/useLocalStorage.svelte.js +105 -0
- package/dist/composables/useMediaQuery.svelte.d.ts +51 -0
- package/dist/composables/useMediaQuery.svelte.js +80 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +42 -0
- package/package.json +58 -3
- package/src/components/actions/NuiButton.svelte +292 -0
- package/src/components/data/NuiBadge.svelte +277 -0
- package/src/components/data/NuiCodeBlock.svelte +256 -0
- package/src/components/data/NuiList.svelte +341 -0
- package/src/components/data/NuiProgressBar.svelte +264 -0
- package/src/components/data/NuiRichText.svelte +260 -0
- package/src/components/data/NuiTable.svelte +470 -0
- package/src/components/data/chart/NuiChart.svelte +384 -0
- package/src/components/data/chart/color.ts +83 -0
- package/src/components/data/chart/index.ts +10 -0
- package/src/components/data/chart/registry.ts +71 -0
- package/src/components/data/chart/renderers/bar.ts +90 -0
- package/src/components/data/chart/renderers/base.ts +32 -0
- package/src/components/data/chart/renderers/line.ts +99 -0
- package/src/components/data/chart/renderers/pie.ts +66 -0
- package/src/components/data/chart/scale.ts +139 -0
- package/src/components/data/chart/types.ts +98 -0
- package/src/components/data/chart/validator.ts +98 -0
- package/src/components/forms/NuiCheckbox.svelte +218 -0
- package/src/components/forms/NuiDatepicker.svelte +755 -0
- package/src/components/forms/NuiFileInput.svelte +1047 -0
- package/src/components/forms/NuiFileInput.types.ts +29 -0
- package/src/components/forms/NuiForm.svelte +391 -0
- package/src/components/forms/NuiInput.svelte +296 -0
- package/src/components/forms/NuiRadioGroup.svelte +238 -0
- package/src/components/forms/NuiSelect.svelte +819 -0
- package/src/components/layout/NuiCard.svelte +314 -0
- package/src/components/layout/NuiCardGrid.svelte +98 -0
- package/src/components/layout/NuiFlex.svelte +121 -0
- package/src/components/layout/NuiGrid.svelte +66 -0
- package/src/components/layout/NuiStack.svelte +138 -0
- package/src/components/navigation/NuiAccordion.svelte +323 -0
- package/src/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/src/components/navigation/NuiMenu.svelte +596 -0
- package/src/components/navigation/NuiPagination.svelte +306 -0
- package/src/components/navigation/NuiSidebar.svelte +186 -0
- package/src/components/navigation/NuiTabs.svelte +249 -0
- package/src/components/overlay/NuiDialog.svelte +118 -0
- package/src/components/overlay/NuiModal.svelte +992 -0
- package/src/components/overlay/NuiTooltip.svelte +159 -0
- package/src/composables/useClickOutside.ts +132 -0
- package/src/composables/useFormValidation.ts +105 -0
- package/src/composables/useIdle.svelte.ts +191 -0
- package/src/composables/useKeyboardShortcut.svelte.ts +278 -0
- package/src/composables/useLocalStorage.svelte.ts +137 -0
- package/src/composables/useMediaQuery.svelte.ts +102 -0
- package/src/index.ts +66 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { drawMessage } from './base';
|
|
2
|
+
import { getColor } from '../color';
|
|
3
|
+
import { calculateScale, drawGridAndYLabels, DEFAULT_CHART_PADDING } from '../scale';
|
|
4
|
+
/**
|
|
5
|
+
* Renderer for bar and column charts.
|
|
6
|
+
*/
|
|
7
|
+
export class BarChartRenderer {
|
|
8
|
+
render(context, data, options) {
|
|
9
|
+
const { ctx, width, height } = context;
|
|
10
|
+
const padding = DEFAULT_CHART_PADDING;
|
|
11
|
+
const chartW = Math.max(10, width - padding.left - padding.right);
|
|
12
|
+
const chartH = Math.max(10, height - padding.top - padding.bottom);
|
|
13
|
+
const labels = data.labels || [];
|
|
14
|
+
const datasets = data.datasets.filter((d) => !d.hidden);
|
|
15
|
+
if (datasets.length === 0) {
|
|
16
|
+
drawMessage(ctx, width, height, 'All datasets are hidden');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const scale = calculateScale(datasets, options, chartH, padding.top);
|
|
20
|
+
drawGridAndYLabels(ctx, padding, chartW, chartH, scale, options);
|
|
21
|
+
const showLabels = options.showLabels !== false;
|
|
22
|
+
const groupCount = Math.max(labels.length, ...datasets.map((d) => d.data.length), 1);
|
|
23
|
+
const groupW = chartW / groupCount;
|
|
24
|
+
const totalBarWidth = (groupW * 0.7) / datasets.length;
|
|
25
|
+
const barGap = Math.min(4, totalBarWidth * 0.2);
|
|
26
|
+
for (let g = 0; g < groupCount; g++) {
|
|
27
|
+
const groupX = padding.left + g * groupW;
|
|
28
|
+
if (showLabels && labels[g]) {
|
|
29
|
+
ctx.fillStyle = '#a1a1aa';
|
|
30
|
+
ctx.textAlign = 'center';
|
|
31
|
+
ctx.textBaseline = 'top';
|
|
32
|
+
ctx.fillText(labels[g], groupX + groupW / 2, padding.top + chartH + 8);
|
|
33
|
+
}
|
|
34
|
+
datasets.forEach((ds, dsIndex) => {
|
|
35
|
+
const val = ds.data[g];
|
|
36
|
+
if (val === null || val === undefined || !Number.isFinite(val)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const clampedVal = Math.max(scale.minVal, Math.min(scale.maxVal, val));
|
|
40
|
+
const valY = padding.top + chartH - ((clampedVal - scale.minVal) / scale.range) * chartH;
|
|
41
|
+
const barX = groupX + (groupW - totalBarWidth * datasets.length) / 2 + dsIndex * totalBarWidth;
|
|
42
|
+
let barY;
|
|
43
|
+
let barH;
|
|
44
|
+
if (clampedVal >= 0) {
|
|
45
|
+
barY = valY;
|
|
46
|
+
barH = scale.baselineY - valY;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
barY = scale.baselineY;
|
|
50
|
+
barH = valY - scale.baselineY;
|
|
51
|
+
}
|
|
52
|
+
if (barH <= 0)
|
|
53
|
+
return;
|
|
54
|
+
const barWidth = Math.max(1, totalBarWidth - barGap);
|
|
55
|
+
ctx.fillStyle = getColor(ds.backgroundColor, dsIndex);
|
|
56
|
+
const r = Math.min(4, barH / 2, barWidth / 2);
|
|
57
|
+
ctx.beginPath();
|
|
58
|
+
if (clampedVal >= 0) {
|
|
59
|
+
ctx.moveTo(barX, scale.baselineY);
|
|
60
|
+
ctx.lineTo(barX, barY + r);
|
|
61
|
+
ctx.quadraticCurveTo(barX, barY, barX + r, barY);
|
|
62
|
+
ctx.lineTo(barX + barWidth - r, barY);
|
|
63
|
+
ctx.quadraticCurveTo(barX + barWidth, barY, barX + barWidth, barY + r);
|
|
64
|
+
ctx.lineTo(barX + barWidth, scale.baselineY);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
ctx.moveTo(barX, scale.baselineY);
|
|
68
|
+
ctx.lineTo(barX, barY + barH - r);
|
|
69
|
+
ctx.quadraticCurveTo(barX, barY + barH, barX + r, barY + barH);
|
|
70
|
+
ctx.lineTo(barX + barWidth - r, barY + barH);
|
|
71
|
+
ctx.quadraticCurveTo(barX + barWidth, barY + barH, barX + barWidth, barY + barH - r);
|
|
72
|
+
ctx.lineTo(barX + barWidth, scale.baselineY);
|
|
73
|
+
}
|
|
74
|
+
ctx.closePath();
|
|
75
|
+
ctx.fill();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChartData, ChartOptions } from '../types';
|
|
2
|
+
export interface RenderContext {
|
|
3
|
+
canvas: HTMLCanvasElement;
|
|
4
|
+
ctx: CanvasRenderingContext2D;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
dpr: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Canvas chart renderer interface.
|
|
11
|
+
*/
|
|
12
|
+
export interface ChartRenderer {
|
|
13
|
+
render(context: RenderContext, data: ChartData, options: ChartOptions): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Draws centered text message on the canvas (eg. empty state).
|
|
17
|
+
*/
|
|
18
|
+
export declare function drawMessage(ctx: CanvasRenderingContext2D, width: number, height: number, text: string): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draws centered text message on the canvas (eg. empty state).
|
|
3
|
+
*/
|
|
4
|
+
export function drawMessage(ctx, width, height, text) {
|
|
5
|
+
ctx.fillStyle = '#71717a';
|
|
6
|
+
ctx.font = '13px sans-serif';
|
|
7
|
+
ctx.textAlign = 'center';
|
|
8
|
+
ctx.textBaseline = 'middle';
|
|
9
|
+
ctx.fillText(text, width / 2, height / 2);
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChartData, ChartOptions } from '../types';
|
|
2
|
+
import type { ChartRenderer, RenderContext } from './base';
|
|
3
|
+
/**
|
|
4
|
+
* Renderer for line charts.
|
|
5
|
+
*/
|
|
6
|
+
export declare class LineChartRenderer implements ChartRenderer {
|
|
7
|
+
render(context: RenderContext, data: ChartData, options: ChartOptions): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { drawMessage } from './base';
|
|
2
|
+
import { getColor, colorToRgba } from '../color';
|
|
3
|
+
import { calculateScale, drawGridAndYLabels, DEFAULT_CHART_PADDING } from '../scale';
|
|
4
|
+
/**
|
|
5
|
+
* Renderer for line charts.
|
|
6
|
+
*/
|
|
7
|
+
export class LineChartRenderer {
|
|
8
|
+
render(context, data, options) {
|
|
9
|
+
const { ctx, width, height } = context;
|
|
10
|
+
const padding = DEFAULT_CHART_PADDING;
|
|
11
|
+
const chartW = Math.max(10, width - padding.left - padding.right);
|
|
12
|
+
const chartH = Math.max(10, height - padding.top - padding.bottom);
|
|
13
|
+
const labels = data.labels || [];
|
|
14
|
+
const datasets = data.datasets.filter((d) => !d.hidden);
|
|
15
|
+
if (datasets.length === 0) {
|
|
16
|
+
drawMessage(ctx, width, height, 'All datasets are hidden');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const scale = calculateScale(datasets, options, chartH, padding.top);
|
|
20
|
+
drawGridAndYLabels(ctx, padding, chartW, chartH, scale, options);
|
|
21
|
+
const showLabels = options.showLabels !== false;
|
|
22
|
+
const pointCount = Math.max(labels.length, ...datasets.map((d) => d.data.length), 1);
|
|
23
|
+
const stepX = pointCount > 1 ? chartW / (pointCount - 1) : chartW / 2;
|
|
24
|
+
if (showLabels) {
|
|
25
|
+
for (let i = 0; i < pointCount; i++) {
|
|
26
|
+
if (labels[i]) {
|
|
27
|
+
const x = padding.left + i * stepX;
|
|
28
|
+
ctx.fillStyle = '#a1a1aa';
|
|
29
|
+
ctx.textAlign = 'center';
|
|
30
|
+
ctx.textBaseline = 'top';
|
|
31
|
+
ctx.fillText(labels[i], x, padding.top + chartH + 8);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
datasets.forEach((ds, dsIndex) => {
|
|
36
|
+
const color = getColor(ds.borderColor || ds.backgroundColor, dsIndex);
|
|
37
|
+
ctx.strokeStyle = color;
|
|
38
|
+
ctx.lineWidth = Math.max(0, ds.borderWidth ?? 2);
|
|
39
|
+
const segments = [];
|
|
40
|
+
let currentSegment = [];
|
|
41
|
+
for (let i = 0; i < pointCount; i++) {
|
|
42
|
+
const val = ds.data[i];
|
|
43
|
+
if (val !== null && val !== undefined && Number.isFinite(val)) {
|
|
44
|
+
const safeVal = Math.max(scale.minVal, Math.min(scale.maxVal, val));
|
|
45
|
+
const x = padding.left + i * stepX;
|
|
46
|
+
const y = padding.top + chartH - ((safeVal - scale.minVal) / scale.range) * chartH;
|
|
47
|
+
currentSegment.push({ x, y });
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
if (currentSegment.length > 0) {
|
|
51
|
+
segments.push(currentSegment);
|
|
52
|
+
currentSegment = [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (currentSegment.length > 0) {
|
|
57
|
+
segments.push(currentSegment);
|
|
58
|
+
}
|
|
59
|
+
segments.forEach((segment) => {
|
|
60
|
+
if (segment.length === 0)
|
|
61
|
+
return;
|
|
62
|
+
ctx.beginPath();
|
|
63
|
+
segment.forEach((p, idx) => {
|
|
64
|
+
if (idx === 0)
|
|
65
|
+
ctx.moveTo(p.x, p.y);
|
|
66
|
+
else
|
|
67
|
+
ctx.lineTo(p.x, p.y);
|
|
68
|
+
});
|
|
69
|
+
ctx.stroke();
|
|
70
|
+
// Area fill clipped to baselineY
|
|
71
|
+
ctx.lineTo(segment[segment.length - 1].x, scale.baselineY);
|
|
72
|
+
ctx.lineTo(segment[0].x, scale.baselineY);
|
|
73
|
+
ctx.closePath();
|
|
74
|
+
ctx.fillStyle = colorToRgba(color, 0.1);
|
|
75
|
+
ctx.fill();
|
|
76
|
+
// Render point markers
|
|
77
|
+
ctx.fillStyle = '#ffffff';
|
|
78
|
+
ctx.strokeStyle = color;
|
|
79
|
+
ctx.lineWidth = 2;
|
|
80
|
+
segment.forEach((p) => {
|
|
81
|
+
ctx.beginPath();
|
|
82
|
+
ctx.arc(p.x, p.y, 3.5, 0, Math.PI * 2);
|
|
83
|
+
ctx.fill();
|
|
84
|
+
ctx.stroke();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChartData, ChartOptions } from '../types';
|
|
2
|
+
import type { ChartRenderer, RenderContext } from './base';
|
|
3
|
+
/**
|
|
4
|
+
* Renderer for pie and doughnut charts.
|
|
5
|
+
*/
|
|
6
|
+
export declare class PieChartRenderer implements ChartRenderer {
|
|
7
|
+
private readonly isDoughnut;
|
|
8
|
+
constructor(isDoughnut?: boolean);
|
|
9
|
+
render(context: RenderContext, data: ChartData, _options: ChartOptions): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { drawMessage } from './base';
|
|
2
|
+
import { getColor } from '../color';
|
|
3
|
+
/**
|
|
4
|
+
* Renderer for pie and doughnut charts.
|
|
5
|
+
*/
|
|
6
|
+
export class PieChartRenderer {
|
|
7
|
+
isDoughnut;
|
|
8
|
+
constructor(isDoughnut = false) {
|
|
9
|
+
this.isDoughnut = isDoughnut;
|
|
10
|
+
}
|
|
11
|
+
render(context, data, _options) {
|
|
12
|
+
const { ctx, width, height } = context;
|
|
13
|
+
const cx = width / 2;
|
|
14
|
+
const cy = height / 2;
|
|
15
|
+
const radius = Math.max(10, Math.min(width, height) / 2 - 20);
|
|
16
|
+
// Pie chart uses the first visible dataset
|
|
17
|
+
const dataset = data.datasets.find((d) => !d.hidden) ?? data.datasets[0];
|
|
18
|
+
if (!dataset || dataset.hidden || !dataset.data || dataset.data.length === 0) {
|
|
19
|
+
drawMessage(ctx, width, height, 'Dataset is hidden or empty');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const positiveValues = dataset.data.map((v) => typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : 0);
|
|
23
|
+
const total = positiveValues.reduce((acc, v) => acc + v, 0);
|
|
24
|
+
if (total <= 0) {
|
|
25
|
+
drawMessage(ctx, width, height, 'No positive values for pie chart');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let startAngle = -Math.PI / 2;
|
|
29
|
+
positiveValues.forEach((val, idx) => {
|
|
30
|
+
if (val <= 0)
|
|
31
|
+
return;
|
|
32
|
+
const sliceAngle = (val / total) * (Math.PI * 2);
|
|
33
|
+
const endAngle = startAngle + sliceAngle;
|
|
34
|
+
const color = getColor(dataset.backgroundColor, idx);
|
|
35
|
+
ctx.fillStyle = color;
|
|
36
|
+
ctx.beginPath();
|
|
37
|
+
ctx.moveTo(cx, cy);
|
|
38
|
+
ctx.arc(cx, cy, radius, startAngle, endAngle);
|
|
39
|
+
ctx.closePath();
|
|
40
|
+
ctx.fill();
|
|
41
|
+
ctx.strokeStyle = 'rgba(24, 24, 27, 0.8)';
|
|
42
|
+
ctx.lineWidth = 2;
|
|
43
|
+
ctx.stroke();
|
|
44
|
+
startAngle = endAngle;
|
|
45
|
+
});
|
|
46
|
+
if (this.isDoughnut) {
|
|
47
|
+
ctx.save();
|
|
48
|
+
ctx.globalCompositeOperation = 'destination-out';
|
|
49
|
+
ctx.beginPath();
|
|
50
|
+
ctx.arc(cx, cy, radius * 0.58, 0, Math.PI * 2);
|
|
51
|
+
ctx.fill();
|
|
52
|
+
ctx.restore();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ChartData, ChartOptions } from './types';
|
|
2
|
+
export interface ScaleInfo {
|
|
3
|
+
minVal: number;
|
|
4
|
+
maxVal: number;
|
|
5
|
+
range: number;
|
|
6
|
+
baselineY: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ChartPadding {
|
|
9
|
+
top: number;
|
|
10
|
+
right: number;
|
|
11
|
+
bottom: number;
|
|
12
|
+
left: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const DEFAULT_CHART_PADDING: ChartPadding;
|
|
15
|
+
/**
|
|
16
|
+
* Calculates Y axis scale supporting both positive and negative values.
|
|
17
|
+
*/
|
|
18
|
+
export declare function calculateScale(datasets: ChartData['datasets'], options: ChartOptions, chartH: number, paddingTop?: number): ScaleInfo;
|
|
19
|
+
/**
|
|
20
|
+
* Draws horizontal grid lines, Y axis labels, and highlighted zero baseline.
|
|
21
|
+
*/
|
|
22
|
+
export declare function drawGridAndYLabels(ctx: CanvasRenderingContext2D, padding: ChartPadding, chartW: number, chartH: number, scale: ScaleInfo, options: ChartOptions): void;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export const DEFAULT_CHART_PADDING = {
|
|
2
|
+
top: 20,
|
|
3
|
+
right: 20,
|
|
4
|
+
bottom: 35,
|
|
5
|
+
left: 45,
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Calculates Y axis scale supporting both positive and negative values.
|
|
9
|
+
*/
|
|
10
|
+
export function calculateScale(datasets, options, chartH, paddingTop = DEFAULT_CHART_PADDING.top) {
|
|
11
|
+
let dataMin = Infinity;
|
|
12
|
+
let dataMax = -Infinity;
|
|
13
|
+
let hasValidData = false;
|
|
14
|
+
for (const ds of datasets) {
|
|
15
|
+
if (ds.hidden)
|
|
16
|
+
continue;
|
|
17
|
+
for (const val of ds.data) {
|
|
18
|
+
if (typeof val === 'number' && Number.isFinite(val)) {
|
|
19
|
+
hasValidData = true;
|
|
20
|
+
if (val < dataMin)
|
|
21
|
+
dataMin = val;
|
|
22
|
+
if (val > dataMax)
|
|
23
|
+
dataMax = val;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!hasValidData) {
|
|
28
|
+
dataMin = 0;
|
|
29
|
+
dataMax = 10;
|
|
30
|
+
}
|
|
31
|
+
const userMinValid = typeof options.min === 'number' && Number.isFinite(options.min);
|
|
32
|
+
const userMaxValid = typeof options.max === 'number' && Number.isFinite(options.max);
|
|
33
|
+
let minVal;
|
|
34
|
+
let maxVal;
|
|
35
|
+
if (userMinValid && userMaxValid) {
|
|
36
|
+
if (options.min < options.max) {
|
|
37
|
+
minVal = options.min;
|
|
38
|
+
maxVal = options.max;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.warn(`[NuiChart] Invalid scale configuration: min (${options.min}) >= max (${options.max}). Falling back to auto range.`);
|
|
42
|
+
minVal = dataMin < 0 ? Math.floor(dataMin * 1.15) : 0;
|
|
43
|
+
maxVal = dataMax > 0 ? Math.ceil(dataMax * 1.15) : 10;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (userMinValid) {
|
|
47
|
+
minVal = options.min;
|
|
48
|
+
maxVal = Math.max(minVal + 1, dataMax > 0 ? Math.ceil(dataMax * 1.15) : 10);
|
|
49
|
+
}
|
|
50
|
+
else if (userMaxValid) {
|
|
51
|
+
maxVal = options.max;
|
|
52
|
+
minVal = Math.min(maxVal - 1, dataMin < 0 ? Math.floor(dataMin * 1.15) : 0);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Auto scale covering zero
|
|
56
|
+
minVal = dataMin < 0 ? Math.floor(dataMin * 1.15) : 0;
|
|
57
|
+
maxVal = dataMax > 0 ? Math.ceil(dataMax * 1.15) : 10;
|
|
58
|
+
}
|
|
59
|
+
if (minVal >= maxVal) {
|
|
60
|
+
maxVal = minVal + 10;
|
|
61
|
+
}
|
|
62
|
+
const range = maxVal - minVal;
|
|
63
|
+
const rawZeroY = paddingTop + chartH - ((0 - minVal) / range) * chartH;
|
|
64
|
+
const baselineY = Math.max(paddingTop, Math.min(paddingTop + chartH, rawZeroY));
|
|
65
|
+
return { minVal, maxVal, range, baselineY };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Draws horizontal grid lines, Y axis labels, and highlighted zero baseline.
|
|
69
|
+
*/
|
|
70
|
+
export function drawGridAndYLabels(ctx, padding, chartW, chartH, scale, options) {
|
|
71
|
+
const showGrid = options.showGrid !== false;
|
|
72
|
+
const showLabels = options.showLabels !== false;
|
|
73
|
+
const gridSteps = 4;
|
|
74
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
|
|
75
|
+
ctx.fillStyle = '#a1a1aa';
|
|
76
|
+
ctx.font = '11px sans-serif';
|
|
77
|
+
ctx.textAlign = 'right';
|
|
78
|
+
ctx.textBaseline = 'middle';
|
|
79
|
+
ctx.lineWidth = 1;
|
|
80
|
+
for (let i = 0; i <= gridSteps; i++) {
|
|
81
|
+
const yVal = scale.minVal + (scale.range / gridSteps) * i;
|
|
82
|
+
const y = padding.top + chartH - ((yVal - scale.minVal) / scale.range) * chartH;
|
|
83
|
+
if (showGrid) {
|
|
84
|
+
ctx.beginPath();
|
|
85
|
+
ctx.moveTo(padding.left, y);
|
|
86
|
+
ctx.lineTo(padding.left + chartW, y);
|
|
87
|
+
ctx.stroke();
|
|
88
|
+
}
|
|
89
|
+
if (showLabels) {
|
|
90
|
+
ctx.fillText(Math.round(yVal).toString(), padding.left - 8, y);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Highlight zero baseline if range covers both negative and positive values
|
|
94
|
+
if (scale.minVal < 0 && scale.maxVal > 0) {
|
|
95
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
|
|
96
|
+
ctx.beginPath();
|
|
97
|
+
ctx.moveTo(padding.left, scale.baselineY);
|
|
98
|
+
ctx.lineTo(padding.left + chartW, scale.baselineY);
|
|
99
|
+
ctx.stroke();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported Canvas chart types.
|
|
3
|
+
*/
|
|
4
|
+
export type ChartType = 'bar' | 'line' | 'pie' | 'doughnut';
|
|
5
|
+
/**
|
|
6
|
+
* Single dataset to render on a chart.
|
|
7
|
+
*/
|
|
8
|
+
export interface ChartDataset {
|
|
9
|
+
/** Dataset label displayed in the legend and accessibility attributes. */
|
|
10
|
+
label: string;
|
|
11
|
+
/**
|
|
12
|
+
* Numerical values for the dataset.
|
|
13
|
+
* `null` or `undefined` indicates a missing data point.
|
|
14
|
+
*/
|
|
15
|
+
data: (number | null | undefined)[];
|
|
16
|
+
/**
|
|
17
|
+
* Background color for bars, slices, or area fill.
|
|
18
|
+
* Accepts any valid CSS color string or an array of colors.
|
|
19
|
+
*/
|
|
20
|
+
backgroundColor?: string | string[];
|
|
21
|
+
/**
|
|
22
|
+
* Border color for lines or bars.
|
|
23
|
+
* Accepts any valid CSS color string or an array of colors.
|
|
24
|
+
*/
|
|
25
|
+
borderColor?: string | string[];
|
|
26
|
+
/**
|
|
27
|
+
* Border / line width in pixels.
|
|
28
|
+
* @defaultValue 2
|
|
29
|
+
*/
|
|
30
|
+
borderWidth?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the dataset is initially hidden.
|
|
33
|
+
* @defaultValue false
|
|
34
|
+
*/
|
|
35
|
+
hidden?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Chart input data structure.
|
|
39
|
+
*/
|
|
40
|
+
export interface ChartData {
|
|
41
|
+
/** Category labels for the X axis or slice labels for pie/doughnut. */
|
|
42
|
+
labels: string[];
|
|
43
|
+
/** Array of datasets to render. */
|
|
44
|
+
datasets: ChartDataset[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Chart rendering, grid, scale, and accessibility options.
|
|
48
|
+
*/
|
|
49
|
+
export interface ChartOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Legend position configuration.
|
|
52
|
+
* @defaultValue { position: 'bottom' }
|
|
53
|
+
*/
|
|
54
|
+
legend?: {
|
|
55
|
+
position?: 'top' | 'bottom' | 'none';
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Backward compatible plugin configuration.
|
|
59
|
+
*/
|
|
60
|
+
plugins?: {
|
|
61
|
+
legend?: {
|
|
62
|
+
position?: 'top' | 'bottom' | 'none';
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Whether to render horizontal grid lines.
|
|
67
|
+
* @defaultValue true
|
|
68
|
+
*/
|
|
69
|
+
showGrid?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Whether to show axis labels.
|
|
72
|
+
* @defaultValue true
|
|
73
|
+
*/
|
|
74
|
+
showLabels?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Forced value for the Y axis scale.
|
|
77
|
+
*/
|
|
78
|
+
min?: number;
|
|
79
|
+
max?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Accessible description for screen readers.
|
|
82
|
+
*/
|
|
83
|
+
ariaLabel?: string;
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChartData } from './types';
|
|
2
|
+
export interface ChartValidationResult {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
data?: ChartData;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Validates chart data structure (especially useful for NUI game events).
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateChartData(raw: unknown): ChartValidationResult;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates chart data structure (especially useful for NUI game events).
|
|
3
|
+
*/
|
|
4
|
+
export function validateChartData(raw) {
|
|
5
|
+
if (!raw || typeof raw !== 'object') {
|
|
6
|
+
return { valid: false, error: 'Chart data must be an object' };
|
|
7
|
+
}
|
|
8
|
+
const obj = raw;
|
|
9
|
+
if (!Array.isArray(obj.datasets)) {
|
|
10
|
+
return { valid: false, error: 'Missing datasets array' };
|
|
11
|
+
}
|
|
12
|
+
const labels = Array.isArray(obj.labels)
|
|
13
|
+
? obj.labels.map((l) => (l !== null && l !== undefined ? String(l) : ''))
|
|
14
|
+
: [];
|
|
15
|
+
const datasets = [];
|
|
16
|
+
for (let i = 0; i < obj.datasets.length; i++) {
|
|
17
|
+
const ds = obj.datasets[i];
|
|
18
|
+
if (!ds || typeof ds !== 'object') {
|
|
19
|
+
return { valid: false, error: `Dataset at index ${i} is not a valid object` };
|
|
20
|
+
}
|
|
21
|
+
const dsObj = ds;
|
|
22
|
+
const label = typeof dsObj.label === 'string' ? dsObj.label : `Dataset ${i + 1}`;
|
|
23
|
+
if (!Array.isArray(dsObj.data)) {
|
|
24
|
+
return { valid: false, error: `Dataset '${label}' is missing data array` };
|
|
25
|
+
}
|
|
26
|
+
const data = [];
|
|
27
|
+
for (let j = 0; j < dsObj.data.length; j++) {
|
|
28
|
+
const v = dsObj.data[j];
|
|
29
|
+
if (v === null || v === undefined) {
|
|
30
|
+
data.push(null);
|
|
31
|
+
}
|
|
32
|
+
else if (typeof v === 'number' && Number.isFinite(v)) {
|
|
33
|
+
data.push(v);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return {
|
|
37
|
+
valid: false,
|
|
38
|
+
error: `Dataset '${label}' contains invalid value at index ${j}: ${String(v)} (expected finite number or null)`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let borderWidth = undefined;
|
|
43
|
+
if (dsObj.borderWidth !== undefined) {
|
|
44
|
+
if (typeof dsObj.borderWidth !== 'number' || !Number.isFinite(dsObj.borderWidth) || dsObj.borderWidth < 0) {
|
|
45
|
+
return { valid: false, error: `Dataset '${label}' has invalid borderWidth (must be number >= 0)` };
|
|
46
|
+
}
|
|
47
|
+
borderWidth = dsObj.borderWidth;
|
|
48
|
+
}
|
|
49
|
+
let backgroundColor = undefined;
|
|
50
|
+
if (typeof dsObj.backgroundColor === 'string') {
|
|
51
|
+
backgroundColor = dsObj.backgroundColor;
|
|
52
|
+
}
|
|
53
|
+
else if (Array.isArray(dsObj.backgroundColor)) {
|
|
54
|
+
if (dsObj.backgroundColor.some((c) => typeof c !== 'string' || !c.trim())) {
|
|
55
|
+
return { valid: false, error: `Dataset '${label}' has invalid colors in backgroundColor` };
|
|
56
|
+
}
|
|
57
|
+
backgroundColor = dsObj.backgroundColor;
|
|
58
|
+
}
|
|
59
|
+
let borderColor = undefined;
|
|
60
|
+
if (typeof dsObj.borderColor === 'string') {
|
|
61
|
+
borderColor = dsObj.borderColor;
|
|
62
|
+
}
|
|
63
|
+
else if (Array.isArray(dsObj.borderColor)) {
|
|
64
|
+
if (dsObj.borderColor.some((c) => typeof c !== 'string' || !c.trim())) {
|
|
65
|
+
return { valid: false, error: `Dataset '${label}' has invalid colors in borderColor` };
|
|
66
|
+
}
|
|
67
|
+
borderColor = dsObj.borderColor;
|
|
68
|
+
}
|
|
69
|
+
datasets.push({
|
|
70
|
+
label,
|
|
71
|
+
data,
|
|
72
|
+
backgroundColor,
|
|
73
|
+
borderColor,
|
|
74
|
+
borderWidth,
|
|
75
|
+
hidden: typeof dsObj.hidden === 'boolean' ? dsObj.hidden : false,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (datasets.length === 0) {
|
|
79
|
+
return { valid: false, error: 'datasets array cannot be empty' };
|
|
80
|
+
}
|
|
81
|
+
return { valid: true, data: { labels, datasets } };
|
|
82
|
+
}
|