@componyx/ui 0.0.0-stage → 1.0.3

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 (169) hide show
  1. package/AppSkin/AppSkin.css +1268 -0
  2. package/AppSkin/AppSkin.js +2748 -0
  3. package/AppSkin/Themes/Default.css +241 -0
  4. package/Base/Base.js +3407 -0
  5. package/Base/ComponentIcons.css +66 -0
  6. package/Base/Font/ComponentIcons.woff +0 -0
  7. package/Base/Font/UI.woff +0 -0
  8. package/Base/Library.js +8432 -0
  9. package/Base/NavigationManager.js +462 -0
  10. package/Base/ResponsiveTemplates.js +371 -0
  11. package/Base/Sanitizer.js +288 -0
  12. package/Base/Themes/Default.css +255 -0
  13. package/Base/UI.css +618 -0
  14. package/Base/UIResourcePath.js +6 -0
  15. package/Bindary/Bindary.js +2011 -0
  16. package/Bindary/Modules/Connection.js +216 -0
  17. package/Bindary/Modules/Core.js +854 -0
  18. package/Bindary/Modules/DataUpdater.js +351 -0
  19. package/Bindary/Modules/ExpressionEngine.js +347 -0
  20. package/Bindary/Modules/ProxyManager.js +482 -0
  21. package/Bindary/Modules/Renderer.js +605 -0
  22. package/Bindary/Modules/Router.js +291 -0
  23. package/Bindary/Modules/Scroller.js +89 -0
  24. package/Bindary/Modules/Template.js +1273 -0
  25. package/BookmarkSpy/BookmarkSpy.js +430 -0
  26. package/Box/Box.css +36 -0
  27. package/Box/Box.js +1685 -0
  28. package/Button/Button.css +470 -0
  29. package/Button/Button.js +1398 -0
  30. package/Button/Themes/Default.css +200 -0
  31. package/Carousel/Carousel.css +273 -0
  32. package/Carousel/Carousel.js +607 -0
  33. package/ColorButton/ColorButton.css +39 -0
  34. package/ColorButton/ColorButton.js +336 -0
  35. package/ColorPicker/ColorPicker.css +359 -0
  36. package/ColorPicker/ColorPicker.js +972 -0
  37. package/ComboBox/ComboBox.css +357 -0
  38. package/ComboBox/ComboBox.js +2342 -0
  39. package/ComboBox/Themes/Default.css +23 -0
  40. package/DataPager/DataPager.css +72 -0
  41. package/DataPager/DataPager.js +674 -0
  42. package/DatePicker/DatePicker.css +227 -0
  43. package/DatePicker/DatePicker.js +1531 -0
  44. package/DatePicker/Themes/Default.css +26 -0
  45. package/Dialog/Dialog.css +106 -0
  46. package/Dialog/Dialog.js +572 -0
  47. package/Dialog/Themes/Default.css +10 -0
  48. package/Editor/Data/Emoji.json +8285 -0
  49. package/Editor/Data/Special.json +1042 -0
  50. package/Editor/Editor.css +1562 -0
  51. package/Editor/Editor.js +4058 -0
  52. package/Editor/Font/Editor.woff +0 -0
  53. package/Editor/Modules/ComponentFactory.js +565 -0
  54. package/Editor/Modules/ContentManager.js +748 -0
  55. package/Editor/Modules/DialogManager.js +1234 -0
  56. package/Editor/Modules/EventManager.js +458 -0
  57. package/Editor/Modules/Format.js +950 -0
  58. package/Editor/Modules/History.js +191 -0
  59. package/Editor/Modules/LayoutState.js +379 -0
  60. package/Editor/Modules/ListManager.js +296 -0
  61. package/Editor/Modules/MenuManager.js +388 -0
  62. package/Editor/Modules/NodeManager.js +681 -0
  63. package/Editor/Modules/ParagraphButtons.js +122 -0
  64. package/Editor/Modules/Resizer.js +176 -0
  65. package/Editor/Modules/SelectionRange.js +564 -0
  66. package/Editor/Modules/SourceViewBuilder.js +212 -0
  67. package/Editor/Modules/SymbolPicker.js +208 -0
  68. package/Editor/Modules/Table.js +2327 -0
  69. package/Editor/Modules/TableDialog.js +757 -0
  70. package/Editor/Modules/Toolbar.js +375 -0
  71. package/Editor/Modules/TooltipMenu.js +125 -0
  72. package/Editor/Modules/Utility.js +131 -0
  73. package/Editor/Themes/Default.css +204 -0
  74. package/FileUpload/FileUpload.css +156 -0
  75. package/FileUpload/FileUpload.js +898 -0
  76. package/FileUpload/Themes/Default.css +17 -0
  77. package/Form/Form.css +1934 -0
  78. package/Form/Form.js +4943 -0
  79. package/Form/Modules/ActionManager.js +368 -0
  80. package/Form/Modules/BuildPanelManager.js +319 -0
  81. package/Form/Modules/ComponentFactory.js +468 -0
  82. package/Form/Modules/ComponentPanel.js +265 -0
  83. package/Form/Modules/ConfigPanelManager.js +685 -0
  84. package/Form/Modules/DataObserver.js +717 -0
  85. package/Form/Modules/Databinder.js +602 -0
  86. package/Form/Modules/Draggable.js +756 -0
  87. package/Form/Modules/OptionsPanel.js +342 -0
  88. package/Form/Modules/Renderer.js +1889 -0
  89. package/Form/Modules/RuleEngine.js +585 -0
  90. package/Form/Modules/RulesPanel.js +1158 -0
  91. package/Form/Modules/Types.js +1709 -0
  92. package/Form/Modules/ValidationManager.js +194 -0
  93. package/Form/Themes/Default.css +216 -0
  94. package/FormField/FormField.css +468 -0
  95. package/FormField/FormField.js +576 -0
  96. package/FormField/Themes/Default.css +99 -0
  97. package/Grid/Grid.css +544 -0
  98. package/Grid/Grid.js +3761 -0
  99. package/Grid/Themes/Default.css +43 -0
  100. package/LICENSE +126 -0
  101. package/MaskedTextBox/MaskedTextBox.css +28 -0
  102. package/MaskedTextBox/MaskedTextBox.js +642 -0
  103. package/MaskedTextBox/Themes/Default.css +6 -0
  104. package/Menu/Menu.css +328 -0
  105. package/Menu/Menu.js +2386 -0
  106. package/Menu/Themes/Default.css +24 -0
  107. package/NumericBox/NumericBox.css +120 -0
  108. package/NumericBox/NumericBox.js +984 -0
  109. package/PanelBar/PanelBar.css +123 -0
  110. package/PanelBar/PanelBar.js +505 -0
  111. package/PanelBar/Themes/Default.css +51 -0
  112. package/PieChart/PieChart.css +95 -0
  113. package/PieChart/PieChart.js +820 -0
  114. package/PieChart/Themes/Default.css +9 -0
  115. package/README.md +178 -3
  116. package/Slider/Slider.css +348 -0
  117. package/Slider/Slider.js +1306 -0
  118. package/Slider/Themes/Default.css +44 -0
  119. package/TabStrip/TabStrip.css +52 -0
  120. package/TabStrip/TabStrip.js +362 -0
  121. package/TabStrip/Themes/Default.css +35 -0
  122. package/TimePicker/Themes/Default.css +30 -0
  123. package/TimePicker/TimePicker.css +253 -0
  124. package/TimePicker/TimePicker.js +1485 -0
  125. package/TooltipManager/Themes/Default.css +21 -0
  126. package/TooltipManager/TooltipManager.css +227 -0
  127. package/TooltipManager/TooltipManager.js +791 -0
  128. package/Validator/Validator.css +54 -0
  129. package/Validator/Validator.js +2508 -0
  130. package/componyx.html-data.json +619 -0
  131. package/index.js +27 -0
  132. package/package.json +43 -4
  133. package/types/AppSkin.d.ts +842 -0
  134. package/types/Base.d.ts +1034 -0
  135. package/types/Bindary.d.ts +1196 -0
  136. package/types/BookmarkSpy.d.ts +112 -0
  137. package/types/Box.d.ts +374 -0
  138. package/types/Button.d.ts +455 -0
  139. package/types/Carousel.d.ts +136 -0
  140. package/types/ColorButton.d.ts +120 -0
  141. package/types/ColorPicker.d.ts +225 -0
  142. package/types/ComboBox.d.ts +507 -0
  143. package/types/DataPager.d.ts +152 -0
  144. package/types/DatePicker.d.ts +354 -0
  145. package/types/Dialog.d.ts +160 -0
  146. package/types/Editor.d.ts +1289 -0
  147. package/types/FileUpload.d.ts +206 -0
  148. package/types/Form.d.ts +3078 -0
  149. package/types/FormField.d.ts +204 -0
  150. package/types/Grid.d.ts +911 -0
  151. package/types/Library.d.ts +1899 -0
  152. package/types/MaskedTextBox.d.ts +91 -0
  153. package/types/Menu.d.ts +577 -0
  154. package/types/NumericBox.d.ts +310 -0
  155. package/types/PanelBar.d.ts +223 -0
  156. package/types/PieChart.d.ts +224 -0
  157. package/types/ResponsiveTemplates.d.ts +108 -0
  158. package/types/Slider.d.ts +227 -0
  159. package/types/TabStrip.d.ts +185 -0
  160. package/types/TimePicker.d.ts +265 -0
  161. package/types/TooltipManager.d.ts +178 -0
  162. package/types/UI.d.ts +29 -0
  163. package/types/Validator.d.ts +653 -0
  164. package/types/index.d.ts +59 -0
  165. package/ui.css +11783 -0
  166. package/ui.js +79477 -0
  167. package/ui.min.css +138 -0
  168. package/ui.min.js +264 -0
  169. package/ui.min.js.map +1 -0
