juxscript 1.0.20 → 1.0.21
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/bin/cli.js +121 -72
- package/lib/components/alert.ts +143 -92
- package/lib/components/badge.ts +93 -94
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +40 -131
- package/lib/components/card.ts +57 -79
- package/lib/components/charts/areachart.ts +315 -0
- package/lib/components/charts/barchart.ts +421 -0
- package/lib/components/charts/doughnutchart.ts +263 -0
- package/lib/components/charts/lib/BaseChart.ts +402 -0
- package/lib/components/{chart-types.ts → charts/lib/chart-types.ts} +1 -1
- package/lib/components/{chart-utils.ts → charts/lib/chart-utils.ts} +1 -1
- package/lib/components/{chart.ts → charts/lib/chart.ts} +3 -3
- package/lib/components/checkbox.ts +255 -204
- package/lib/components/code.ts +31 -78
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +180 -147
- package/lib/components/dialog.ts +218 -221
- package/lib/components/divider.ts +63 -87
- package/lib/components/docs-data.json +498 -2404
- package/lib/components/dropdown.ts +191 -236
- package/lib/components/element.ts +196 -145
- package/lib/components/fileupload.ts +253 -167
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +31 -97
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +51 -114
- package/lib/components/icon.ts +33 -120
- package/lib/components/icons.ts +2 -1
- package/lib/components/include.ts +76 -3
- package/lib/components/input.ts +155 -407
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +358 -261
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +63 -152
- package/lib/components/modal.ts +42 -129
- package/lib/components/nav.ts +79 -101
- package/lib/components/paragraph.ts +38 -102
- package/lib/components/progress.ts +108 -166
- package/lib/components/radio.ts +283 -234
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +189 -199
- package/lib/components/sidebar.ts +110 -141
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +254 -183
- package/lib/components/table.ts +1078 -208
- package/lib/components/tabs.ts +42 -106
- package/lib/components/theme-toggle.ts +73 -165
- package/lib/components/tooltip.ts +85 -316
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +67 -41
- package/machinery/build.js +466 -0
- package/machinery/compiler.js +354 -105
- package/machinery/server.js +23 -100
- package/machinery/watcher.js +153 -130
- package/package.json +1 -1
- package/presets/base.css +1166 -0
- package/presets/notion.css +2 -1975
- package/lib/adapters/base-adapter.js +0 -35
- package/lib/adapters/index.js +0 -33
- package/lib/adapters/mysql-adapter.js +0 -65
- package/lib/adapters/postgres-adapter.js +0 -70
- package/lib/adapters/sqlite-adapter.js +0 -56
- package/lib/components/areachart.ts +0 -1128
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1322
- package/lib/components/doughnutchart.ts +0 -1259
- package/lib/components/footer.ts +0 -165
- package/lib/components/header.ts +0 -187
- package/lib/components/layout.ts +0 -239
- package/lib/components/main.ts +0 -137
- package/lib/layouts/default.jux +0 -8
- package/lib/layouts/figma.jux +0 -0
- /package/lib/{themes → components/charts/lib}/charts.js +0 -0
package/lib/components/card.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
import { renderIcon } from './icons.js';
|
|
3
|
+
|
|
4
|
+
// Event definitions - Card is display-only, no interactive events
|
|
5
|
+
const TRIGGER_EVENTS = [] as const;
|
|
6
|
+
const CALLBACK_EVENTS = ['click'] as const; // ✅ Can be clicked for navigation/selection
|
|
3
7
|
|
|
4
8
|
export interface CardOptions {
|
|
5
9
|
title?: string;
|
|
@@ -7,6 +11,7 @@ export interface CardOptions {
|
|
|
7
11
|
footer?: string;
|
|
8
12
|
variant?: string;
|
|
9
13
|
hoverable?: boolean;
|
|
14
|
+
icon?: string;
|
|
10
15
|
style?: string;
|
|
11
16
|
class?: string;
|
|
12
17
|
}
|
|
@@ -17,40 +22,39 @@ type CardState = {
|
|
|
17
22
|
footer: string;
|
|
18
23
|
variant: string;
|
|
19
24
|
hoverable: boolean;
|
|
25
|
+
icon: string;
|
|
20
26
|
style: string;
|
|
21
27
|
class: string;
|
|
22
28
|
};
|
|
23
29
|
|
|
24
|
-
export class Card {
|
|
25
|
-
state: CardState;
|
|
26
|
-
container: HTMLElement | null = null;
|
|
27
|
-
_id: string;
|
|
28
|
-
id: string;
|
|
29
|
-
|
|
30
|
-
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
31
|
-
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
32
|
-
private _syncBindings: Array<{
|
|
33
|
-
property: string,
|
|
34
|
-
stateObj: State<any>,
|
|
35
|
-
toState?: Function,
|
|
36
|
-
toComponent?: Function
|
|
37
|
-
}> = [];
|
|
38
|
-
|
|
30
|
+
export class Card extends BaseComponent<CardState> {
|
|
39
31
|
constructor(id: string, options: CardOptions = {}) {
|
|
40
|
-
|
|
41
|
-
this.id = id;
|
|
42
|
-
|
|
43
|
-
this.state = {
|
|
32
|
+
super(id, {
|
|
44
33
|
title: options.title ?? '',
|
|
45
34
|
content: options.content ?? '',
|
|
46
35
|
footer: options.footer ?? '',
|
|
47
36
|
variant: options.variant ?? 'default',
|
|
48
37
|
hoverable: options.hoverable ?? false,
|
|
38
|
+
icon: options.icon ?? '',
|
|
49
39
|
style: options.style ?? '',
|
|
50
40
|
class: options.class ?? ''
|
|
51
|
-
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected getTriggerEvents(): readonly string[] {
|
|
45
|
+
return TRIGGER_EVENTS;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected getCallbackEvents(): readonly string[] {
|
|
49
|
+
return CALLBACK_EVENTS;
|
|
52
50
|
}
|
|
53
51
|
|
|
52
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
53
|
+
* FLUENT API
|
|
54
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
55
|
+
|
|
56
|
+
// ✅ Inherited from BaseComponent
|
|
57
|
+
|
|
54
58
|
title(value: string): this {
|
|
55
59
|
this.state.title = value;
|
|
56
60
|
return this;
|
|
@@ -66,47 +70,20 @@ export class Card {
|
|
|
66
70
|
return this;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
this.state.
|
|
71
|
-
return this;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
class(value: string): this {
|
|
75
|
-
this.state.class = value;
|
|
73
|
+
icon(value: string): this {
|
|
74
|
+
this.state.icon = value;
|
|
76
75
|
return this;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
85
|
-
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
86
|
-
throw new Error(`Card.sync: Expected a State object for property "${property}"`);
|
|
87
|
-
}
|
|
88
|
-
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
89
|
-
return this;
|
|
90
|
-
}
|
|
78
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
79
|
+
* RENDER
|
|
80
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
91
81
|
|
|
92
82
|
render(targetId?: string): this {
|
|
93
|
-
|
|
94
|
-
let container: HTMLElement;
|
|
95
|
-
if (targetId) {
|
|
96
|
-
const target = document.querySelector(targetId);
|
|
97
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
98
|
-
throw new Error(`Card: Target "${targetId}" not found`);
|
|
99
|
-
}
|
|
100
|
-
container = target;
|
|
101
|
-
} else {
|
|
102
|
-
container = getOrCreateContainer(this._id);
|
|
103
|
-
}
|
|
104
|
-
this.container = container;
|
|
83
|
+
const container = this._setupContainer(targetId);
|
|
105
84
|
|
|
106
|
-
|
|
107
|
-
const { title, content, footer, variant, hoverable, style, class: className } = this.state;
|
|
85
|
+
const { title, content, footer, variant, hoverable, icon, style, class: className } = this.state;
|
|
108
86
|
|
|
109
|
-
// === 3. BUILD: Create DOM elements ===
|
|
110
87
|
const card = document.createElement('div');
|
|
111
88
|
card.className = `jux-card jux-card-${variant}`;
|
|
112
89
|
card.id = this._id;
|
|
@@ -114,10 +91,24 @@ export class Card {
|
|
|
114
91
|
if (className) card.className += ` ${className}`;
|
|
115
92
|
if (style) card.setAttribute('style', style);
|
|
116
93
|
|
|
117
|
-
if (title) {
|
|
94
|
+
if (title || icon) {
|
|
118
95
|
const header = document.createElement('div');
|
|
119
96
|
header.className = 'jux-card-header';
|
|
120
|
-
|
|
97
|
+
|
|
98
|
+
if (icon) {
|
|
99
|
+
const iconEl = document.createElement('span');
|
|
100
|
+
iconEl.className = 'jux-card-icon';
|
|
101
|
+
iconEl.appendChild(renderIcon(icon));
|
|
102
|
+
header.appendChild(iconEl);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (title) {
|
|
106
|
+
const titleEl = document.createElement('span');
|
|
107
|
+
titleEl.className = 'jux-card-title';
|
|
108
|
+
titleEl.textContent = title;
|
|
109
|
+
header.appendChild(titleEl);
|
|
110
|
+
}
|
|
111
|
+
|
|
121
112
|
card.appendChild(header);
|
|
122
113
|
}
|
|
123
114
|
|
|
@@ -133,20 +124,15 @@ export class Card {
|
|
|
133
124
|
card.appendChild(footerEl);
|
|
134
125
|
}
|
|
135
126
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// Wire custom bindings from .bind() calls
|
|
139
|
-
this._bindings.forEach(({ event, handler }) => {
|
|
140
|
-
card.addEventListener(event, handler as EventListener);
|
|
141
|
-
});
|
|
127
|
+
this._wireStandardEvents(card);
|
|
142
128
|
|
|
143
|
-
// Wire sync bindings
|
|
129
|
+
// Wire sync bindings
|
|
144
130
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
145
131
|
if (property === 'title') {
|
|
146
|
-
const
|
|
132
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
147
133
|
|
|
148
134
|
stateObj.subscribe((val: any) => {
|
|
149
|
-
const transformed =
|
|
135
|
+
const transformed = transform(val);
|
|
150
136
|
const header = card.querySelector('.jux-card-header');
|
|
151
137
|
if (header) {
|
|
152
138
|
header.textContent = transformed;
|
|
@@ -155,19 +141,19 @@ export class Card {
|
|
|
155
141
|
});
|
|
156
142
|
}
|
|
157
143
|
else if (property === 'content') {
|
|
158
|
-
const
|
|
144
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
159
145
|
|
|
160
146
|
stateObj.subscribe((val: any) => {
|
|
161
|
-
const transformed =
|
|
147
|
+
const transformed = transform(val);
|
|
162
148
|
body.innerHTML = transformed;
|
|
163
149
|
this.state.content = transformed;
|
|
164
150
|
});
|
|
165
151
|
}
|
|
166
152
|
else if (property === 'footer') {
|
|
167
|
-
const
|
|
153
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
168
154
|
|
|
169
155
|
stateObj.subscribe((val: any) => {
|
|
170
|
-
const transformed =
|
|
156
|
+
const transformed = transform(val);
|
|
171
157
|
const footerEl = card.querySelector('.jux-card-footer');
|
|
172
158
|
if (footerEl) {
|
|
173
159
|
footerEl.innerHTML = transformed;
|
|
@@ -177,17 +163,9 @@ export class Card {
|
|
|
177
163
|
}
|
|
178
164
|
});
|
|
179
165
|
|
|
180
|
-
// === 5. RENDER: Append to DOM and finalize ===
|
|
181
166
|
container.appendChild(card);
|
|
182
167
|
return this;
|
|
183
168
|
}
|
|
184
|
-
|
|
185
|
-
renderTo(juxComponent: any): this {
|
|
186
|
-
if (!juxComponent?._id) {
|
|
187
|
-
throw new Error('Card.renderTo: Invalid component');
|
|
188
|
-
}
|
|
189
|
-
return this.render(`#${juxComponent._id}`);
|
|
190
|
-
}
|
|
191
169
|
}
|
|
192
170
|
|
|
193
171
|
export function card(id: string, options: CardOptions = {}): Card {
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { BaseChart, BaseChartState, ChartDataPoint } from './lib/BaseChart.js';
|
|
2
|
+
|
|
3
|
+
export interface AreaChartOptions {
|
|
4
|
+
data?: ChartDataPoint[];
|
|
5
|
+
title?: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
xAxisLabel?: string;
|
|
8
|
+
yAxisLabel?: string;
|
|
9
|
+
showTicksX?: boolean;
|
|
10
|
+
showTicksY?: boolean;
|
|
11
|
+
showScaleX?: boolean;
|
|
12
|
+
showScaleY?: boolean;
|
|
13
|
+
scaleXUnit?: string;
|
|
14
|
+
scaleYUnit?: string;
|
|
15
|
+
showLegend?: boolean;
|
|
16
|
+
legendOrientation?: 'horizontal' | 'vertical';
|
|
17
|
+
showDataTable?: boolean;
|
|
18
|
+
showDataLabels?: boolean;
|
|
19
|
+
animate?: boolean;
|
|
20
|
+
animationDuration?: number;
|
|
21
|
+
width?: number;
|
|
22
|
+
height?: number;
|
|
23
|
+
colors?: string[];
|
|
24
|
+
class?: string;
|
|
25
|
+
style?: string;
|
|
26
|
+
theme?: 'google' | 'seriesa' | 'hr' | 'figma' | 'notion' | 'chalk' | 'mint';
|
|
27
|
+
styleMode?: 'default' | 'gradient' | 'outline' | 'dashed' | 'glow' | 'glass';
|
|
28
|
+
borderRadius?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface AreaChartState extends BaseChartState {
|
|
32
|
+
data: ChartDataPoint[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class AreaChart extends BaseChart<AreaChartState> {
|
|
36
|
+
constructor(id: string, options: AreaChartOptions = {}) {
|
|
37
|
+
const defaultColors = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6'];
|
|
38
|
+
const randomColor = defaultColors[Math.floor(Math.random() * defaultColors.length)];
|
|
39
|
+
|
|
40
|
+
super(id, {
|
|
41
|
+
data: options.data ?? [],
|
|
42
|
+
title: options.title ?? '',
|
|
43
|
+
subtitle: options.subtitle ?? '',
|
|
44
|
+
xAxisLabel: options.xAxisLabel ?? '',
|
|
45
|
+
yAxisLabel: options.yAxisLabel ?? '',
|
|
46
|
+
showTicksX: options.showTicksX ?? true,
|
|
47
|
+
showTicksY: options.showTicksY ?? true,
|
|
48
|
+
showScaleX: options.showScaleX ?? true,
|
|
49
|
+
showScaleY: options.showScaleY ?? true,
|
|
50
|
+
scaleXUnit: options.scaleXUnit ?? '',
|
|
51
|
+
scaleYUnit: options.scaleYUnit ?? '',
|
|
52
|
+
showLegend: options.showLegend ?? false,
|
|
53
|
+
legendOrientation: options.legendOrientation ?? 'horizontal',
|
|
54
|
+
showDataTable: options.showDataTable ?? false,
|
|
55
|
+
showDataLabels: options.showDataLabels ?? true,
|
|
56
|
+
animate: options.animate ?? true,
|
|
57
|
+
animationDuration: options.animationDuration ?? 800,
|
|
58
|
+
width: options.width ?? 600,
|
|
59
|
+
height: options.height ?? 400,
|
|
60
|
+
colors: options.colors ?? [randomColor],
|
|
61
|
+
class: options.class ?? '',
|
|
62
|
+
style: options.style ?? '',
|
|
63
|
+
theme: options.theme,
|
|
64
|
+
styleMode: options.styleMode ?? 'default',
|
|
65
|
+
borderRadius: options.borderRadius ?? 4
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected _getChartClassName(): string {
|
|
70
|
+
return 'jux-areachart';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected _createSVG(): SVGSVGElement {
|
|
74
|
+
const { data, width, height, colors, animate, animationDuration } = this.state;
|
|
75
|
+
|
|
76
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
77
|
+
svg.setAttribute('width', width.toString());
|
|
78
|
+
svg.setAttribute('height', height.toString());
|
|
79
|
+
svg.setAttribute('class', 'jux-areachart-svg');
|
|
80
|
+
|
|
81
|
+
if (!data.length) return svg;
|
|
82
|
+
|
|
83
|
+
if (animate) this._addAnimationStyles(svg);
|
|
84
|
+
|
|
85
|
+
const padding = { top: 40, right: 40, bottom: 60, left: 60 };
|
|
86
|
+
const chartWidth = width - padding.left - padding.right;
|
|
87
|
+
const chartHeight = height - padding.top - padding.bottom;
|
|
88
|
+
const maxValue = Math.max(...data.map(d => d.value));
|
|
89
|
+
|
|
90
|
+
this._renderAreaChart(svg, padding, chartWidth, chartHeight, maxValue);
|
|
91
|
+
|
|
92
|
+
return svg;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected _getBaseStyles(): string {
|
|
96
|
+
return `
|
|
97
|
+
.jux-areachart {
|
|
98
|
+
font-family: var(--chart-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
|
99
|
+
display: inline-block;
|
|
100
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
101
|
+
border-radius: 12px;
|
|
102
|
+
background: white;
|
|
103
|
+
padding: 24px;
|
|
104
|
+
}
|
|
105
|
+
.jux-areachart-title {
|
|
106
|
+
margin: 0 0 0.5rem 0;
|
|
107
|
+
font-size: 1.25rem;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
}
|
|
110
|
+
.jux-areachart-subtitle {
|
|
111
|
+
margin: 0 0 1rem 0;
|
|
112
|
+
font-size: 0.875rem;
|
|
113
|
+
color: #6b7280;
|
|
114
|
+
}
|
|
115
|
+
.jux-areachart-legend {
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-wrap: wrap;
|
|
118
|
+
gap: 1rem;
|
|
119
|
+
margin-top: 1rem;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
}
|
|
122
|
+
.jux-areachart-legend-item {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 0.5rem;
|
|
126
|
+
}
|
|
127
|
+
.jux-areachart-legend-swatch {
|
|
128
|
+
width: 12px;
|
|
129
|
+
height: 12px;
|
|
130
|
+
border-radius: 2px;
|
|
131
|
+
}
|
|
132
|
+
.jux-areachart-legend-label {
|
|
133
|
+
font-size: 0.875rem;
|
|
134
|
+
color: #374151;
|
|
135
|
+
}
|
|
136
|
+
.jux-areachart-table {
|
|
137
|
+
width: 100%;
|
|
138
|
+
margin-top: 1rem;
|
|
139
|
+
border-collapse: collapse;
|
|
140
|
+
font-size: 0.875rem;
|
|
141
|
+
}
|
|
142
|
+
.jux-areachart-table thead th {
|
|
143
|
+
text-align: center;
|
|
144
|
+
padding: 0.5rem;
|
|
145
|
+
border-bottom: 2px solid #e5e7eb;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
}
|
|
148
|
+
.jux-areachart-table tbody td {
|
|
149
|
+
padding: 0.5rem;
|
|
150
|
+
border-bottom: 1px solid #f3f4f6;
|
|
151
|
+
text-align: center;
|
|
152
|
+
}
|
|
153
|
+
.jux-areachart-svg {
|
|
154
|
+
font-family: inherit;
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private _addAnimationStyles(svg: SVGSVGElement): void {
|
|
160
|
+
const { animationDuration } = this.state;
|
|
161
|
+
const animationId = `area-grow-${this._id}`;
|
|
162
|
+
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
|
163
|
+
|
|
164
|
+
style.textContent = `
|
|
165
|
+
@keyframes ${animationId} {
|
|
166
|
+
from { opacity: 0; transform: scaleY(0); }
|
|
167
|
+
to { opacity: 1; transform: scaleY(1); }
|
|
168
|
+
}
|
|
169
|
+
.jux-area-animated {
|
|
170
|
+
transform-origin: bottom;
|
|
171
|
+
animation: ${animationId} ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
svg.appendChild(style);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private _renderAreaChart(svg: SVGSVGElement, padding: any, chartWidth: number, chartHeight: number, maxValue: number): void {
|
|
178
|
+
const { data, width, height, colors, showDataLabels, animate, xAxisLabel, yAxisLabel, showTicksX, showTicksY, showScaleX, showScaleY, scaleYUnit } = this.state;
|
|
179
|
+
|
|
180
|
+
const yScale = chartHeight / maxValue;
|
|
181
|
+
const xStep = chartWidth / (data.length - 1 || 1);
|
|
182
|
+
const color = colors[0];
|
|
183
|
+
|
|
184
|
+
// Axes
|
|
185
|
+
if (showScaleY) {
|
|
186
|
+
this._renderAxis(svg, padding.left, padding.top, padding.left, height - padding.bottom);
|
|
187
|
+
if (yAxisLabel && showTicksY) {
|
|
188
|
+
this._renderAxisLabel(svg, yAxisLabel, 20, padding.top + chartHeight / 2, -90);
|
|
189
|
+
}
|
|
190
|
+
if (showTicksY) {
|
|
191
|
+
this._renderYTicks(svg, maxValue, yScale, padding, width, height, scaleYUnit, chartHeight);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (showScaleX) {
|
|
196
|
+
this._renderAxis(svg, padding.left, height - padding.bottom, width - padding.right, height - padding.bottom);
|
|
197
|
+
if (xAxisLabel && showTicksX) {
|
|
198
|
+
this._renderAxisLabel(svg, xAxisLabel, padding.left + chartWidth / 2, height - 15, 0);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Build area path
|
|
203
|
+
let pathData = `M ${padding.left} ${height - padding.bottom}`;
|
|
204
|
+
data.forEach((point, i) => {
|
|
205
|
+
const x = padding.left + (i * xStep);
|
|
206
|
+
const y = height - padding.bottom - (point.value * yScale);
|
|
207
|
+
pathData += ` L ${x} ${y}`;
|
|
208
|
+
});
|
|
209
|
+
pathData += ` L ${padding.left + ((data.length - 1) * xStep)} ${height - padding.bottom} Z`;
|
|
210
|
+
|
|
211
|
+
const areaPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
212
|
+
areaPath.setAttribute('d', pathData);
|
|
213
|
+
areaPath.setAttribute('fill', color);
|
|
214
|
+
areaPath.setAttribute('fill-opacity', '0.3');
|
|
215
|
+
if (animate) areaPath.classList.add('jux-area-animated');
|
|
216
|
+
svg.appendChild(areaPath);
|
|
217
|
+
|
|
218
|
+
// Line path
|
|
219
|
+
let lineData = '';
|
|
220
|
+
data.forEach((point, i) => {
|
|
221
|
+
const x = padding.left + (i * xStep);
|
|
222
|
+
const y = height - padding.bottom - (point.value * yScale);
|
|
223
|
+
lineData += (i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const linePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
227
|
+
linePath.setAttribute('d', lineData);
|
|
228
|
+
linePath.setAttribute('stroke', color);
|
|
229
|
+
linePath.setAttribute('stroke-width', '3');
|
|
230
|
+
linePath.setAttribute('fill', 'none');
|
|
231
|
+
svg.appendChild(linePath);
|
|
232
|
+
|
|
233
|
+
// Data points
|
|
234
|
+
data.forEach((point, i) => {
|
|
235
|
+
const x = padding.left + (i * xStep);
|
|
236
|
+
const y = height - padding.bottom - (point.value * yScale);
|
|
237
|
+
|
|
238
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
239
|
+
circle.setAttribute('cx', x.toString());
|
|
240
|
+
circle.setAttribute('cy', y.toString());
|
|
241
|
+
circle.setAttribute('r', '4');
|
|
242
|
+
circle.setAttribute('fill', color);
|
|
243
|
+
circle.setAttribute('stroke', 'white');
|
|
244
|
+
circle.setAttribute('stroke-width', '2');
|
|
245
|
+
svg.appendChild(circle);
|
|
246
|
+
|
|
247
|
+
if (showDataLabels) {
|
|
248
|
+
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
249
|
+
label.setAttribute('x', x.toString());
|
|
250
|
+
label.setAttribute('y', (y - 10).toString());
|
|
251
|
+
label.setAttribute('text-anchor', 'middle');
|
|
252
|
+
label.setAttribute('fill', '#374151');
|
|
253
|
+
label.setAttribute('font-size', '11');
|
|
254
|
+
label.setAttribute('font-weight', '600');
|
|
255
|
+
label.textContent = point.value.toString();
|
|
256
|
+
svg.appendChild(label);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private _renderAxis(svg: SVGSVGElement, x1: number, y1: number, x2: number, y2: number): void {
|
|
262
|
+
const axis = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
263
|
+
axis.setAttribute('x1', x1.toString());
|
|
264
|
+
axis.setAttribute('y1', y1.toString());
|
|
265
|
+
axis.setAttribute('x2', x2.toString());
|
|
266
|
+
axis.setAttribute('y2', y2.toString());
|
|
267
|
+
axis.setAttribute('stroke', '#9ca3af');
|
|
268
|
+
axis.setAttribute('stroke-width', '2');
|
|
269
|
+
svg.appendChild(axis);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private _renderAxisLabel(svg: SVGSVGElement, text: string, x: number, y: number, rotate: number): void {
|
|
273
|
+
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
274
|
+
label.setAttribute('x', x.toString());
|
|
275
|
+
label.setAttribute('y', y.toString());
|
|
276
|
+
label.setAttribute('text-anchor', 'middle');
|
|
277
|
+
if (rotate !== 0) label.setAttribute('transform', `rotate(${rotate}, ${x}, ${y})`);
|
|
278
|
+
label.setAttribute('fill', '#6b7280');
|
|
279
|
+
label.setAttribute('font-size', '12');
|
|
280
|
+
label.setAttribute('font-weight', '500');
|
|
281
|
+
label.textContent = text;
|
|
282
|
+
svg.appendChild(label);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private _renderYTicks(svg: SVGSVGElement, maxValue: number, yScale: number, padding: any, width: number, height: number, unit: string, chartHeight: number): void {
|
|
286
|
+
const numTicks = 5;
|
|
287
|
+
for (let i = 0; i <= numTicks; i++) {
|
|
288
|
+
const value = (maxValue / numTicks) * i;
|
|
289
|
+
const y = height - padding.bottom - (value * yScale);
|
|
290
|
+
|
|
291
|
+
const gridLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
292
|
+
gridLine.setAttribute('x1', padding.left.toString());
|
|
293
|
+
gridLine.setAttribute('y1', y.toString());
|
|
294
|
+
gridLine.setAttribute('x2', (width - padding.right).toString());
|
|
295
|
+
gridLine.setAttribute('y2', y.toString());
|
|
296
|
+
gridLine.setAttribute('stroke', '#e5e7eb');
|
|
297
|
+
gridLine.setAttribute('stroke-width', '1');
|
|
298
|
+
gridLine.setAttribute('stroke-dasharray', '4,4');
|
|
299
|
+
svg.appendChild(gridLine);
|
|
300
|
+
|
|
301
|
+
const tickLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
302
|
+
tickLabel.setAttribute('x', (padding.left - 10).toString());
|
|
303
|
+
tickLabel.setAttribute('y', (y + 4).toString());
|
|
304
|
+
tickLabel.setAttribute('text-anchor', 'end');
|
|
305
|
+
tickLabel.setAttribute('fill', '#6b7280');
|
|
306
|
+
tickLabel.setAttribute('font-size', '11');
|
|
307
|
+
tickLabel.textContent = Math.round(value).toString() + (unit || '');
|
|
308
|
+
svg.appendChild(tickLabel);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function areachart(id: string, options: AreaChartOptions = {}): AreaChart {
|
|
314
|
+
return new AreaChart(id, options);
|
|
315
|
+
}
|