flet-box 0.0.0-stage → 1.0.2

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.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -2
  3. package/bin/cli.js +394 -0
  4. package/bin/commands/buildAndroid.js +200 -0
  5. package/bin/commands/buildIOS.js +237 -0
  6. package/bin/commands/create.js +406 -0
  7. package/bin/commands/createBundle.js +375 -0
  8. package/bin/commands/createComponent.js +115 -0
  9. package/bin/commands/createScreen.js +161 -0
  10. package/bin/commands/killServer.js +107 -0
  11. package/bin/commands/package.js +131 -0
  12. package/bin/commands/release.js +189 -0
  13. package/bin/commands/runBundle.js +62 -0
  14. package/bin/commands/runCloudflare.js +89 -0
  15. package/bin/commands/runNative.js +109 -0
  16. package/bin/commands/runServer.js +390 -0
  17. package/bin/ota/loader.js +361 -0
  18. package/bin/utils/bundleConfig.js +178 -0
  19. package/bin/utils/capacitor.js +137 -0
  20. package/bin/utils/colors.js +215 -0
  21. package/bin/utils/helpers.js +80 -0
  22. package/bin/utils/spinner.js +96 -0
  23. package/bin/utils/templates.js +1824 -0
  24. package/bin/utils/version.js +108 -0
  25. package/package.json +58 -4
  26. package/src/animations/AnimatedBox.js +607 -0
  27. package/src/animations/AnimatedText.js +105 -0
  28. package/src/animations/MatrixRain.js +220 -0
  29. package/src/animations/ParallaxBox.js +369 -0
  30. package/src/animations/animate.js +332 -0
  31. package/src/animations/index.js +25 -0
  32. package/src/app.js +41 -0
  33. package/src/assets/icon-1024-maskable.png +0 -0
  34. package/src/assets/icon-192-maskable.png +0 -0
  35. package/src/assets/icon-192.png +0 -0
  36. package/src/assets/icon-512-maskable.png +0 -0
  37. package/src/assets/icon-512.png +0 -0
  38. package/src/assets/logo.png +0 -0
  39. package/src/core/App.js +44 -0
  40. package/src/core/hmr-client.js +142 -0
  41. package/src/core/index.js +34 -0
  42. package/src/core/pwa.js +100 -0
  43. package/src/core/runApp.js +430 -0
  44. package/src/create-index.sh +237 -0
  45. package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
  46. package/src/fonts/icons.css +23 -0
  47. package/src/index.d.ts +1930 -0
  48. package/src/index.js +288 -0
  49. package/src/navigations/AdaptiveScaffold.js +149 -0
  50. package/src/navigations/AppBar.js +331 -0
  51. package/src/navigations/BottomNavigation.js +298 -0
  52. package/src/navigations/CollapsibleSideBar.js +139 -0
  53. package/src/navigations/Drawer.js +333 -0
  54. package/src/navigations/DrawerItem.js +260 -0
  55. package/src/navigations/Router.js +475 -0
  56. package/src/navigations/Scaffold.js +537 -0
  57. package/src/navigations/Tabs.js +384 -0
  58. package/src/navigations/index.js +57 -0
  59. package/src/services/RamStore.js +213 -0
  60. package/src/services/Session.js +310 -0
  61. package/src/services/Storage.js +309 -0
  62. package/src/services/http.js +350 -0
  63. package/src/services/index.js +57 -0
  64. package/src/tools/animation.js +42 -0
  65. package/src/tools/array.js +95 -0
  66. package/src/tools/border.js +28 -0
  67. package/src/tools/clipboard.js +104 -0
  68. package/src/tools/color.js +329 -0
  69. package/src/tools/createList.js +215 -0
  70. package/src/tools/delay.js +55 -0
  71. package/src/tools/device.js +138 -0
  72. package/src/tools/dict.js +297 -0
  73. package/src/tools/dimensions.js +118 -0
  74. package/src/tools/filter.js +41 -0
  75. package/src/tools/flex.js +26 -0
  76. package/src/tools/gradient.js +34 -0
  77. package/src/tools/grid.js +57 -0
  78. package/src/tools/index.js +38 -0
  79. package/src/tools/mapList.js +107 -0
  80. package/src/tools/margin.js +93 -0
  81. package/src/tools/memo.js +60 -0
  82. package/src/tools/os.js +104 -0
  83. package/src/tools/padding.js +93 -0
  84. package/src/tools/print.js +69 -0
  85. package/src/tools/random.js +299 -0
  86. package/src/tools/ref.js +70 -0
  87. package/src/tools/rgba.js +4 -0
  88. package/src/tools/shadow.js +23 -0
  89. package/src/tools/string.js +85 -0
  90. package/src/tools/time.js +674 -0
  91. package/src/tools/transform.js +68 -0
  92. package/src/tools/transition.js +48 -0
  93. package/src/tools/useState.js +268 -0
  94. package/src/tools/uuid.js +42 -0
  95. package/src/utils/TextInputValidator.js +256 -0
  96. package/src/utils/composeUpdate.js +65 -0
  97. package/src/utils/getWidgetProps.js +35 -0
  98. package/src/utils/index.js +76 -0
  99. package/src/utils/markdownParser.js +368 -0
  100. package/src/utils/mediaTime.js +79 -0
  101. package/src/utils/navigation.js +263 -0
  102. package/src/utils/scriptInjector.js +73 -0
  103. package/src/utils/stackPosition.js +31 -0
  104. package/src/utils/stopWebRefresh.js +77 -0
  105. package/src/utils/styleInjector.js +38 -0
  106. package/src/utils/syntaxHighlight.js +339 -0
  107. package/src/utils/themes.js +347 -0
  108. package/src/utils/units.js +87 -0
  109. package/src/utils/visualEffects.js +169 -0
  110. package/src/widget-factory/addChildren.js +114 -0
  111. package/src/widget-factory/assignProps.js +256 -0
  112. package/src/widget-factory/createWidget.js +28 -0
  113. package/src/widget-factory/effects.js +132 -0
  114. package/src/widget-factory/index.js +7 -0
  115. package/src/widget-factory/lifecycle.js +210 -0
  116. package/src/widget-factory/makeParentable.js +89 -0
  117. package/src/widget-factory/pixTool.js +40 -0
  118. package/src/widget-factory/processProps.js +122 -0
  119. package/src/widget-factory/reactivity.js +98 -0
  120. package/src/widget-factory/remTool.js +34 -0
  121. package/src/widget-factory/stackPosition.js +87 -0
  122. package/src/widget-factory/stringTool.js +26 -0
  123. package/src/widget-factory/tools.js +249 -0
  124. package/src/widget-factory/translateProps.js +1049 -0
  125. package/src/widget-factory/warnings.js +284 -0
  126. package/src/widget-factory/widgetFactory.js +200 -0
  127. package/src/widgets/Accordion.js +562 -0
  128. package/src/widgets/AdMob.js +315 -0
  129. package/src/widgets/AdSense.js +268 -0
  130. package/src/widgets/AlertDialog.js +162 -0
  131. package/src/widgets/Audio.js +311 -0
  132. package/src/widgets/Avatar.js +210 -0
  133. package/src/widgets/Badge.js +189 -0
  134. package/src/widgets/BottomSheet.js +287 -0
  135. package/src/widgets/Button.js +247 -0
  136. package/src/widgets/Card.js +33 -0
  137. package/src/widgets/Carousel.js +301 -0
  138. package/src/widgets/Chart.js +632 -0
  139. package/src/widgets/Checkbox.js +106 -0
  140. package/src/widgets/Chip.js +153 -0
  141. package/src/widgets/CircularBar.js +352 -0
  142. package/src/widgets/CircularChart.js +549 -0
  143. package/src/widgets/CodeViewer.js +236 -0
  144. package/src/widgets/Column.js +29 -0
  145. package/src/widgets/Container.js +31 -0
  146. package/src/widgets/DataTable.js +257 -0
  147. package/src/widgets/Divider.js +93 -0
  148. package/src/widgets/DraggBox.js +115 -0
  149. package/src/widgets/Dropdown.js +505 -0
  150. package/src/widgets/DroppBox.js +180 -0
  151. package/src/widgets/FloatingActionButton.js +91 -0
  152. package/src/widgets/GridView.js +20 -0
  153. package/src/widgets/Icon.js +33 -0
  154. package/src/widgets/Image.js +15 -0
  155. package/src/widgets/Input.js +565 -0
  156. package/src/widgets/Inspector.js +254 -0
  157. package/src/widgets/InstallButton.js +110 -0
  158. package/src/widgets/ListTile.js +224 -0
  159. package/src/widgets/ListView.js +503 -0
  160. package/src/widgets/Markdown.js +1236 -0
  161. package/src/widgets/Modal.js +496 -0
  162. package/src/widgets/Pagination.js +195 -0
  163. package/src/widgets/ProgressBar.js +178 -0
  164. package/src/widgets/QRCode.js +467 -0
  165. package/src/widgets/Radio.js +132 -0
  166. package/src/widgets/Rating.js +261 -0
  167. package/src/widgets/Row.js +29 -0
  168. package/src/widgets/Skeleton.js +243 -0
  169. package/src/widgets/Slider.js +527 -0
  170. package/src/widgets/SnackBar.js +227 -0
  171. package/src/widgets/Stack.js +29 -0
  172. package/src/widgets/Stepper.js +296 -0
  173. package/src/widgets/Switch.js +102 -0
  174. package/src/widgets/Text.js +98 -0
  175. package/src/widgets/Tooltip.js +267 -0
  176. package/src/widgets/TreeView.js +370 -0
  177. package/src/widgets/Video.js +92 -0
  178. package/src/widgets/index.js +52 -0