@@ -0,0 +1,820 @@
1
+ /*! Componyx.UI | (c) E.H. Daanen / Componyx | BUSL-1.1 | componyx.com/license */
2
+ import '../Base/Base.js';
3
+ import '../Base/Library.js';
4
+ import '../TooltipManager/TooltipManager.js';
5
+
6
+ (function ()
7
+ {
8
+ /**
9
+ * PieChart CSS Variables.
10
+ * @typedef {Object} CSSVariables
11
+ * @memberof componyx.UI.PieChart
12
+ * @property {string} ["--donut-inner-stop"] - This CSS value is based on the donutInnerRatio setting.
13
+ * @property {string} ["--chart-size"] - The rendered chart size in pixels (half-pie only), used to align the legend with the semicircle.
14
+ */
15
+
16
+ /**
17
+ * PieChart class.
18
+ * @class
19
+ * @memberof componyx.UI
20
+ * @augments componyx.UI.base.WebComponent
21
+ * @mixes componyx.UI.base.methods
22
+ * @param {String} id The id of the component.
23
+ * @param {Object|HTMLElement} properties The properties used to initialize the component or the container element.
24
+ * @returns {componyx.UI.PieChart} An instance of the component.
25
+ */
26
+ class PieChart extends componyx.UI.base.WebComponent
27
+ {
28
+ #chart;
29
+ #segmentOverlay;
30
+ #legend;
31
+ #size;
32
+ #totalAngleDeg;
33
+ #startAngleDeg;
34
+ #totalValue;
35
+ #segmentAngleRanges = []; // Cache for segment angle ranges
36
+ #currentHoveredIndex = null;
37
+ #tooltipManager;
38
+ #leaveTimerId;
39
+ #segmentLabels = []; // label elements with their mid angle, for repositioning
40
+ #resizeObserver;
41
+ #outsidePointerDown;
42
+
43
+ /**
44
+ * Internal CSS class name constants.
45
+ * You can override any of these classes on the Component instance by defining a property named
46
+ * `cssClass<Key>` where <Key> is the PascalCase key from this object.
47
+ * Example:
48
+ * 'PANEL' -> 'cssClassPanel'
49
+ * Be cautious: overriding these classes without including the default names may break styling and functionality.
50
+ * @constant
51
+ * @type {Readonly<Object<string, string>>}
52
+ */
53
+ classOption =
54
+ {
55
+ CHART: 'chart',
56
+ HALF: 'half-pie',
57
+ DONUT: 'donut',
58
+ LEGEND: 'legend',
59
+ LEGEND_ITEM: 'legend-item',
60
+ LEGEND_ITEM_COLOR: 'legend-item-color',
61
+ LEGEND_ITEM_LABEL: 'legend-item-label',
62
+ SEGMENT_LABEL: 'segment-label',
63
+ SEGMENT_OVERLAY: 'segment-overlay',
64
+ SEGMENT_HOVER: 'segment-hover'
65
+ };
66
+
67
+ constructor(id, properties)
68
+ {
69
+ super(id, properties);
70
+
71
+ /**
72
+ * Gets or sets the shape of the pie chart (PIE or DONUT).
73
+ * @type {componyx.UI.PieChart.ShapeOption}
74
+ */
75
+ this.shape = PieChart.ShapeOption.PIE;
76
+
77
+ /**
78
+ * Gets or sets where the segment labels are displayed (NONE, INNER, or OUTER).
79
+ * @type {componyx.UI.PieChart.SegmentLabelDisplayOption}
80
+ */
81
+ this.segmentLabelDisplay = PieChart.SegmentLabelDisplayOption.OUTER;
82
+
83
+ /**
84
+ * Gets or sets the content of the segment labels (VALUE, LABEL, LABEL_VALUE).
85
+ * @type {componyx.UI.PieChart.SegmentLabelContentOption}
86
+ */
87
+ this.segmentLabelContent = PieChart.SegmentLabelContentOption.VALUE;
88
+
89
+ /**
90
+ * Gets or sets a value indicating whether the chart spans half a circle.
91
+ * @type {Boolean}
92
+ */
93
+ this.isHalf = false;
94
+
95
+ /**
96
+ * Gets or sets a value indicating whether the legend is displayed.
97
+ * @type {Boolean}
98
+ */
99
+ this.showLegend = true;
100
+
101
+ /**
102
+ * Gets or sets a value indicating whether segment values are shown in the legend labels.
103
+ * @type {Boolean}
104
+ */
105
+ this.showValuesInLegend = false;
106
+
107
+ /**
108
+ * Gets or sets the start angle in degrees (0 = top, clockwise). Only applies to FULL variant.
109
+ * @type {Number}
110
+ */
111
+ this.startAngle = 0;
112
+
113
+ /**
114
+ * Gets or sets the inner hole size for DONUT variant as a ratio (0-1, where 0.6 = 60% hole).
115
+ * @type {Number}
116
+ */
117
+ this.donutInnerRatio = 0.6;
118
+
119
+ /**
120
+ * Gets or sets the base HEX color used for segments without a specified color. The colors of such segments are generated by shifting the hue of the base color.
121
+ * @type {String}
122
+ */
123
+ this.baseColor = '#3b82f6';
124
+
125
+ /**
126
+ * Gets or sets the segment label positioning offset.
127
+ * For OUTER labels: distance in pixels from chart edge (default: 20).
128
+ * For INNER labels: position ratio from 0 (inner edge) to 1 (outer edge) (default: 0.5).
129
+ * @type {Number}
130
+ */
131
+ this.segmentLabelOffset = null;
132
+
133
+ /**
134
+ * Gets or sets the list of segments.
135
+ * @type {componyx.UI.PieChart.Segment[]}
136
+ */
137
+ this.segments = [];
138
+
139
+ /**
140
+ * Gets or sets the id of the component from which the settings are cloned for the tooltip manager used by the PieChart component.
141
+ * @type {string}
142
+ */
143
+ this.tooltipManagerId = null;
144
+
145
+ /**
146
+ * @class
147
+ * @augments componyx.UI.base.Events
148
+ * @memberof componyx.UI.PieChart
149
+ * @property {componyx.UI.base.Event} onSegmentEnter - Fires when the pointer enters a segment. @see {@link componyx.UI.PieChart.PieChartEventArgs}
150
+ * @property {componyx.UI.base.Event} onSegmentLeave - Fires when the pointer leaves a segment. @see {@link componyx.UI.PieChart.PieChartEventArgs}
151
+ * @property {componyx.UI.base.Event} onSegmentClick - Fires when a segment is clicked. @see {@link componyx.UI.PieChart.PieChartEventArgs}
152
+ * @property {componyx.UI.base.Event} onRenderSegmentLabel - Fires when a segment label is rendered. @see {@link componyx.UI.PieChart.PieChartLabelRenderEventArgs}
153
+ * @property {componyx.UI.base.Event} onRenderLegendLabel - Fires when a legend label is rendered. @see {@link componyx.UI.PieChart.PieChartLabelRenderEventArgs}
154
+ * @see {@link componyx.UI.base.Events}
155
+ */
156
+ function PieChartEvents(events)
157
+ {
158
+ Object.assign(this, events);
159
+
160
+ this.onSegmentEnter = $base.static.createEvent('onSegmentEnter');
161
+ this.onSegmentLeave = $base.static.createEvent('onSegmentLeave');
162
+ this.onSegmentClick = $base.static.createEvent('onSegmentClick');
163
+ this.onRenderSegmentLabel = $base.static.createEvent('onRenderSegmentLabel');
164
+ this.onRenderLegendLabel = $base.static.createEvent('onRenderLegendLabel');
165
+ }
166
+ /**
167
+ * PieChart events
168
+ * @type {componyx.UI.PieChart.PieChartEvents}
169
+ */
170
+ this.events = new PieChartEvents(this.events);
171
+
172
+ /**
173
+ * @typedef {Object} componyx.UI.PieChart.PieChartEventArgs
174
+ * @property {componyx.UI.PieChart} PieChart - The PieChart instance.
175
+ * @property {Object} eventArgs - The event object containing more detailed information about the event.
176
+ * @property {componyx.UI.PieChart.Segment} eventArgs.segment - The data segment.
177
+ * @property {Event} eventArgs.event - The original event object.
178
+ */
179
+
180
+ /**
181
+ * @typedef {Object} componyx.UI.PieChart.PieChartLabelRenderEventArgs
182
+ * @property {componyx.UI.PieChart} PieChart - The PieChart instance.
183
+ * @property {componyx.UI.PieChart.Segment} segment - The data segment.
184
+ * @property {HTMLElement} element - The label DOM element (segment label or legend label) that can be customized.
185
+ */
186
+ }
187
+
188
+ /**
189
+ * Renders the component.
190
+ */
191
+ render()
192
+ {
193
+ if (this.renderState != $base.static.RenderState.RENDERING)
194
+ {
195
+ super.render(this.#preRender, 'pie-chart');
196
+ return;
197
+ }
198
+
199
+ // render logic after loading resources
200
+ this.#draw();
201
+ this.renderChildren();
202
+ }
203
+
204
+ /**
205
+ * Executes post render tasks.
206
+ * @private
207
+ */
208
+ async postRender()
209
+ {
210
+ if (this.renderState != $base.static.RenderState.RENDERING) // extra safety to never execute a postRender when the component state is incorrect
211
+ return;
212
+
213
+ super.postRender();
214
+ this.#positionSegmentLabels();
215
+ }
216
+
217
+ /**
218
+ * Destroys the component.
219
+ * @param {Boolean} keepEvents A value indicating if component events should be kept.
220
+ * @param {Boolean} removeElement=true A value indicating if the corresponding HTML Element should be removed.
221
+ * @see {@link componyx.UI.base.methods#destroy}
222
+ * @function
223
+ */
224
+ destroy(keepEvents, removeElement = true)
225
+ {
226
+ this.#dispose();
227
+ super.destroy(keepEvents, removeElement);
228
+ }
229
+
230
+ /**
231
+ * Gets the css class if it exists and otherwise the default css class.
232
+ * @returns {String} The css class.
233
+ */
234
+ getCssClass(cssClassValue)
235
+ {
236
+ return super.getCssClass(this.classOption, cssClassValue);
237
+ }
238
+
239
+ #preRender()
240
+ {
241
+ let script = [];
242
+
243
+ script.push('TooltipManager');
244
+ return ['PieChart', script];
245
+ }
246
+
247
+ #draw()
248
+ {
249
+ // Sum values for normalization
250
+ this.#totalValue = this.segments.reduce((sum, seg) => sum + seg.value, 0);
251
+
252
+ // For HALF variant, we'll start at 270° (bottom/6 o'clock) and go 180° clockwise to 90° (top/12 o'clock)
253
+ // This way the semicircle goes from left to right across the top
254
+ if (this.isHalf)
255
+ {
256
+ this.classList.add(this.getCssClass(this.classOption.HALF));
257
+ this.#startAngleDeg = 270; // Start at bottom
258
+ this.#totalAngleDeg = 180; // Half circle
259
+ }
260
+ else
261
+ {
262
+ this.#startAngleDeg = this.startAngle ?? 0;
263
+ this.#totalAngleDeg = 360; // Full circle
264
+ }
265
+
266
+ if (this.shape === PieChart.ShapeOption.DONUT)
267
+ {
268
+ this.classList.add(this.getCssClass(this.classOption.DONUT));
269
+ this.style.setProperty('--donut-inner-stop', `${this.donutInnerRatio * 100}%`);
270
+ }
271
+
272
+ this.#tooltipManager = this.#createTooltipManager(this, this.id + '_TooltipManager', this.tooltipManagerId);
273
+ this.#buildSegmentAngleRanges();
274
+ this.#generateSegmentColors();
275
+ this.#createChart();
276
+
277
+ if (this.showLegend)
278
+ this.#createLegend();
279
+
280
+ this.style.removeProperty('--chart-size');
281
+
282
+ if (this.isHalf)
283
+ {
284
+ this.style.setProperty('--chart-size', `${this.#size}px`); // used by the CSS to align the legend with the semicircle
285
+ }
286
+ }
287
+
288
+ #createTooltipManager(container, id, cloneId)
289
+ {
290
+ const tooltipBox = $UI.createComponent(componyx.UI.Box, {
291
+ id: id + '_TooltipManagerBox',
292
+ containerElement: container,
293
+ trackPointer: true,
294
+ alignX: componyx.UI.Box.AlignXOption.CENTER,
295
+ alignY: componyx.UI.Box.AlignYOption.BOTTOM,
296
+ expandDirection: componyx.UI.Box.ExpandDirectionOption.UP,
297
+ autoInvertFit: true,
298
+ animation: { showType: 0, hideType: 0 }
299
+ });
300
+
301
+ return this.#createComponent(container, componyx.UI.TooltipManager, id, cloneId, {
302
+ boxId: tooltipBox.id,
303
+ selectableTooltipContent: false,
304
+ arrowless: true
305
+ });
306
+ }
307
+
308
+ #createComponent(container, type, id, cloneId, config = {})
309
+ {
310
+ const component = $UI.createComponent(type, { id: id, containerElement: container });
311
+
312
+ component.clone($UI.store[cloneId], this);
313
+ Object.assign(component, Object.fromEntries(Object.entries(config).filter(([k, v]) => v !== undefined))); // don't assign undefined properties
314
+ component.showing = true;
315
+ component.events.onPostRender.priorityAdd(() => { this.isReady.apply(this); }, null, true);
316
+
317
+ return component;
318
+ }
319
+
320
+ /**
321
+ * Assign colors to segments if not already set.
322
+ * Colors are based on this.baseColor and evenly shifted in hue.
323
+ * @private
324
+ */
325
+ #generateSegmentColors()
326
+ {
327
+ const segments = this.#segmentAngleRanges.map(r => r.segment),
328
+ segmentCount = segments.length,
329
+ [baseR, baseG, baseB] = $lib.hexToRgb(this.baseColor),
330
+ [baseHue, baseSaturation, baseValue] = $lib.rgbToHsv(baseR, baseG, baseB),
331
+ hueStep = (this.isHalf ? 0.5 : 1) / segmentCount; // Calculate hue step per segment, 0.5 = half pie, 1 = full pie
332
+
333
+ segments.forEach((segment, index) =>
334
+ {
335
+ if (!segment.color)
336
+ {
337
+ const newHue = (baseHue + index * hueStep) % 1, // Shift hue for this segment
338
+ saturation = Math.max(baseSaturation, 0.6), // clamp saturation/value for nicer colors
339
+ value = Math.max(baseValue, 0.75),
340
+ [r, g, b] = $lib.hsvToRgb(newHue, saturation, value);
341
+
342
+ segment.color = '#' + $lib.rgbToHex(r, g, b);
343
+ }
344
+ });
345
+ }
346
+
347
+ #createChart()
348
+ {
349
+ this.#chart = $lib.element({ container: this, attrs: { class: this.getCssClass(this.classOption.CHART) } });
350
+ this.#chart.style.background = this.#createConicGradient();
351
+
352
+ this.classList.remove('hidden');
353
+ this.#size = this.#chart.getBoundingClientRect().width;
354
+ this.classList.add('hidden');
355
+
356
+ if (this.segmentLabelDisplay !== PieChart.SegmentLabelDisplayOption.NONE)
357
+ this.#createSegmentLabels();
358
+
359
+ this.#segmentOverlay = $lib.element({ container: this.#chart, attrs: { class: this.getCssClass(this.classOption.SEGMENT_OVERLAY) } });
360
+ this.#bindEvents();
361
+ this.#observeResize();
362
+ }
363
+
364
+ #createSegmentLabels()
365
+ {
366
+ let currentAngleDeg = this.#startAngleDeg;
367
+ this.#segmentLabels = [];
368
+
369
+ this.segments.forEach(segment =>
370
+ {
371
+ const segmentAngleDeg = this.#getSegmentAngleDeg(segment.value),
372
+ midAngleDeg = this.#getMidAngleDeg(currentAngleDeg, segmentAngleDeg),
373
+ label = {
374
+ element: $lib.element({ container: this, attrs: { class: this.getCssClass(this.classOption.SEGMENT_LABEL) } }),
375
+ midAngleRad: this.#degToRad(this.#cssNormalize(midAngleDeg))
376
+ },
377
+ value = `${segment.value}${segment.valueSuffix || ''}`;
378
+
379
+ if (this.segmentLabelContent == PieChart.SegmentLabelContentOption.LABEL_VALUE)
380
+ label.element.innerText = `${segment.label} (${value})`;
381
+ else if (this.segmentLabelContent == PieChart.SegmentLabelContentOption.LABEL)
382
+ label.element.innerText = segment.label;
383
+ else // default to VALUE
384
+ label.element.innerText = value;
385
+
386
+ this.#segmentLabels.push(label);
387
+ this.#positionSegmentLabel(label);
388
+ this.events.onRenderSegmentLabel.fire(this, { segment, element: label.element });
389
+
390
+ currentAngleDeg += segmentAngleDeg;
391
+ });
392
+ }
393
+
394
+ #positionSegmentLabels()
395
+ {
396
+ this.#segmentLabels.forEach(label => this.#positionSegmentLabel(label));
397
+ }
398
+
399
+ #positionSegmentLabel(label)
400
+ {
401
+ const outerChartRadius = this.#size / 2,
402
+ innerChartRadius = this.shape === PieChart.ShapeOption.DONUT ? outerChartRadius * this.donutInnerRatio : 0,
403
+ isInnerLabel = (this.segmentLabelDisplay === PieChart.SegmentLabelDisplayOption.INNER),
404
+ labelOffset = this.segmentLabelOffset ?? (isInnerLabel ? 0.5 : 20),
405
+ labelRadius = isInnerLabel
406
+ ? innerChartRadius + (outerChartRadius - innerChartRadius) * labelOffset // ratio (0-1) of the ring width
407
+ : outerChartRadius + labelOffset, // pixels from the outer edge
408
+ chartRect = this.#chart.getBoundingClientRect(), // rendered position of the chart, including any CSS transforms
409
+ hostRect = this.getBoundingClientRect(),
410
+ centerX = chartRect.left - hostRect.left + outerChartRadius,
411
+ centerY = chartRect.top - hostRect.top + outerChartRadius;
412
+
413
+ label.element.style.left = `${centerX + (labelRadius * Math.cos(label.midAngleRad))}px`;
414
+ label.element.style.top = `${centerY + (labelRadius * Math.sin(label.midAngleRad))}px`;
415
+ }
416
+
417
+ #observeResize()
418
+ {
419
+ this.#resizeObserver?.disconnect();
420
+ this.#resizeObserver = new ResizeObserver(() =>
421
+ {
422
+ const size = this.#chart?.getBoundingClientRect().width;
423
+
424
+ if (!size || size === this.#size) // hidden (0) or unchanged
425
+ return;
426
+
427
+ this.#size = size;
428
+ this.#positionSegmentLabels();
429
+
430
+ if (this.isHalf)
431
+ {
432
+ this.style.setProperty('--chart-size', `${size}px`);
433
+ }
434
+ });
435
+
436
+ this.#resizeObserver.observe(this.#chart);
437
+ }
438
+
439
+ #createLegend()
440
+ {
441
+ this.#legend = $lib.element({ container: this, attrs: { class: this.getCssClass(this.classOption.LEGEND) } });
442
+
443
+ this.segments.forEach(segment =>
444
+ {
445
+ const itemEl = $lib.element({ container: this.#legend, attrs: { class: this.getCssClass(this.classOption.LEGEND_ITEM) } }),
446
+ colorEl = $lib.element({ tag: 'span', container: itemEl, attrs: { class: this.getCssClass(this.classOption.LEGEND_ITEM_COLOR) } }),
447
+ labelEl = $lib.element({ tag: 'span', container: itemEl, attrs: { class: this.getCssClass(this.classOption.LEGEND_ITEM_LABEL) } });
448
+
449
+ colorEl.style.backgroundColor = segment.color;
450
+
451
+ if (this.showValuesInLegend)
452
+ labelEl.innerText = `${segment.label} (${segment.value}${segment.valueSuffix})`;
453
+ else
454
+ labelEl.innerText = segment.label;
455
+
456
+ this.events.onRenderLegendLabel.fire(this, { segment, element: labelEl });
457
+ });
458
+ }
459
+
460
+ #bindEvents()
461
+ {
462
+ $lib.on(this.#chart, 'pointermove pointerdown', (e) =>
463
+ {
464
+ clearTimeout(this.#leaveTimerId);
465
+ const segmentIndex = this.#getSegmentAtPointer(e);
466
+
467
+ if (segmentIndex !== this.#currentHoveredIndex)
468
+ {
469
+ this.#currentHoveredIndex = segmentIndex;
470
+ this.#hoverSegment(segmentIndex);
471
+
472
+ if (segmentIndex !== null)
473
+ {
474
+ this.#chart.style.cursor = 'pointer';
475
+ this.events.onSegmentEnter.fire(this, { segment: this.segments[segmentIndex], event: e });
476
+ }
477
+ else
478
+ {
479
+ this.#chart.style.cursor = 'default';
480
+ this.events.onSegmentLeave.fire(this, { segment: null, event: e });
481
+ }
482
+ }
483
+ });
484
+
485
+ $lib.on(this.#chart, 'click', (e) =>
486
+ {
487
+ const segmentIndex = this.#getSegmentAtPointer(e);
488
+ if (segmentIndex !== null)
489
+ {
490
+ this.events.onSegmentClick.fire(this, { segment: this.segments[segmentIndex], event: e });
491
+ }
492
+ });
493
+
494
+ $lib.on(this.#chart, 'pointerleave', (e) =>
495
+ {
496
+ if (e.pointerType === 'touch') // a touch pointer "leaves" when the finger lifts, the tooltip stays until a tap elsewhere
497
+ return;
498
+
499
+ this.#leaveTimerId = setTimeout(() => this.#clearHover(e));
500
+ });
501
+
502
+ // on touch, a tap outside the chart dismisses the hovered segment
503
+ this.#outsidePointerDown = (e) =>
504
+ {
505
+ if (e.pointerType === 'touch' && !this.#chart?.contains(e.target))
506
+ this.#clearHover(e);
507
+ };
508
+
509
+ this.ownerDocument.addEventListener('pointerdown', this.#outsidePointerDown);
510
+ }
511
+
512
+ #clearHover(e)
513
+ {
514
+ if (this.#currentHoveredIndex !== null)
515
+ {
516
+ this.#currentHoveredIndex = null;
517
+ this.#hoverSegment(null);
518
+ this.#chart.style.cursor = 'default';
519
+ this.events.onSegmentLeave.fire(this, { segment: null, event: e });
520
+ }
521
+ }
522
+
523
+ #buildSegmentAngleRanges()
524
+ {
525
+ this.#segmentAngleRanges = [];
526
+ let currentAngleDeg = 0;
527
+
528
+ this.segments.forEach((segment, index) =>
529
+ {
530
+ const segmentAngleDeg = this.#getSegmentAngleDeg(segment.value);
531
+ const nextAngleDeg = currentAngleDeg + segmentAngleDeg;
532
+
533
+ this.#segmentAngleRanges.push({
534
+ startAngle: currentAngleDeg,
535
+ endAngle: nextAngleDeg,
536
+ segment
537
+ });
538
+
539
+ currentAngleDeg = nextAngleDeg;
540
+ });
541
+ }
542
+
543
+ #createConicGradient()
544
+ {
545
+ let gradientParts = [];
546
+
547
+ this.#segmentAngleRanges.forEach((range) =>
548
+ {
549
+ const endColor = range.segment.endColor || range.segment.color;
550
+ gradientParts.push(`${range.segment.color} ${range.startAngle}deg, ${endColor} ${range.endAngle}deg`);
551
+ });
552
+
553
+ // For half-pie, fill the rest of the circle with transparent
554
+ if (this.isHalf)
555
+ gradientParts.push(`transparent ${this.#totalAngleDeg}deg 360deg`);
556
+
557
+ // Use 'from' to set where the gradient starts
558
+ return `conic-gradient(from ${this.#startAngleDeg}deg, ${gradientParts.join(', ')})`;
559
+ }
560
+
561
+ #hoverSegment(hoveredIndex)
562
+ {
563
+ this.#tooltipManager.removeTooltip(this.id);
564
+
565
+ if (hoveredIndex === null)
566
+ {
567
+ this.#segmentOverlay.style.background = 'transparent';
568
+ this.#segmentOverlay.classList.remove(this.getCssClass(this.classOption.SEGMENT_HOVER));
569
+ return;
570
+ }
571
+
572
+ const range = this.#segmentAngleRanges[hoveredIndex];
573
+
574
+ // Simple 3-stop gradient: transparent before, color on segment (the color is lightened with CSS), transparent after
575
+ this.#segmentOverlay.style.background = `conic-gradient(from ${this.#startAngleDeg}deg, transparent 0deg ${range.startAngle}deg, ${range.segment.color} ${range.startAngle}deg ${range.endAngle}deg, transparent ${range.endAngle}deg 360deg)`;
576
+ this.#segmentOverlay.classList.add(this.getCssClass(this.classOption.SEGMENT_HOVER));
577
+
578
+ if (range.segment.tooltip)
579
+ {
580
+ this.#tooltipManager.addTooltip(this.id, range.segment.tooltip);
581
+ this.#tooltipManager.addTrigger(this.#segmentOverlay, this.id);
582
+ this.#tooltipManager.showTooltip(this.#segmentOverlay, true);
583
+ }
584
+ }
585
+
586
+ #getSegmentAtPointer(event)
587
+ {
588
+ const rect = this.#chart.getBoundingClientRect(),
589
+ size = rect.width,
590
+ x = event.clientX - rect.left,
591
+ y = event.clientY - rect.top,
592
+ centerX = size / 2,
593
+ centerY = size / 2,
594
+ dx = x - centerX,
595
+ dy = y - centerY,
596
+ distance = Math.hypot(dx, dy), // calculates Euclidean distance (same as sqrt(dx*dx + dy*dy)) using Pythagorean theorem
597
+ outerRadius = size / 2,
598
+ innerRadius = this.shape === PieChart.ShapeOption.DONUT ? outerRadius * this.donutInnerRatio : 0;
599
+
600
+ if (distance < innerRadius || distance > outerRadius)
601
+ {
602
+ return null;
603
+ }
604
+
605
+ // Calculate angle from mouse position
606
+ let angleRad = Math.atan2(dy, dx);
607
+ let angleDeg = (angleRad * 180 / Math.PI);
608
+ if (angleDeg < 0) angleDeg += 360;
609
+
610
+ // Adjust for CSS coordinate system (0° = top, clockwise)
611
+ angleDeg = (angleDeg + 90) % 360;
612
+
613
+ // Adjust for startAngle to get relative angle
614
+ let relativeAngle = angleDeg - this.#startAngleDeg;
615
+ if (relativeAngle < 0) relativeAngle += 360;
616
+
617
+ // For HALF variant, check if in visible range
618
+ if (this.isHalf && relativeAngle > this.#totalAngleDeg)
619
+ {
620
+ return null;
621
+ }
622
+
623
+ // Binary search through cached angle ranges
624
+ let left = 0;
625
+ let right = this.#segmentAngleRanges.length - 1;
626
+
627
+ while (left <= right)
628
+ {
629
+ const mid = Math.floor((left + right) / 2);
630
+ const range = this.#segmentAngleRanges[mid];
631
+
632
+ if (relativeAngle >= range.startAngle && relativeAngle < range.endAngle)
633
+ {
634
+ return mid;
635
+ }
636
+ else if (relativeAngle < range.startAngle)
637
+ {
638
+ right = mid - 1; // Search left half
639
+ }
640
+ else
641
+ {
642
+ left = mid + 1; // Search right half
643
+ }
644
+ }
645
+
646
+ return null;
647
+ }
648
+
649
+ /**
650
+ * Convert degrees to radians
651
+ * @private
652
+ */
653
+ #degToRad(degrees)
654
+ {
655
+ return (degrees / 180) * Math.PI;
656
+ }
657
+
658
+ /**
659
+ * Calculate the angle (in degrees) that a segment should occupy
660
+ * @private
661
+ */
662
+ #getSegmentAngleDeg(segmentValue)
663
+ {
664
+ return (segmentValue / this.#totalValue) * this.#totalAngleDeg;
665
+ }
666
+
667
+ /**
668
+ * Find the middle angle of a segment given its start angle and size
669
+ * @private
670
+ */
671
+ #getMidAngleDeg(startAngleDeg, segmentAngleDeg)
672
+ {
673
+ return startAngleDeg + segmentAngleDeg / 2;
674
+ }
675
+
676
+ /**
677
+ * Convert CSS conic-gradient angle (0° = top, clockwise) to standard math angle
678
+ * Math uses 0° = right, counterclockwise, so we need to adjust
679
+ * @private
680
+ */
681
+ #cssNormalize(cssAngleDeg)
682
+ {
683
+ return cssAngleDeg - 90;
684
+ }
685
+
686
+ #dispose()
687
+ {
688
+ // Clean up references
689
+ this.#resizeObserver?.disconnect();
690
+ this.#resizeObserver = null;
691
+ this.#chart = null;
692
+ this.#segmentOverlay = null;
693
+ this.#legend = null;
694
+ this.#segmentAngleRanges = [];
695
+ this.#segmentLabels = [];
696
+ this.#currentHoveredIndex = null;
697
+ this.#tooltipManager = null;
698
+ this.ownerDocument.removeEventListener('pointerdown', this.#outsidePointerDown);
699
+ this.#outsidePointerDown = null;
700
+ }
701
+ }
702
+
703
+
704
+
705
+ /**
706
+ * Represents a chart segment.
707
+ * @class
708
+ * @memberof componyx.UI.PieChart
709
+ */
710
+ class Segment
711
+ {
712
+ /**
713
+ * @param {Object} [properties] The properties used to initialize the object.
714
+ * @param {string} [properties.id] - Gets or sets the id of the segment.
715
+ * @param {string} [properties.label] - Gets or sets the label of the segment
716
+ * @param {string} [properties.tooltip] - Gets or sets the tooltip of the segment
717
+ * @param {any} [properties.color] - Gets or sets the CSS color value of the segment.
718
+ * @param {any} [properties.value] - Gets or sets the value of the segment.
719
+ * @param {any} [properties.valueSuffix] - Gets or sets the value suffix (e.g. %) of the segment.
720
+ */
721
+ constructor(properties)
722
+ {
723
+ /**
724
+ * Gets or sets the id of the segment.
725
+ * @type {String}
726
+ */
727
+ this.id = null;
728
+
729
+ /**
730
+ * Gets or sets the label of the segment
731
+ * @type {String}
732
+ */
733
+ this.label = null;
734
+
735
+ /**
736
+ * Gets or sets the tooltip of the segment
737
+ * @type {String}
738
+ */
739
+ this.tooltip = null;
740
+
741
+ /**
742
+ * Gets or sets the CSS color value of the segment.
743
+ * @type {String}
744
+ */
745
+ this.color = null;
746
+
747
+ /**
748
+ * Gets or sets the optional CSS end color value of the segment.
749
+ * @type {String}
750
+ */
751
+ this.endColor = null;
752
+
753
+ /**
754
+ * Gets or sets the value of the segment.
755
+ * @type {Number}
756
+ */
757
+ this.value = null;
758
+
759
+ /**
760
+ * Gets or sets the value suffix (e.g. %) of the segment.
761
+ * @type {Number}
762
+ */
763
+ this.valueSuffix = '';
764
+
765
+ _clone(this, properties);
766
+ }
767
+ }
768
+
769
+ PieChart.Segment = Segment;
770
+
771
+ /**
772
+ * ShapeOption
773
+ * @readonly
774
+ * @enum {string}
775
+ */
776
+ PieChart.ShapeOption =
777
+ {
778
+ PIE: 'pie',
779
+ DONUT: 'donut'
780
+ };
781
+
782
+ /**
783
+ * SegmentLabelDisplayOption
784
+ * @readonly
785
+ * @enum {string}
786
+ */
787
+ PieChart.SegmentLabelDisplayOption =
788
+ {
789
+ NONE: 'none',
790
+ INNER: 'inner',
791
+ OUTER: 'outer'
792
+ };
793
+
794
+ /**
795
+ * SegmentLabelContentOption
796
+ * @readonly
797
+ * @enum {string}
798
+ */
799
+ PieChart.SegmentLabelContentOption =
800
+ {
801
+ VALUE: 'value',
802
+ LABEL: 'label',
803
+ LABEL_VALUE: 'label_value'
804
+ };
805
+
806
+
807
+ // Preserve HTMLElement prototype and extend it with $base.methods
808
+ Object.assign(PieChart.prototype, Object.fromEntries(Object.entries($base.methods).filter(([key]) => !['render', 'postRender', 'destroy', 'getCssClass'].includes(key))));
809
+
810
+ // Restore the constructor reference
811
+ PieChart.prototype.constructor = PieChart;
812
+ componyx.UI.PieChart = PieChart;
813
+
814
+ // Define the custom element
815
+ customElements.define(`${componyx.UI.tagPrefix}${PieChart.name.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')}`, PieChart); // cui-pie-chart
816
+ })();
817
+
818
+ const PieChart = componyx.UI.PieChart;
819
+
820
+ export { PieChart };