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.
- supervisely/__init__.py +136 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/json_geometries_map.py +2 -0
- supervisely/annotation/label.py +80 -3
- supervisely/api/annotation_api.py +9 -9
- supervisely/api/api.py +67 -43
- supervisely/api/app_api.py +72 -5
- supervisely/api/dataset_api.py +108 -33
- supervisely/api/entity_annotation/figure_api.py +113 -49
- supervisely/api/image_api.py +82 -0
- supervisely/api/module_api.py +10 -0
- supervisely/api/nn/deploy_api.py +15 -9
- supervisely/api/nn/ecosystem_models_api.py +201 -0
- supervisely/api/nn/neural_network_api.py +12 -3
- supervisely/api/pointcloud/pointcloud_api.py +38 -0
- supervisely/api/pointcloud/pointcloud_episode_annotation_api.py +3 -0
- supervisely/api/project_api.py +213 -6
- supervisely/api/task_api.py +11 -1
- supervisely/api/video/video_annotation_api.py +4 -2
- supervisely/api/video/video_api.py +79 -1
- supervisely/api/video/video_figure_api.py +24 -11
- supervisely/api/volume/volume_api.py +38 -0
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +14 -6
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +88 -0
- supervisely/app/fastapi/subapp.py +175 -42
- supervisely/app/fastapi/templating.py +1 -1
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/__init__.py +11 -1
- supervisely/app/widgets/agent_selector/template.html +1 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
- supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
- supervisely/app/widgets/deploy_model/deploy_model.py +750 -0
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
- supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
- supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
- supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +195 -0
- supervisely/app/widgets/experiment_selector/experiment_selector.py +454 -263
- supervisely/app/widgets/fast_table/fast_table.py +713 -126
- supervisely/app/widgets/fast_table/script.js +492 -95
- supervisely/app/widgets/fast_table/style.css +54 -0
- supervisely/app/widgets/fast_table/template.html +45 -5
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +523 -0
- supervisely/app/widgets/heatmap/script.js +378 -0
- supervisely/app/widgets/heatmap/style.css +227 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/input_tag/input_tag.py +102 -15
- supervisely/app/widgets/input_tag_list/__init__.py +0 -0
- supervisely/app/widgets/input_tag_list/input_tag_list.py +274 -0
- supervisely/app/widgets/input_tag_list/template.html +70 -0
- supervisely/app/widgets/radio_table/radio_table.py +10 -2
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -4
- supervisely/app/widgets/select_dataset/select_dataset.py +6 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +83 -7
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tabs/tabs.py +22 -6
- supervisely/app/widgets/tabs/template.html +5 -1
- supervisely/app/widgets/transfer/style.css +3 -0
- supervisely/app/widgets/transfer/template.html +3 -1
- supervisely/app/widgets/transfer/transfer.py +48 -45
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +43 -41
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +75 -51
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +137 -124
- supervisely/convert/video/video_converter.py +2 -2
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/io/env.py +161 -1
- supervisely/nn/artifacts/__init__.py +1 -1
- supervisely/nn/artifacts/artifacts.py +10 -2
- supervisely/nn/artifacts/detectron2.py +1 -0
- supervisely/nn/artifacts/hrda.py +1 -0
- supervisely/nn/artifacts/mmclassification.py +20 -0
- supervisely/nn/artifacts/mmdetection.py +5 -3
- supervisely/nn/artifacts/mmsegmentation.py +1 -0
- supervisely/nn/artifacts/ritm.py +1 -0
- supervisely/nn/artifacts/rtdetr.py +1 -0
- supervisely/nn/artifacts/unet.py +1 -0
- supervisely/nn/artifacts/utils.py +3 -0
- supervisely/nn/artifacts/yolov5.py +2 -0
- supervisely/nn/artifacts/yolov8.py +1 -0
- supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
- supervisely/nn/experiments.py +9 -0
- supervisely/nn/inference/cache.py +37 -17
- supervisely/nn/inference/gui/serving_gui_template.py +39 -13
- supervisely/nn/inference/inference.py +953 -211
- supervisely/nn/inference/inference_request.py +15 -8
- supervisely/nn/inference/instance_segmentation/instance_segmentation.py +1 -0
- supervisely/nn/inference/object_detection/object_detection.py +1 -0
- supervisely/nn/inference/predict_app/__init__.py +0 -0
- supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
- supervisely/nn/inference/predict_app/gui/classes_selector.py +160 -0
- supervisely/nn/inference/predict_app/gui/gui.py +915 -0
- supervisely/nn/inference/predict_app/gui/input_selector.py +344 -0
- supervisely/nn/inference/predict_app/gui/model_selector.py +77 -0
- supervisely/nn/inference/predict_app/gui/output_selector.py +179 -0
- supervisely/nn/inference/predict_app/gui/preview.py +93 -0
- supervisely/nn/inference/predict_app/gui/settings_selector.py +881 -0
- supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
- supervisely/nn/inference/predict_app/gui/utils.py +399 -0
- supervisely/nn/inference/predict_app/predict_app.py +176 -0
- supervisely/nn/inference/session.py +47 -39
- supervisely/nn/inference/tracking/bbox_tracking.py +5 -1
- supervisely/nn/inference/tracking/point_tracking.py +5 -1
- supervisely/nn/inference/tracking/tracker_interface.py +4 -0
- supervisely/nn/inference/uploader.py +9 -5
- supervisely/nn/model/model_api.py +44 -22
- supervisely/nn/model/prediction.py +15 -1
- supervisely/nn/model/prediction_session.py +70 -14
- supervisely/nn/prediction_dto.py +7 -0
- supervisely/nn/tracker/__init__.py +6 -8
- supervisely/nn/tracker/base_tracker.py +54 -0
- supervisely/nn/tracker/botsort/__init__.py +1 -0
- supervisely/nn/tracker/botsort/botsort_config.yaml +30 -0
- supervisely/nn/tracker/botsort/osnet_reid/__init__.py +0 -0
- supervisely/nn/tracker/botsort/osnet_reid/osnet.py +566 -0
- supervisely/nn/tracker/botsort/osnet_reid/osnet_reid_interface.py +88 -0
- supervisely/nn/tracker/botsort/tracker/__init__.py +0 -0
- supervisely/nn/tracker/{bot_sort → botsort/tracker}/basetrack.py +1 -2
- supervisely/nn/tracker/{utils → botsort/tracker}/gmc.py +51 -59
- supervisely/nn/tracker/{deep_sort/deep_sort → botsort/tracker}/kalman_filter.py +71 -33
- supervisely/nn/tracker/botsort/tracker/matching.py +202 -0
- supervisely/nn/tracker/{bot_sort/bot_sort.py → botsort/tracker/mc_bot_sort.py} +68 -81
- supervisely/nn/tracker/botsort_tracker.py +273 -0
- supervisely/nn/tracker/calculate_metrics.py +264 -0
- supervisely/nn/tracker/utils.py +273 -0
- supervisely/nn/tracker/visualize.py +520 -0
- supervisely/nn/training/gui/gui.py +152 -49
- supervisely/nn/training/gui/hyperparameters_selector.py +1 -1
- supervisely/nn/training/gui/model_selector.py +8 -6
- supervisely/nn/training/gui/train_val_splits_selector.py +144 -71
- supervisely/nn/training/gui/training_artifacts.py +3 -1
- supervisely/nn/training/train_app.py +225 -46
- supervisely/project/pointcloud_episode_project.py +12 -8
- supervisely/project/pointcloud_project.py +12 -8
- supervisely/project/project.py +221 -75
- supervisely/template/experiment/experiment.html.jinja +105 -55
- supervisely/template/experiment/experiment_generator.py +258 -112
- supervisely/template/experiment/header.html.jinja +31 -13
- supervisely/template/experiment/sly-style.css +7 -2
- supervisely/versions.json +3 -1
- supervisely/video/sampling.py +42 -20
- supervisely/video/video.py +41 -12
- supervisely/video_annotation/video_figure.py +38 -4
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/METADATA +22 -14
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/RECORD +167 -148
- supervisely_lib/__init__.py +6 -1
- supervisely/app/widgets/experiment_selector/style.css +0 -27
- supervisely/app/widgets/experiment_selector/template.html +0 -61
- supervisely/nn/tracker/bot_sort/__init__.py +0 -21
- supervisely/nn/tracker/bot_sort/fast_reid_interface.py +0 -152
- supervisely/nn/tracker/bot_sort/matching.py +0 -127
- supervisely/nn/tracker/bot_sort/sly_tracker.py +0 -401
- supervisely/nn/tracker/deep_sort/__init__.py +0 -6
- supervisely/nn/tracker/deep_sort/deep_sort/__init__.py +0 -1
- supervisely/nn/tracker/deep_sort/deep_sort/detection.py +0 -49
- supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py +0 -81
- supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py +0 -202
- supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py +0 -176
- supervisely/nn/tracker/deep_sort/deep_sort/track.py +0 -166
- supervisely/nn/tracker/deep_sort/deep_sort/tracker.py +0 -145
- supervisely/nn/tracker/deep_sort/deep_sort.py +0 -301
- supervisely/nn/tracker/deep_sort/generate_clip_detections.py +0 -90
- supervisely/nn/tracker/deep_sort/preprocessing.py +0 -70
- supervisely/nn/tracker/deep_sort/sly_tracker.py +0 -273
- supervisely/nn/tracker/tracker.py +0 -285
- supervisely/nn/tracker/utils/kalman_filter.py +0 -492
- supervisely/nn/tracking/__init__.py +0 -1
- supervisely/nn/tracking/boxmot.py +0 -114
- supervisely/nn/tracking/tracking.py +0 -24
- /supervisely/{nn/tracker/utils → app/widgets/deploy_model}/__init__.py +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
- {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>
|