@@ -0,0 +1,632 @@
1
+ /**
2
+ * @file Chart.js
3
+ * @description A canvas-based chart widget supporting bar, line, area, and
4
+ * candlestick chart types. Features smooth Bézier curves, gradient fills,
5
+ * configurable grid/axis rendering, and automatic resizing via ResizeObserver.
6
+ * Candlestick charts support horizontal scrolling when the data set is wider
7
+ * than the container.
8
+ */
9
+
10
+ import { WidgetFactory } from "../widget-factory/index.js";
11
+ import { colors } from "../utils/themes.js";
12
+
13
+ /**
14
+ * Creates a Chart widget rendered on an HTML5 `<canvas>` element.
15
+ *
16
+ * @param {Object} props - Configuration for the chart.
17
+ * @param {'candle'|'bar'|'line'|'area'} [props.type='candle'] - Chart type to render.
18
+ * @param {Array} [props.data=[]] - Dataset.
19
+ * - For 'bar', 'line', 'area': an array of numbers.
20
+ * - For 'candle': an array of `{open, high, low, close}` objects, or 4-element arrays `[open, high, low, close]`.
21
+ * @param {string[]} [props.labels=[]] - X-axis labels, one per data point.
22
+ * @param {number|string} [props.width='100%'] - Container width (CSS value or pixel number).
23
+ * @param {number} [props.height=400] - Container height in pixels.
24
+ * @param {string} [props.bgColor=colors.surface] - Background color of the chart container.
25
+ * @param {Object} [props.padding={top:20,right:50,bottom:50,left:50}] - Canvas padding in pixels. Can also be a single number.
26
+ * @param {number} [props.candleWidth=8] - Width of each candlestick body in pixels.
27
+ * @param {number} [props.candleSpacing=2] - Gap between consecutive candlesticks in pixels.
28
+ * @param {string} [props.barColor=colors.primary] - Fill color for bar chart bars.
29
+ * @param {string} [props.lineColor=colors.primary] - Stroke color for line/area charts.
30
+ * @param {string} [props.areaColor] - Fill color for the area under line charts (default: primary at 25% opacity).
31
+ * @param {boolean} [props.smooth=false] - When true, line/area charts use quadratic Bézier curves instead of straight segments.
32
+ * @param {boolean} [props.areaGradient=false] - When true, the area fill uses a vertical gradient instead of a flat color.
33
+ * @param {string[]|null} [props.areaGradientColors=null] - Two-element color array `[bottomColor, topColor]` for the gradient. Defaults to `[lineColor, transparent]`.
34
+ * @param {string} [props.candleUpColor=colors.success] - Candlestick body color for bullish (close ≥ open) candles.
35
+ * @param {string} [props.candleDownColor=colors.danger] - Candlestick body color for bearish (close < open) candles.
36
+ * @param {string} [props.axisColor=colors.border] - Color of the axis lines and grid lines.
37
+ * @param {string} [props.textColor=colors.textSecondary] - Color of axis labels.
38
+ * @param {string} [props.yAxisColor=colors.primary] - Color of the Y-axis value labels.
39
+ * @param {boolean} [props.showGrid=true] - Whether to render horizontal grid lines.
40
+ * @param {boolean} [props.showLabels=true] - Whether to render X-axis labels below each data point.
41
+ * @param {boolean} [props.showValues=false] - Whether to render the raw value above each data point.
42
+ * @param {number} [props.borderRadius=8] - Corner radius of the chart container in pixels.
43
+ * @returns {HTMLElement} The chart container element, augmented with `updateData` and `redraw` methods.
44
+ */
45
+ export const Chart = (props) => {
46
+ const {
47
+ type = "candle",
48
+ data = [],
49
+ labels = [],
50
+ width = "100%",
51
+ height = 400,
52
+ bgColor = colors.surface,
53
+ padding = { top: 20, right: 50, bottom: 50, left: 50 },
54
+ candleWidth = 8,
55
+ candleSpacing = 2,
56
+ barColor = colors.primary,
57
+ lineColor = colors.primary,
58
+ areaColor = `${colors.primary}40`,
59
+ // Smooth curves and gradient fill options
60
+ smooth = false,
61
+ areaGradient = false,
62
+ areaGradientColors = null,
63
+ candleUpColor = colors.success,
64
+ candleDownColor = colors.danger,
65
+ axisColor = colors.border,
66
+ textColor = colors.textSecondary,
67
+ yAxisColor = colors.primary,
68
+ showGrid = true,
69
+ showLabels = true,
70
+ showValues = false,
71
+ borderRadius = 8,
72
+ ...rest
73
+ } = props;
74
+
75
+ if (!data.length) return null;
76
+
77
+ let canvasRef = null;
78
+ let context = null;
79
+ let scrollContainerRef = null;
80
+ let isCandle = type === "candle";
81
+ let dynamicCandleWidth = candleWidth;
82
+ let dynamicCandleSpacing = candleSpacing;
83
+ let useFixedWidth = true;
84
+
85
+ /**
86
+ * Normalises the `padding` prop into a consistent `{top, right, bottom, left}` object.
87
+ * Accepts either a single number (applied to all sides) or a partial object.
88
+ *
89
+ * @returns {{top: number, right: number, bottom: number, left: number}}
90
+ */
91
+ const normalizePadding = () => {
92
+ if (typeof padding === "number") {
93
+ return { top: padding, right: padding, bottom: padding, left: padding };
94
+ }
95
+ return { top: 20, right: 50, bottom: 50, left: 50, ...padding };
96
+ };
97
+
98
+ const pad = normalizePadding();
99
+
100
+ /**
101
+ * Normalises the candle data so that every entry is a `{open, high, low, close}` object,
102
+ * regardless of whether the caller passed 4-element arrays or plain objects.
103
+ *
104
+ * @returns {{open: number, high: number, low: number, close: number}[]}
105
+ */
106
+ const normalizeCandleData = () => {
107
+ return data.map((item) => {
108
+ if (Array.isArray(item)) {
109
+ return { open: item[0], high: item[1], low: item[2], close: item[3] };
110
+ }
111
+ return item;
112
+ });
113
+ };
114
+
115
+ /**
116
+ * Computes the minimum and maximum values across the dataset, adding 10%
117
+ * padding on each side so data points are never flush against the edges.
118
+ *
119
+ * @returns {{maxValue: number, minValue: number}}
120
+ */
121
+ const getMinMaxValues = () => {
122
+ let minVal, maxVal;
123
+ if (type === "candle") {
124
+ const candleData = normalizeCandleData();
125
+ const highs = candleData.map((d) => d.high);
126
+ const lows = candleData.map((d) => d.low);
127
+ maxVal = Math.max(...highs);
128
+ minVal = Math.min(...lows);
129
+ } else {
130
+ maxVal = Math.max(...data);
131
+ minVal = Math.min(...data);
132
+ }
133
+ const range = maxVal - minVal;
134
+ const paddingRange = range * 0.1;
135
+ return {
136
+ maxValue: maxVal + paddingRange,
137
+ minValue: Math.max(0, minVal - paddingRange),
138
+ };
139
+ };
140
+
141
+ /**
142
+ * Converts a data value to a Y pixel coordinate on the canvas.
143
+ *
144
+ * @param {number} value - The data value to convert.
145
+ * @param {number} minValue - Minimum value in the visible range.
146
+ * @param {number} maxValue - Maximum value in the visible range.
147
+ * @param {number} chartHeight - Total canvas height in pixels.
148
+ * @returns {number} The Y coordinate in canvas space.
149
+ */
150
+ const getYPosition = (value, minValue, maxValue, chartHeight) => {
151
+ const range = maxValue - minValue;
152
+ if (range === 0) return chartHeight - pad.bottom;
153
+ const ratio = (value - minValue) / range;
154
+ return (
155
+ chartHeight - pad.bottom - ratio * (chartHeight - pad.top - pad.bottom)
156
+ );
157
+ };
158
+
159
+ /**
160
+ * Draws a bar chart onto the canvas context.
161
+ *
162
+ * @param {CanvasRenderingContext2D} ctx
163
+ * @param {number} canvasWidth
164
+ * @param {number} chartHeight
165
+ * @param {number} maxValue
166
+ * @param {number} minValue
167
+ */
168
+ const drawBar = (ctx, canvasWidth, chartHeight, maxValue, minValue) => {
169
+ const availableWidth = canvasWidth - pad.left - pad.right;
170
+ const barWidth = (availableWidth / data.length) * 0.7;
171
+ const barSpacing = (availableWidth / data.length) * 0.3;
172
+
173
+ data.forEach((value, index) => {
174
+ const x = pad.left + index * (barWidth + barSpacing) + barSpacing / 2;
175
+ const y = getYPosition(value, minValue, maxValue, chartHeight);
176
+ const baselineY = getYPosition(0, minValue, maxValue, chartHeight);
177
+ const barHeight = Math.abs(y - baselineY);
178
+
179
+ ctx.fillStyle = barColor;
180
+ ctx.fillRect(x, Math.min(y, baselineY), barWidth, barHeight);
181
+
182
+ if (showValues && labels[index]) {
183
+ ctx.fillStyle = textColor;
184
+ ctx.font = "8px sans-serif";
185
+ ctx.fillText(value, x + barWidth / 2 - 5, y - 2);
186
+ ctx.save();
187
+ ctx.translate(x + barWidth / 2, chartHeight - pad.bottom + 8);
188
+ ctx.rotate(-0.3);
189
+ ctx.fillStyle = textColor;
190
+ ctx.textAlign = "center";
191
+ ctx.fillText(labels[index], 0, 0);
192
+ ctx.restore();
193
+ }
194
+ });
195
+ };
196
+
197
+ /**
198
+ * Draws a line or area chart onto the canvas context.
199
+ * Uses quadratic Bézier midpoints to produce smooth curves when `smooth` is true.
200
+ *
201
+ * @param {CanvasRenderingContext2D} ctx
202
+ * @param {number} canvasWidth
203
+ * @param {number} chartHeight
204
+ * @param {number} maxValue
205
+ * @param {number} minValue
206
+ * @param {boolean} [isArea=false] - When true, fills the area below the line.
207
+ */
208
+ const drawLine = (
209
+ ctx,
210
+ canvasWidth,
211
+ chartHeight,
212
+ maxValue,
213
+ minValue,
214
+ isArea = false,
215
+ ) => {
216
+ const availableWidth = canvasWidth - pad.left - pad.right;
217
+ const step = availableWidth / (data.length - 1);
218
+ const points = [];
219
+
220
+ data.forEach((value, index) => {
221
+ const x = pad.left + index * step;
222
+ const y = getYPosition(value, minValue, maxValue, chartHeight);
223
+ points.push({ x, y, value, label: labels[index] });
224
+ });
225
+
226
+ if (points.length < 2) return;
227
+
228
+ // Fill the area under the line when rendering an area chart
229
+ if (isArea) {
230
+ // Build a gradient or flat fill depending on configuration
231
+ let fillStyle = areaColor;
232
+ if (areaGradient) {
233
+ let grad;
234
+ if (areaGradientColors && areaGradientColors.length >= 2) {
235
+ // Caller-supplied two-color gradient (bottom → top)
236
+ grad = ctx.createLinearGradient(
237
+ 0,
238
+ chartHeight - pad.bottom,
239
+ 0,
240
+ pad.top,
241
+ );
242
+ grad.addColorStop(0, areaGradientColors[0]);
243
+ grad.addColorStop(1, areaGradientColors[1]);
244
+ } else {
245
+ // Default gradient: solid lineColor at bottom, transparent at top
246
+ grad = ctx.createLinearGradient(
247
+ 0,
248
+ chartHeight - pad.bottom,
249
+ 0,
250
+ pad.top,
251
+ );
252
+ grad.addColorStop(0, lineColor);
253
+ grad.addColorStop(1, "transparent");
254
+ }
255
+ fillStyle = grad;
256
+ }
257
+ ctx.beginPath();
258
+ if (smooth) {
259
+ // Smooth area: trace through midpoints, then close back to the baseline
260
+ ctx.moveTo(points[0].x, points[0].y);
261
+ for (let i = 0; i < points.length - 1; i++) {
262
+ const xc = (points[i].x + points[i + 1].x) / 2;
263
+ const yc = (points[i].y + points[i + 1].y) / 2;
264
+ ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
265
+ }
266
+ ctx.quadraticCurveTo(
267
+ points[points.length - 2].x,
268
+ points[points.length - 2].y,
269
+ points[points.length - 1].x,
270
+ points[points.length - 1].y,
271
+ );
272
+ ctx.lineTo(points[points.length - 1].x, chartHeight - pad.bottom);
273
+ ctx.lineTo(points[0].x, chartHeight - pad.bottom);
274
+ ctx.fillStyle = fillStyle;
275
+ ctx.fill();
276
+ } else {
277
+ // Straight-line area
278
+ ctx.moveTo(points[0].x, points[0].y);
279
+ for (let i = 1; i < points.length; i++) {
280
+ ctx.lineTo(points[i].x, points[i].y);
281
+ }
282
+ ctx.lineTo(points[points.length - 1].x, chartHeight - pad.bottom);
283
+ ctx.lineTo(points[0].x, chartHeight - pad.bottom);
284
+ ctx.fillStyle = fillStyle;
285
+ ctx.fill();
286
+ }
287
+ }
288
+
289
+ // Draw the line stroke on top of (or without) the fill
290
+ ctx.beginPath();
291
+ ctx.moveTo(points[0].x, points[0].y);
292
+ if (smooth) {
293
+ for (let i = 0; i < points.length - 1; i++) {
294
+ const xc = (points[i].x + points[i + 1].x) / 2;
295
+ const yc = (points[i].y + points[i + 1].y) / 2;
296
+ ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
297
+ }
298
+ ctx.quadraticCurveTo(
299
+ points[points.length - 2].x,
300
+ points[points.length - 2].y,
301
+ points[points.length - 1].x,
302
+ points[points.length - 1].y,
303
+ );
304
+ } else {
305
+ for (let i = 1; i < points.length; i++) {
306
+ ctx.lineTo(points[i].x, points[i].y);
307
+ }
308
+ }
309
+ ctx.strokeStyle = lineColor;
310
+ ctx.lineWidth = 2;
311
+ ctx.stroke();
312
+
313
+ // Draw small circles at each data point and optional labels
314
+ points.forEach((point) => {
315
+ ctx.fillStyle = lineColor;
316
+ ctx.beginPath();
317
+ ctx.arc(point.x, point.y, 2, 0, 2 * Math.PI);
318
+ ctx.fill();
319
+
320
+ if (showValues) {
321
+ ctx.fillStyle = textColor;
322
+ ctx.font = "8px sans-serif";
323
+ ctx.fillText(point.value, point.x - 5, point.y - 4);
324
+ }
325
+ if (showLabels && point.label) {
326
+ ctx.save();
327
+ ctx.translate(point.x, chartHeight - pad.bottom + 8);
328
+ ctx.rotate(-0.3);
329
+ ctx.fillStyle = textColor;
330
+ ctx.textAlign = "center";
331
+ ctx.fillText(point.label, 0, 0);
332
+ ctx.restore();
333
+ }
334
+ });
335
+ };
336
+
337
+ /**
338
+ * Draws a candlestick chart onto the canvas context.
339
+ *
340
+ * @param {CanvasRenderingContext2D} ctx
341
+ * @param {number} canvasWidth
342
+ * @param {number} chartHeight
343
+ * @param {number} maxValue
344
+ * @param {number} minValue
345
+ * @param {number} candleW - Width of each candle body in pixels.
346
+ * @param {number} candleSp - Spacing between candles in pixels.
347
+ */
348
+ const drawCandle = (
349
+ ctx,
350
+ canvasWidth,
351
+ chartHeight,
352
+ maxValue,
353
+ minValue,
354
+ candleW,
355
+ candleSp,
356
+ ) => {
357
+ const candleData = normalizeCandleData();
358
+ const step = candleW + candleSp;
359
+ const startX = pad.left;
360
+
361
+ candleData.forEach((item, index) => {
362
+ const isUp = item.close >= item.open;
363
+ const color = isUp ? candleUpColor : candleDownColor;
364
+ const x = startX + index * step;
365
+ const highY = getYPosition(item.high, minValue, maxValue, chartHeight);
366
+ const lowY = getYPosition(item.low, minValue, maxValue, chartHeight);
367
+ const openY = getYPosition(item.open, minValue, maxValue, chartHeight);
368
+ const closeY = getYPosition(item.close, minValue, maxValue, chartHeight);
369
+
370
+ // Wick (high–low line through the center of the candle)
371
+ ctx.beginPath();
372
+ ctx.moveTo(x + candleW / 2, highY);
373
+ ctx.lineTo(x + candleW / 2, lowY);
374
+ ctx.strokeStyle = color;
375
+ ctx.lineWidth = 1;
376
+ ctx.stroke();
377
+
378
+ // Candle body (open–close rectangle); minimum 1 px tall for doji candles
379
+ const bodyTop = Math.min(openY, closeY);
380
+ const bodyHeight = Math.max(1, Math.abs(closeY - openY));
381
+ ctx.fillStyle = color;
382
+ ctx.fillRect(x, bodyTop, candleW, bodyHeight);
383
+
384
+ if (showLabels && labels[index]) {
385
+ ctx.save();
386
+ ctx.translate(x + candleW / 2, chartHeight - pad.bottom + 8);
387
+ ctx.rotate(-0.3);
388
+ ctx.fillStyle = textColor;
389
+ ctx.textAlign = "center";
390
+ ctx.fillText(labels[index], 0, 0);
391
+ ctx.restore();
392
+ }
393
+ });
394
+ };
395
+
396
+ /**
397
+ * Draws horizontal grid lines and Y-axis value labels.
398
+ *
399
+ * @param {CanvasRenderingContext2D} ctx
400
+ * @param {number} canvasWidth
401
+ * @param {number} chartHeight
402
+ * @param {number} maxValue
403
+ * @param {number} minValue
404
+ */
405
+ const drawGrid = (ctx, canvasWidth, chartHeight, maxValue, minValue) => {
406
+ if (!showGrid) return;
407
+
408
+ ctx.strokeStyle = axisColor;
409
+ ctx.lineWidth = 0.5;
410
+
411
+ const steps = 5;
412
+ const range = maxValue - minValue;
413
+
414
+ for (let i = 0; i <= steps; i++) {
415
+ const value = minValue + (i / steps) * range;
416
+ const y = getYPosition(value, minValue, maxValue, chartHeight);
417
+ ctx.beginPath();
418
+ ctx.moveTo(pad.left, y);
419
+ ctx.lineTo(canvasWidth - pad.right, y);
420
+ ctx.stroke();
421
+
422
+ // Round very small floating-point values to 0 to avoid displaying "-0.00"
423
+ const displayValue = Math.abs(value) < 0.01 ? 0 : Math.round(value);
424
+ ctx.fillStyle = yAxisColor;
425
+ ctx.font = "9px sans-serif";
426
+ ctx.textAlign = "right";
427
+ ctx.fillText(displayValue, pad.left - 5, y + 3);
428
+ }
429
+ };
430
+
431
+ /**
432
+ * Draws the X and Y axis lines.
433
+ *
434
+ * @param {CanvasRenderingContext2D} ctx
435
+ * @param {number} canvasWidth
436
+ * @param {number} chartHeight
437
+ */
438
+ const drawAxis = (ctx, canvasWidth, chartHeight) => {
439
+ ctx.beginPath();
440
+ ctx.moveTo(pad.left, pad.top);
441
+ ctx.lineTo(pad.left, chartHeight - pad.bottom);
442
+ ctx.lineTo(canvasWidth - pad.right, chartHeight - pad.bottom);
443
+ ctx.strokeStyle = axisColor;
444
+ ctx.lineWidth = 1;
445
+ ctx.stroke();
446
+ };
447
+
448
+ /**
449
+ * Full redraw cycle: clears the canvas, recomputes sizes, and paints all layers.
450
+ * Triggered by ResizeObserver, window resize, and data updates.
451
+ */
452
+ const draw = () => {
453
+ if (!canvasRef || !context || !scrollContainerRef) return;
454
+
455
+ const containerRect =
456
+ scrollContainerRef.parentElement.getBoundingClientRect();
457
+ const chartHeight = containerRect.height;
458
+ const containerWidth = containerRect.width;
459
+
460
+ if (isCandle) {
461
+ // For candlestick charts: if the data fits inside the container, distribute
462
+ // candles evenly; otherwise use fixed sizes and enable horizontal scrolling
463
+ const fixedTotalWidth =
464
+ pad.left + data.length * (candleWidth + candleSpacing) + pad.right;
465
+
466
+ if (fixedTotalWidth <= containerWidth) {
467
+ useFixedWidth = false;
468
+ const availableWidth = containerWidth - pad.left - pad.right;
469
+ const totalSpacing = (data.length - 1) * candleSpacing;
470
+ let newCandleWidth = (availableWidth - totalSpacing) / data.length;
471
+ newCandleWidth = Math.max(2, newCandleWidth);
472
+ dynamicCandleWidth = newCandleWidth;
473
+ dynamicCandleSpacing = candleSpacing;
474
+
475
+ canvasRef.width = containerWidth;
476
+ canvasRef.style.width = "100%";
477
+ } else {
478
+ useFixedWidth = true;
479
+ dynamicCandleWidth = candleWidth;
480
+ dynamicCandleSpacing = candleSpacing;
481
+ const totalFixed =
482
+ pad.left + data.length * (candleWidth + candleSpacing) + pad.right;
483
+ canvasRef.width = totalFixed;
484
+ canvasRef.style.width = `${totalFixed}px`;
485
+ }
486
+ } else {
487
+ canvasRef.width = containerWidth;
488
+ canvasRef.style.width = "100%";
489
+ }
490
+
491
+ canvasRef.height = chartHeight;
492
+ canvasRef.style.height = `${chartHeight}px`;
493
+
494
+ context.clearRect(0, 0, canvasRef.width, chartHeight);
495
+
496
+ const { maxValue, minValue } = getMinMaxValues();
497
+ const canvasWidth = canvasRef.width;
498
+
499
+ drawGrid(context, canvasWidth, chartHeight, maxValue, minValue);
500
+ drawAxis(context, canvasWidth, chartHeight);
501
+
502
+ if (type === "bar") {
503
+ drawBar(context, canvasWidth, chartHeight, maxValue, minValue);
504
+ } else if (type === "line") {
505
+ drawLine(context, canvasWidth, chartHeight, maxValue, minValue, false);
506
+ } else if (type === "area") {
507
+ drawLine(context, canvasWidth, chartHeight, maxValue, minValue, true);
508
+ } else if (type === "candle") {
509
+ drawCandle(
510
+ context,
511
+ canvasWidth,
512
+ chartHeight,
513
+ maxValue,
514
+ minValue,
515
+ dynamicCandleWidth,
516
+ dynamicCandleSpacing,
517
+ );
518
+ }
519
+ };
520
+
521
+ // ========== DOM CONSTRUCTION ==========
522
+
523
+ const container = WidgetFactory({
524
+ widgetName: "Chart",
525
+ width: typeof width === "number" ? `${width}px` : width,
526
+ height: typeof height === "number" ? `${height}px` : height,
527
+ backgroundColor: bgColor,
528
+ borderRadius:
529
+ typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
530
+ overflow: "hidden",
531
+ display: "flex",
532
+ flexDirection: "column",
533
+ ...rest,
534
+ });
535
+
536
+ // Inner scrollable layer — only relevant for candlestick overflow
537
+ const scrollContainer = WidgetFactory({
538
+ tag: "div",
539
+ flex: 1,
540
+ overflow: isCandle && useFixedWidth ? "auto" : "hidden",
541
+ position: "relative",
542
+ });
543
+ scrollContainerRef = scrollContainer;
544
+
545
+ const canvas = document.createElement("canvas");
546
+ canvas.style.display = "block";
547
+
548
+ canvasRef = canvas;
549
+ context = canvas.getContext("2d");
550
+
551
+ scrollContainer.appendChild(canvas);
552
+ container.appendChild(scrollContainer);
553
+
554
+ // Throttle scroll redraws to ~60 fps
555
+ let drawTimeout;
556
+ const handleScroll = () => {
557
+ if (drawTimeout) clearTimeout(drawTimeout);
558
+ drawTimeout = setTimeout(draw, 16);
559
+ };
560
+
561
+ if (isCandle) {
562
+ scrollContainer.addEventListener("scroll", handleScroll);
563
+ }
564
+
565
+ // Automatically redraw when the container is resized
566
+ const resizeObserver = new ResizeObserver(() => draw());
567
+ resizeObserver.observe(scrollContainer);
568
+
569
+ /**
570
+ * Updates the scroll container's overflow mode after a draw, since
571
+ * `useFixedWidth` may have changed during the draw call.
572
+ */
573
+ const updateOverflow = () => {
574
+ if (isCandle && scrollContainerRef) {
575
+ const shouldScroll = useFixedWidth;
576
+ scrollContainerRef.style.overflow = shouldScroll ? "auto" : "hidden";
577
+ }
578
+ };
579
+
580
+ const drawWithOverflow = () => {
581
+ draw();
582
+ updateOverflow();
583
+ };
584
+
585
+ // Throttle window resize redraws to avoid excessive repaints
586
+ let windowResizeTimeout;
587
+ const handleWindowResize = () => {
588
+ if (windowResizeTimeout) clearTimeout(windowResizeTimeout);
589
+ windowResizeTimeout = setTimeout(drawWithOverflow, 50);
590
+ };
591
+ window.addEventListener("resize", handleWindowResize);
592
+
593
+ // Defer the initial draw so the container has been laid out by the browser
594
+ const initialDrawTimeout = setTimeout(drawWithOverflow, 100);
595
+
596
+ // ========== PUBLIC API ==========
597
+
598
+ /**
599
+ * Replaces the chart's dataset and redraws.
600
+ *
601
+ * @param {Array} newData - New data array (same format as the original `data` prop).
602
+ * @param {string[]} [newLabels] - New labels array. When provided, replaces the existing labels.
603
+ */
604
+ container.updateData = (newData, newLabels) => {
605
+ data.length = 0;
606
+ data.push(...newData);
607
+ if (newLabels) {
608
+ labels.length = 0;
609
+ labels.push(...newLabels);
610
+ }
611
+ drawWithOverflow();
612
+ };
613
+
614
+ /** Triggers a full redraw without changing the data. */
615
+ container.redraw = drawWithOverflow;
616
+
617
+ // Cleanup: remove all listeners, observers, and timers
618
+ container.onUnmount(() => {
619
+ if (drawTimeout) clearTimeout(drawTimeout);
620
+ if (windowResizeTimeout) clearTimeout(windowResizeTimeout);
621
+ if (initialDrawTimeout) clearTimeout(initialDrawTimeout);
622
+ resizeObserver?.disconnect();
623
+ window.removeEventListener("resize", handleWindowResize);
624
+ if (isCandle) {
625
+ scrollContainer.removeEventListener("scroll", handleScroll);
626
+ }
627
+ });
628
+
629
+ return container;
630
+ };
631
+
632
+ export default Chart;