supervisely 6.73.410__py3-none-any.whl → 6.73.470__py3-none-any.whl

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.

Potentially problematic release.


This version of supervisely might be problematic. Click here for more details.

Files changed (190) hide show
  1. supervisely/__init__.py +136 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/json_geometries_map.py +2 -0
  4. supervisely/annotation/label.py +80 -3
  5. supervisely/api/annotation_api.py +9 -9
  6. supervisely/api/api.py +67 -43
  7. supervisely/api/app_api.py +72 -5
  8. supervisely/api/dataset_api.py +108 -33
  9. supervisely/api/entity_annotation/figure_api.py +113 -49
  10. supervisely/api/image_api.py +82 -0
  11. supervisely/api/module_api.py +10 -0
  12. supervisely/api/nn/deploy_api.py +15 -9
  13. supervisely/api/nn/ecosystem_models_api.py +201 -0
  14. supervisely/api/nn/neural_network_api.py +12 -3
  15. supervisely/api/pointcloud/pointcloud_api.py +38 -0
  16. supervisely/api/pointcloud/pointcloud_episode_annotation_api.py +3 -0
  17. supervisely/api/project_api.py +213 -6
  18. supervisely/api/task_api.py +11 -1
  19. supervisely/api/video/video_annotation_api.py +4 -2
  20. supervisely/api/video/video_api.py +79 -1
  21. supervisely/api/video/video_figure_api.py +24 -11
  22. supervisely/api/volume/volume_api.py +38 -0
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +14 -6
  25. supervisely/app/fastapi/__init__.py +1 -0
  26. supervisely/app/fastapi/custom_static_files.py +1 -1
  27. supervisely/app/fastapi/multi_user.py +88 -0
  28. supervisely/app/fastapi/subapp.py +175 -42
  29. supervisely/app/fastapi/templating.py +1 -1
  30. supervisely/app/fastapi/websocket.py +77 -9
  31. supervisely/app/singleton.py +21 -0
  32. supervisely/app/v1/app_service.py +18 -2
  33. supervisely/app/v1/constants.py +7 -1
  34. supervisely/app/widgets/__init__.py +11 -1
  35. supervisely/app/widgets/agent_selector/template.html +1 -0
  36. supervisely/app/widgets/card/card.py +20 -0
  37. supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
  38. supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
  39. supervisely/app/widgets/deploy_model/deploy_model.py +750 -0
  40. supervisely/app/widgets/dialog/dialog.py +12 -0
  41. supervisely/app/widgets/dialog/template.html +2 -1
  42. supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
  43. supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
  44. supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
  45. supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
  46. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +195 -0
  47. supervisely/app/widgets/experiment_selector/experiment_selector.py +454 -263
  48. supervisely/app/widgets/fast_table/fast_table.py +713 -126
  49. supervisely/app/widgets/fast_table/script.js +492 -95
  50. supervisely/app/widgets/fast_table/style.css +54 -0
  51. supervisely/app/widgets/fast_table/template.html +45 -5
  52. supervisely/app/widgets/heatmap/__init__.py +0 -0
  53. supervisely/app/widgets/heatmap/heatmap.py +523 -0
  54. supervisely/app/widgets/heatmap/script.js +378 -0
  55. supervisely/app/widgets/heatmap/style.css +227 -0
  56. supervisely/app/widgets/heatmap/template.html +21 -0
  57. supervisely/app/widgets/input_tag/input_tag.py +102 -15
  58. supervisely/app/widgets/input_tag_list/__init__.py +0 -0
  59. supervisely/app/widgets/input_tag_list/input_tag_list.py +274 -0
  60. supervisely/app/widgets/input_tag_list/template.html +70 -0
  61. supervisely/app/widgets/radio_table/radio_table.py +10 -2
  62. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  63. supervisely/app/widgets/radio_tabs/template.html +1 -0
  64. supervisely/app/widgets/select/select.py +6 -4
  65. supervisely/app/widgets/select_dataset/select_dataset.py +6 -0
  66. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +83 -7
  67. supervisely/app/widgets/table/table.py +68 -13
  68. supervisely/app/widgets/tabs/tabs.py +22 -6
  69. supervisely/app/widgets/tabs/template.html +5 -1
  70. supervisely/app/widgets/transfer/style.css +3 -0
  71. supervisely/app/widgets/transfer/template.html +3 -1
  72. supervisely/app/widgets/transfer/transfer.py +48 -45
  73. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  74. supervisely/convert/image/csv/csv_converter.py +24 -15
  75. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +43 -41
  76. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +75 -51
  77. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +137 -124
  78. supervisely/convert/video/video_converter.py +2 -2
  79. supervisely/geometry/polyline_3d.py +110 -0
  80. supervisely/io/env.py +161 -1
  81. supervisely/nn/artifacts/__init__.py +1 -1
  82. supervisely/nn/artifacts/artifacts.py +10 -2
  83. supervisely/nn/artifacts/detectron2.py +1 -0
  84. supervisely/nn/artifacts/hrda.py +1 -0
  85. supervisely/nn/artifacts/mmclassification.py +20 -0
  86. supervisely/nn/artifacts/mmdetection.py +5 -3
  87. supervisely/nn/artifacts/mmsegmentation.py +1 -0
  88. supervisely/nn/artifacts/ritm.py +1 -0
  89. supervisely/nn/artifacts/rtdetr.py +1 -0
  90. supervisely/nn/artifacts/unet.py +1 -0
  91. supervisely/nn/artifacts/utils.py +3 -0
  92. supervisely/nn/artifacts/yolov5.py +2 -0
  93. supervisely/nn/artifacts/yolov8.py +1 -0
  94. supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
  95. supervisely/nn/experiments.py +9 -0
  96. supervisely/nn/inference/cache.py +37 -17
  97. supervisely/nn/inference/gui/serving_gui_template.py +39 -13
  98. supervisely/nn/inference/inference.py +953 -211
  99. supervisely/nn/inference/inference_request.py +15 -8
  100. supervisely/nn/inference/instance_segmentation/instance_segmentation.py +1 -0
  101. supervisely/nn/inference/object_detection/object_detection.py +1 -0
  102. supervisely/nn/inference/predict_app/__init__.py +0 -0
  103. supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
  104. supervisely/nn/inference/predict_app/gui/classes_selector.py +160 -0
  105. supervisely/nn/inference/predict_app/gui/gui.py +915 -0
  106. supervisely/nn/inference/predict_app/gui/input_selector.py +344 -0
  107. supervisely/nn/inference/predict_app/gui/model_selector.py +77 -0
  108. supervisely/nn/inference/predict_app/gui/output_selector.py +179 -0
  109. supervisely/nn/inference/predict_app/gui/preview.py +93 -0
  110. supervisely/nn/inference/predict_app/gui/settings_selector.py +881 -0
  111. supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
  112. supervisely/nn/inference/predict_app/gui/utils.py +399 -0
  113. supervisely/nn/inference/predict_app/predict_app.py +176 -0
  114. supervisely/nn/inference/session.py +47 -39
  115. supervisely/nn/inference/tracking/bbox_tracking.py +5 -1
  116. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  117. supervisely/nn/inference/tracking/tracker_interface.py +4 -0
  118. supervisely/nn/inference/uploader.py +9 -5
  119. supervisely/nn/model/model_api.py +44 -22
  120. supervisely/nn/model/prediction.py +15 -1
  121. supervisely/nn/model/prediction_session.py +70 -14
  122. supervisely/nn/prediction_dto.py +7 -0
  123. supervisely/nn/tracker/__init__.py +6 -8
  124. supervisely/nn/tracker/base_tracker.py +54 -0
  125. supervisely/nn/tracker/botsort/__init__.py +1 -0
  126. supervisely/nn/tracker/botsort/botsort_config.yaml +30 -0
  127. supervisely/nn/tracker/botsort/osnet_reid/__init__.py +0 -0
  128. supervisely/nn/tracker/botsort/osnet_reid/osnet.py +566 -0
  129. supervisely/nn/tracker/botsort/osnet_reid/osnet_reid_interface.py +88 -0
  130. supervisely/nn/tracker/botsort/tracker/__init__.py +0 -0
  131. supervisely/nn/tracker/{bot_sort → botsort/tracker}/basetrack.py +1 -2
  132. supervisely/nn/tracker/{utils → botsort/tracker}/gmc.py +51 -59
  133. supervisely/nn/tracker/{deep_sort/deep_sort → botsort/tracker}/kalman_filter.py +71 -33
  134. supervisely/nn/tracker/botsort/tracker/matching.py +202 -0
  135. supervisely/nn/tracker/{bot_sort/bot_sort.py → botsort/tracker/mc_bot_sort.py} +68 -81
  136. supervisely/nn/tracker/botsort_tracker.py +273 -0
  137. supervisely/nn/tracker/calculate_metrics.py +264 -0
  138. supervisely/nn/tracker/utils.py +273 -0
  139. supervisely/nn/tracker/visualize.py +520 -0
  140. supervisely/nn/training/gui/gui.py +152 -49
  141. supervisely/nn/training/gui/hyperparameters_selector.py +1 -1
  142. supervisely/nn/training/gui/model_selector.py +8 -6
  143. supervisely/nn/training/gui/train_val_splits_selector.py +144 -71
  144. supervisely/nn/training/gui/training_artifacts.py +3 -1
  145. supervisely/nn/training/train_app.py +225 -46
  146. supervisely/project/pointcloud_episode_project.py +12 -8
  147. supervisely/project/pointcloud_project.py +12 -8
  148. supervisely/project/project.py +221 -75
  149. supervisely/template/experiment/experiment.html.jinja +105 -55
  150. supervisely/template/experiment/experiment_generator.py +258 -112
  151. supervisely/template/experiment/header.html.jinja +31 -13
  152. supervisely/template/experiment/sly-style.css +7 -2
  153. supervisely/versions.json +3 -1
  154. supervisely/video/sampling.py +42 -20
  155. supervisely/video/video.py +41 -12
  156. supervisely/video_annotation/video_figure.py +38 -4
  157. supervisely/volume/stl_converter.py +2 -0
  158. supervisely/worker_api/agent_rpc.py +24 -1
  159. supervisely/worker_api/rpc_servicer.py +31 -7
  160. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/METADATA +22 -14
  161. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/RECORD +167 -148
  162. supervisely_lib/__init__.py +6 -1
  163. supervisely/app/widgets/experiment_selector/style.css +0 -27
  164. supervisely/app/widgets/experiment_selector/template.html +0 -61
  165. supervisely/nn/tracker/bot_sort/__init__.py +0 -21
  166. supervisely/nn/tracker/bot_sort/fast_reid_interface.py +0 -152
  167. supervisely/nn/tracker/bot_sort/matching.py +0 -127
  168. supervisely/nn/tracker/bot_sort/sly_tracker.py +0 -401
  169. supervisely/nn/tracker/deep_sort/__init__.py +0 -6
  170. supervisely/nn/tracker/deep_sort/deep_sort/__init__.py +0 -1
  171. supervisely/nn/tracker/deep_sort/deep_sort/detection.py +0 -49
  172. supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py +0 -81
  173. supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py +0 -202
  174. supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py +0 -176
  175. supervisely/nn/tracker/deep_sort/deep_sort/track.py +0 -166
  176. supervisely/nn/tracker/deep_sort/deep_sort/tracker.py +0 -145
  177. supervisely/nn/tracker/deep_sort/deep_sort.py +0 -301
  178. supervisely/nn/tracker/deep_sort/generate_clip_detections.py +0 -90
  179. supervisely/nn/tracker/deep_sort/preprocessing.py +0 -70
  180. supervisely/nn/tracker/deep_sort/sly_tracker.py +0 -273
  181. supervisely/nn/tracker/tracker.py +0 -285
  182. supervisely/nn/tracker/utils/kalman_filter.py +0 -492
  183. supervisely/nn/tracking/__init__.py +0 -1
  184. supervisely/nn/tracking/boxmot.py +0 -114
  185. supervisely/nn/tracking/tracking.py +0 -24
  186. /supervisely/{nn/tracker/utils → app/widgets/deploy_model}/__init__.py +0 -0
  187. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
  188. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
  189. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
  190. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,378 @@
1
+ Vue.component('heatmap-image', {
2
+ template: `
3
+ <div
4
+ class="heatmap-container"
5
+ :style="widthStyle"
6
+ @click="$emit('click')"
7
+ >
8
+ <div class="heatmap-header">
9
+ <div class="opacity-slider" @click.stop>
10
+ <div class="opacity-label">
11
+ <span class="opacity-label-text">Opacity:</span>
12
+ <span class="opacity-value">{{opacity}}%</span>
13
+ </div>
14
+
15
+ <el-slider
16
+ type="range"
17
+ :min="0"
18
+ :max="100"
19
+ :step="1"
20
+ :value="opacity"
21
+ @input="$emit('update:opacity', $event)"
22
+ class="slider"
23
+ >
24
+ </div>
25
+
26
+ <div class="legend" @click.stop>
27
+ <span class="legend-label legend-min">{{ formatValue(minValue) }}</span>
28
+ <div class="legend-gradient" :style="{ background: gradientStyle }"></div>
29
+ <span class="legend-label legend-max">{{ formatValue(maxValue) }}</span>
30
+ </div>
31
+ </div>
32
+
33
+ <div
34
+ class="image-wrapper"
35
+ ref="wrapper"
36
+ :style="imageWrapperStyle"
37
+ @click.stop="handleImageClick"
38
+ @mouseleave="handleMouseLeave"
39
+ >
40
+ <img
41
+ class="base-image"
42
+ :src="backgroundUrl"
43
+ @load="handleImageLoad"
44
+ draggable="false"
45
+ >
46
+ <img
47
+ class="overlay-image"
48
+ :style="{ opacity: opacity / 100 }"
49
+ :src="maskUrl"
50
+ draggable="false"
51
+ >
52
+
53
+ <div
54
+ v-if="clickedValue !== null"
55
+ class="click-indicator"
56
+ :class="{ 'hiding': isHiding }"
57
+ :style="indicatorStyle"
58
+ >
59
+ <div class="click-dot"></div>
60
+ </div>
61
+
62
+ <div
63
+ v-if="clickedValue !== null"
64
+ class="value-popup"
65
+ :class="['popup-position-' + popupPosition, { 'hiding': isHiding }]"
66
+ :style="popupStyle"
67
+ >
68
+ <div class="value-popup-content">
69
+ <span class="value-popup-value">{{ formatValue(clickedValue) }}</span>
70
+ </div>
71
+ <div class="value-popup-arrow"></div>
72
+ </div>
73
+
74
+ </div>
75
+ </div>
76
+ `,
77
+ data() {
78
+ return {
79
+ naturalWidth: null,
80
+ naturalHeight: null,
81
+ clickX: 0,
82
+ clickY: 0,
83
+ popupX: 0,
84
+ popupY: 0,
85
+ popupPosition: 'top', // 'top', 'bottom', 'right', 'left'
86
+ isHiding: false, // Flag for fade-out animation
87
+ };
88
+ },
89
+ computed: {
90
+ gradientStyle() {
91
+ return `linear-gradient(to right, ${this.legendColors.join(', ')})`;
92
+ },
93
+ widthStyle() {
94
+ const styles = {};
95
+
96
+ if (this.width) {
97
+ styles.width = typeof this.width === 'number' ? `${this.width}px` : this.width;
98
+ } else if (this.height) {
99
+ // Use naturalWidth/Height from loaded image, or fallback to maskWidth/Height
100
+ const effectiveWidth = this.naturalWidth || this.maskWidth;
101
+ const effectiveHeight = this.naturalHeight || this.maskHeight;
102
+
103
+ if (effectiveWidth && effectiveHeight) {
104
+ const heightValue = typeof this.height === 'number' ? this.height : parseFloat(this.height);
105
+ const aspectRatio = effectiveWidth / effectiveHeight;
106
+ styles.width = `${heightValue * aspectRatio}px`;
107
+ }
108
+ }
109
+
110
+ return styles;
111
+ },
112
+ imageWrapperStyle() {
113
+ const styles = { ...this.widthStyle };
114
+
115
+ // Use max-height instead of height to allow responsive scaling
116
+ if (this.height) {
117
+ styles.maxHeight = typeof this.height === 'number' ? `${this.height}px` : this.height;
118
+ }
119
+
120
+ // Use naturalWidth/Height from loaded image, or fallback to maskWidth/Height
121
+ const effectiveWidth = this.naturalWidth || this.maskWidth;
122
+ const effectiveHeight = this.naturalHeight || this.maskHeight;
123
+
124
+ // Always use aspect-ratio if we have dimensions
125
+ if (effectiveWidth && effectiveHeight) {
126
+ styles.aspectRatio = `${effectiveWidth} / ${effectiveHeight}`;
127
+ }
128
+
129
+ return styles;
130
+ },
131
+ indicatorStyle() {
132
+ return {
133
+ left: `${this.clickX}px`,
134
+ top: `${this.clickY}px`
135
+ };
136
+ },
137
+ popupStyle() {
138
+ const baseStyle = {
139
+ left: `${this.popupX}px`,
140
+ top: `${this.popupY}px`
141
+ };
142
+
143
+ // Adjust transform based on position
144
+ switch (this.popupPosition) {
145
+ case 'top':
146
+ baseStyle.transform = 'translate(-50%, calc(-100% - 16px))';
147
+ break;
148
+ case 'bottom':
149
+ baseStyle.transform = 'translate(-50%, 16px)';
150
+ break;
151
+ case 'right':
152
+ baseStyle.transform = 'translate(16px, -50%)';
153
+ break;
154
+ case 'left':
155
+ baseStyle.transform = 'translate(calc(-100% - 16px), -50%)';
156
+ break;
157
+ }
158
+
159
+ return baseStyle;
160
+ },
161
+ },
162
+ methods: {
163
+ handleImageLoad(event) {
164
+ this.naturalWidth = event.target.naturalWidth;
165
+ this.naturalHeight = event.target.naturalHeight;
166
+ },
167
+ handleImageClick(event) {
168
+ const wrapper = this.$refs.wrapper;
169
+ if (!wrapper) {
170
+ console.warn('[Heatmap] Wrapper not found', { maskUrl: this.maskUrl, backgroundUrl: this.backgroundUrl });
171
+ return;
172
+ }
173
+
174
+ // Get image element first to calculate position relative to actual image
175
+ const imgEl = wrapper.querySelector('.overlay-image');
176
+ if (!imgEl) {
177
+ console.warn('[Heatmap] Overlay image element not found', { maskUrl: this.maskUrl, backgroundUrl: this.backgroundUrl });
178
+ return;
179
+ }
180
+
181
+ const imgRect = imgEl.getBoundingClientRect();
182
+
183
+ // Get click position relative to actual image (not wrapper!)
184
+ const relativeX = event.clientX - imgRect.left;
185
+ const relativeY = event.clientY - imgRect.top;
186
+
187
+ // Check if click is within image bounds with small tolerance for edge cases
188
+ const tolerance = 1; // 1px tolerance for edge clicks with browser zoom
189
+ if (relativeX < -tolerance || relativeY < -tolerance ||
190
+ relativeX > imgRect.width + tolerance || relativeY > imgRect.height + tolerance) {
191
+ return;
192
+ }
193
+
194
+ // Clamp coordinates to image bounds (handle edge cases from browser zoom)
195
+ const clampedX = Math.max(0, Math.min(relativeX, imgRect.width - 0.01));
196
+ const clampedY = Math.max(0, Math.min(relativeY, imgRect.height - 0.01));
197
+
198
+ // Set visual indicator position (relative to image, not wrapper)
199
+ this.clickX = clampedX;
200
+ this.clickY = clampedY;
201
+ this.popupX = clampedX;
202
+ this.popupY = clampedY;
203
+
204
+ // Determine best popup position based on click location
205
+ const popupHeight = 40; // Approximate popup height
206
+ const popupWidth = 80; // Approximate popup width (half width for centered popup)
207
+ const margin = 20; // Minimum margin from edges
208
+
209
+ // Check available space in each direction (relative to image, not wrapper)
210
+ const spaceTop = clampedY;
211
+ const spaceBottom = imgRect.height - clampedY;
212
+ const spaceLeft = clampedX;
213
+ const spaceRight = imgRect.width - clampedX;
214
+
215
+ // Check if popup would overflow horizontally when positioned top/bottom
216
+ const wouldOverflowLeft = clampedX < (popupWidth / 2);
217
+ const wouldOverflowRight = (imgRect.width - clampedX) < (popupWidth / 2);
218
+
219
+ // Logic: prefer top, but if at edges use left/right
220
+ if (spaceTop > popupHeight + margin && !wouldOverflowLeft && !wouldOverflowRight) {
221
+ // Enough space on top and won't overflow horizontally
222
+ this.popupPosition = 'top';
223
+ }
224
+ else if (wouldOverflowRight && spaceLeft > popupWidth + margin) {
225
+ // Point is at right edge, show popup on left
226
+ this.popupPosition = 'left';
227
+ }
228
+ else if (wouldOverflowLeft && spaceRight > popupWidth + margin) {
229
+ // Point is at left edge, show popup on right
230
+ this.popupPosition = 'right';
231
+ }
232
+ else if (spaceTop > popupHeight + margin) {
233
+ // Use top even if might slightly overflow (better than nothing)
234
+ this.popupPosition = 'top';
235
+ }
236
+ else if (spaceBottom > popupHeight + margin && !wouldOverflowLeft && !wouldOverflowRight) {
237
+ // If no space on top, show popup below (if won't overflow)
238
+ this.popupPosition = 'bottom';
239
+ }
240
+ else if (spaceRight > popupWidth + margin) {
241
+ // Show on right if there's space
242
+ this.popupPosition = 'right';
243
+ }
244
+ else if (spaceLeft > popupWidth + margin) {
245
+ // Show on left if there's space
246
+ this.popupPosition = 'left';
247
+ }
248
+ else if (spaceBottom > popupHeight + margin) {
249
+ // Fallback to bottom even if might overflow
250
+ this.popupPosition = 'bottom';
251
+ }
252
+ else {
253
+ // Final fallback: top
254
+ this.popupPosition = 'top';
255
+ }
256
+
257
+ // Use mask dimensions from server
258
+ const maskWidth = this.maskWidth;
259
+ const maskHeight = this.maskHeight;
260
+
261
+ if (!maskWidth || !maskHeight) {
262
+ console.warn('[Heatmap] Mask dimensions not available', {
263
+ maskUrl: this.maskUrl,
264
+ maskWidth: this.maskWidth,
265
+ maskHeight: this.maskHeight
266
+ });
267
+ return;
268
+ }
269
+
270
+ // Get PNG file dimensions (naturalWidth/Height of the loaded image)
271
+ const pngWidth = imgEl.naturalWidth;
272
+ const pngHeight = imgEl.naturalHeight;
273
+
274
+ if (!pngWidth || !pngHeight) {
275
+ console.warn('[Heatmap] PNG dimensions not available', {
276
+ maskUrl: this.maskUrl,
277
+ naturalWidth: imgEl.naturalWidth,
278
+ naturalHeight: imgEl.naturalHeight
279
+ });
280
+ return;
281
+ }
282
+
283
+ // Two-step coordinate transformation:
284
+ // 1. From screen coordinates to PNG coordinates
285
+ // 2. From PNG coordinates to mask coordinates
286
+
287
+ // Step 1: Scale from displayed size to PNG size
288
+ const displayToImageScaleX = pngWidth / imgRect.width;
289
+ const displayToImageScaleY = pngHeight / imgRect.height;
290
+
291
+ const pngX = clampedX * displayToImageScaleX;
292
+ const pngY = clampedY * displayToImageScaleY;
293
+
294
+ // Step 2: Scale from PNG size to mask size
295
+ const imageTomaskScaleX = maskWidth / pngWidth;
296
+ const imageTomaskScaleY = maskHeight / pngHeight;
297
+
298
+ let maskX = Math.floor(pngX * imageTomaskScaleX);
299
+ let maskY = Math.floor(pngY * imageTomaskScaleY);
300
+
301
+ // Clamp to mask bounds
302
+ maskX = Math.min(Math.max(maskX, 0), maskWidth - 1);
303
+ maskY = Math.min(Math.max(maskY, 0), maskHeight - 1);
304
+
305
+ // Update state - this will trigger server-side callback
306
+ this.$emit('update:mask-x', maskX);
307
+ this.$emit('update:mask-y', maskY);
308
+
309
+ // Reset hiding state for new click
310
+ this.isHiding = false;
311
+
312
+ // Don't set clicked-value here - server will set it after getting value from mask
313
+ this.$emit('update:clicked-value', null);
314
+
315
+ // Call server callback after Vue updates state
316
+ if (this.onImageClick) {
317
+ this.$nextTick(() => {
318
+ this.onImageClick();
319
+ });
320
+ }
321
+ },
322
+ handleMouseLeave() {
323
+ if (this.clickedValue === null) return;
324
+
325
+ this.isHiding = true;
326
+
327
+ setTimeout(() => {
328
+ this.$emit('update:clicked-value', null);
329
+ this.$emit('update:mask-x', null);
330
+ this.$emit('update:mask-y', null);
331
+ this.isHiding = false;
332
+ }, 300);
333
+ },
334
+ formatValue(value) {
335
+ if (value === null || value === undefined) return 'N/A';
336
+ if (Number.isInteger(value)) {
337
+ return value.toString();
338
+ }
339
+ const abs = Math.abs(value);
340
+ let decimals;
341
+ if (abs >= 1000) decimals = 1;
342
+ else if (abs >= 100) decimals = 2;
343
+ else if (abs >= 1) decimals = 3;
344
+ else if (abs >= 0.01) decimals = 4;
345
+ else decimals = 5;
346
+
347
+ return parseFloat(value.toFixed(decimals)).toString();
348
+ }
349
+ },
350
+ watch: {
351
+ backgroundUrl() {
352
+ this.naturalWidth = null;
353
+ this.naturalHeight = null;
354
+ }
355
+ },
356
+ props: {
357
+ backgroundUrl: String,
358
+ maskUrl: String,
359
+ opacity: Number,
360
+ width: [Number, String],
361
+ height: [Number, String],
362
+ maskWidth: Number,
363
+ maskHeight: Number,
364
+ legendColors: {
365
+ type: Array,
366
+ default: () => ['#0000FF', '#00FF00', '#FFFF00', '#FF0000']
367
+ },
368
+ minValue: Number,
369
+ maxValue: Number,
370
+ clickedValue: Number,
371
+ maskX: Number,
372
+ maskY: Number,
373
+ onImageClick: {
374
+ type: Function,
375
+ default: null
376
+ }
377
+ },
378
+ });
@@ -0,0 +1,227 @@
1
+ .heatmap-container {
2
+ max-width: 100%;
3
+ box-sizing: border-box;
4
+ }
5
+
6
+ .image-wrapper {
7
+ position: relative;
8
+ max-width: 100%;
9
+ cursor: pointer;
10
+ overflow: visible; /* Allow popup to overflow */
11
+ box-sizing: border-box;
12
+ transition: height 0.2s ease;
13
+ }
14
+
15
+ .image-wrapper img {
16
+ width: 100%;
17
+ height: auto;
18
+ display: block;
19
+ object-fit: contain;
20
+ object-position: center;
21
+ -webkit-user-drag: none;
22
+ user-select: none;
23
+ -webkit-user-select: none;
24
+ -moz-user-select: none;
25
+ -ms-user-select: none;
26
+ pointer-events: auto;
27
+ }
28
+
29
+ .overlay-image {
30
+ position: absolute;
31
+ top: 0;
32
+ left: 0;
33
+ width: 100%;
34
+ height: 100%;
35
+ pointer-events: none;
36
+ object-fit: contain;
37
+ image-rendering: pixelated;
38
+ }
39
+
40
+ .heatmap-header {
41
+ display: flex;
42
+ justify-content: space-between;
43
+ padding-bottom: 8px;
44
+ }
45
+
46
+ .opacity-slider {
47
+ background: rgba(255, 255, 255);
48
+ border-radius: 8px;
49
+ padding: 10px 10px;
50
+ display: flex;
51
+ align-items: center;
52
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
53
+ flex-direction: column;
54
+ }
55
+
56
+ .opacity-slider .opacity-label {
57
+ display: flex;
58
+ justify-content: space-between;
59
+ width: 100%;
60
+ color: #353a44;
61
+ font-weight: 400;
62
+ font-size: 14px;
63
+ }
64
+
65
+ .opacity-slider .opacity-label .opacity-value{
66
+ align-self: flex-end;
67
+ font-size: 12px;
68
+ }
69
+
70
+ .opacity-slider .slider {
71
+ width: 145px;
72
+ }
73
+
74
+ .opacity-slider .slider .el-slider__runway {
75
+ margin: 8px 0;
76
+ }
77
+
78
+ .legend {
79
+ bottom: 8px;
80
+ background: rgba(255, 255, 255);
81
+ border-radius: 8px;
82
+ padding: 4px 8px;
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 6px;
86
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
87
+ }
88
+
89
+ .legend-gradient {
90
+ width: 120px;
91
+ height: 12px;
92
+ border-radius: 4px;
93
+ border: 1px solid rgba(0, 0, 0, 0.2);
94
+ }
95
+
96
+ .legend-label {
97
+ font-size: 0.75rem;
98
+ font-weight: 500;
99
+ color: #333;
100
+ white-space: nowrap;
101
+ }
102
+
103
+ .click-indicator {
104
+ position: absolute;
105
+ pointer-events: none;
106
+ z-index: 1001;
107
+ transform: translate(-50%, -50%);
108
+ animation: click-appear 0.3s ease;
109
+ transition: opacity 0.3s ease;
110
+ }
111
+
112
+ .click-indicator.hiding {
113
+ opacity: 0;
114
+ transition: opacity 0.3s ease;
115
+ }
116
+
117
+ @keyframes click-appear {
118
+ from {
119
+ transform: translate(-50%, -50%) scale(0);
120
+ }
121
+ to {
122
+ transform: translate(-50%, -50%) scale(1);
123
+ }
124
+ }
125
+
126
+ .click-dot {
127
+ width: 12px;
128
+ height: 12px;
129
+ background: white;
130
+ border: 2px solid #3b82f6;
131
+ border-radius: 50%;
132
+ box-sizing: border-box;
133
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(59, 130, 246, 0.2);
134
+ animation: pulse-ring 1.5s ease-out infinite;
135
+ transform: translate(1px, -1px);
136
+ }
137
+
138
+ @keyframes pulse-ring {
139
+ 0% {
140
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(59, 130, 246, 0.4);
141
+ }
142
+ 50% {
143
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 0 0 6px rgba(59, 130, 246, 0.1);
144
+ }
145
+ 100% {
146
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(59, 130, 246, 0.4);
147
+ }
148
+ }
149
+
150
+ .value-popup {
151
+ position: absolute;
152
+ background: rgba(255, 255, 255, 0.95);
153
+ color: #333;
154
+ padding: 6px 12px;
155
+ border-radius: 6px;
156
+ pointer-events: none;
157
+ z-index: 1002;
158
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
159
+ white-space: nowrap;
160
+ animation: popup-fade-in 0.2s ease;
161
+ transition: opacity 0.3s ease;
162
+ }
163
+
164
+ .value-popup.hiding {
165
+ opacity: 0;
166
+ }
167
+
168
+ @keyframes popup-fade-in {
169
+ from {
170
+ opacity: 0;
171
+ }
172
+ to {
173
+ opacity: 1;
174
+ }
175
+ }
176
+
177
+ .value-popup-content {
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ z-index: 9999;
182
+ }
183
+
184
+ .value-popup-value {
185
+ font-size: 14px;
186
+ font-weight: 600;
187
+ }
188
+
189
+ .value-popup-arrow {
190
+ position: absolute;
191
+ width: 8px;
192
+ height: 8px;
193
+ background: rgba(255, 255, 255, 0.95);
194
+ }
195
+
196
+ /* Arrow positions and rotations for different popup positions */
197
+ .popup-position-top .value-popup-arrow {
198
+ bottom: -4px;
199
+ left: 50%;
200
+ margin-left: -4px;
201
+ transform: rotate(45deg);
202
+ box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.1);
203
+ }
204
+
205
+ .popup-position-bottom .value-popup-arrow {
206
+ top: -4px;
207
+ left: 50%;
208
+ margin-left: -4px;
209
+ transform: rotate(45deg);
210
+ box-shadow: -2px -2px 3px rgba(0, 0, 0, 0.1);
211
+ }
212
+
213
+ .popup-position-right .value-popup-arrow {
214
+ left: -4px;
215
+ top: 50%;
216
+ margin-top: -4px;
217
+ transform: rotate(45deg);
218
+ box-shadow: -2px 2px 3px rgba(0, 0, 0, 0.1);
219
+ }
220
+
221
+ .popup-position-left .value-popup-arrow {
222
+ right: -4px;
223
+ top: 50%;
224
+ margin-top: -4px;
225
+ transform: rotate(45deg);
226
+ box-shadow: 2px -2px 3px rgba(0, 0, 0, 0.1);
227
+ }
@@ -0,0 +1,21 @@
1
+ <link rel="stylesheet" href="./sly/css/app/widgets/heatmap/style.css" />
2
+ <heatmap-image
3
+ :background-url="data.{{{widget.widget_id}}}.backgroundUrl"
4
+ :mask-url="data.{{{widget.widget_id}}}.heatmapUrl"
5
+ :opacity.sync="state.{{{widget.widget_id}}}.opacity"
6
+ :width="data.{{{widget.widget_id}}}.width"
7
+ :height="data.{{{widget.widget_id}}}.height"
8
+ :mask-width="data.{{{widget.widget_id}}}.maskWidth"
9
+ :mask-height="data.{{{widget.widget_id}}}.maskHeight"
10
+ :min-value="data.{{{widget.widget_id}}}.minValue"
11
+ :max-value="data.{{{widget.widget_id}}}.maxValue"
12
+ :legend-colors="data.{{{widget.widget_id}}}.legendColors"
13
+ :clicked-value="state.{{{widget.widget_id}}}.clickedValue"
14
+ :mask-x="state.{{{widget.widget_id}}}.maskX"
15
+ :mask-y="state.{{{widget.widget_id}}}.maskY"
16
+ @update:clicked-value="state.{{{widget.widget_id}}}.clickedValue = $event"
17
+ @update:mask-x="state.{{{widget.widget_id}}}.maskX = $event"
18
+ @update:mask-y="state.{{{widget.widget_id}}}.maskY = $event"
19
+ :on-image-click="() => {post('/{{{widget.widget_id}}}/heatmap_clicked_cb')}"
20
+ >
21
+ </heatmap-image>