Skip to content

Commit 4409db7

Browse files
committed
add buttons to pick detector in the selected pixel, add possibility to copy full path to a detector
1 parent dc3f763 commit 4409db7

File tree

3 files changed

+306
-81
lines changed

3 files changed

+306
-81
lines changed
Lines changed: 195 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,199 @@
11
{% extends "browse/base_generic.html" %} {% block title %}Focal Plane
22
UI{%endblock %} {% block body %}
3-
<h1>Focal Plane UI</h1>
4-
<p>This page displays LiteBIRD Focal Plane interactive visualization tool.</p>
5-
6-
<div id="plotly-div"></div>
7-
8-
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
9-
10-
<body>
11-
<div id="myPlot" style="width: 100%; height: 100%"></div>
12-
13-
<script>
14-
document.addEventListener('DOMContentLoaded', function() {
15-
const xValues = {{ x_values|safe }};
16-
const yValues = {{ y_values|safe }};
17-
const uuids = {{ uuids|safe }};
18-
const channel = {{ channel|safe }};
19-
const size = {{ size|safe }};
20-
const pol = {{ pol|safe }};
21-
const colors = channel.map(val =>
22-
val.slice(0, 2) == "L1" ? 'blue' :
23-
val.slice(0, 2) == "L2" ? 'orange' :
24-
val.slice(0, 2) == "M1" ? 'red' :
25-
val.slice(0, 2) == "M2" ? 'black' :
26-
val.slice(0, 2) == "H1" ? 'purple' :
27-
val.slice(0, 2) == "H2" ? 'pink' :
28-
'gray'
29-
);
30-
const pols = pol.map(val =>
31-
val == 0 ? 'circle-cross':
32-
val == 90 ? 'circle-cross' :
33-
val == 45 ? 'circle-x' :
34-
val == 135 ? 'circle-x' :
35-
27
36-
);
37-
38-
39-
40-
const data = [
41-
{
42-
x: xValues,
43-
y: yValues,
44-
type: 'scatter',
45-
mode: 'markers',
46-
marker: { color: colors, symbol: pols, size: size},
47-
customdata: uuids,
48-
}
49-
];
50-
const layout = {
51-
xaxis: { title: 'X_sky' },
52-
yaxis: { title: 'Y_sky' },
53-
margin: { t: 8, b: 40, l: 40, r: 8 },
54-
autosize: true
55-
};
56-
57-
Plotly.newPlot('myPlot', data, layout);
58-
59-
const plotDiv = document.getElementById('myPlot');
60-
plotDiv.on('plotly_click', function(data) {
61-
62-
const point = data.points[0];
63-
if (point) {
64-
const this_uuid = point.customdata;
65-
66-
if (this_uuid) {
67-
const url = `http://127.0.0.1:8000/browse/data_files/${this_uuid}`;
68-
window.open(url, '_blank');
69-
}
3+
<!--<h1>Focal Plane UI</h1>
4+
<p>This page displays a LiteBIRD Focal Plane interactive visualization tool.</p>-->
5+
6+
<!-- Container for the two-column layout -->
7+
<div style="display: flex; width: 100%">
8+
<!-- Left Column -->
9+
<div
10+
style="
11+
flex: 1;
12+
padding: 10px;
13+
background-color: #f8f9fa;
14+
border: 1px solid #dee2e6;
15+
"
16+
>
17+
<!-- Container for the Plotly plot -->
18+
<div id="myPlot" style="width: 150%; height: 500px"></div>
19+
20+
<!-- Load Plotly.js from CDN -->
21+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
22+
23+
<!-- JavaScript to create the plot -->
24+
<script>
25+
document.addEventListener('DOMContentLoaded', function() {
26+
const xValues = {{ x_values|safe }};
27+
const yValues = {{ y_values|safe }};
28+
const uuids = {{ uuids|safe }};
29+
const channel = {{ channel|safe }};
30+
const size = {{ size|safe }};
31+
const pol = {{ pol|safe }};
32+
const colors = channel.map(val =>
33+
val.slice(0, 2) == "L1" ? 'blue' :
34+
val.slice(0, 2) == "L2" ? 'orange' :
35+
val.slice(0, 2) == "M1" ? 'red' :
36+
val.slice(0, 2) == "M2" ? 'black' :
37+
val.slice(0, 2) == "H1" ? 'purple' :
38+
val.slice(0, 2) == "H2" ? 'pink' :
39+
'gray'
40+
);
41+
const pols = pol.map(val =>
42+
val == 0 ? 'circle-cross':
43+
val == 90 ? 'circle-cross' :
44+
val == 45 ? 'circle-x' :
45+
val == 135 ? 'circle-x' :
46+
27
47+
);
48+
49+
50+
const data = [
51+
{
52+
x: xValues,
53+
y: yValues,
54+
type: 'scatter',
55+
mode: 'markers',
56+
marker: { color: colors, symbol: pols, size: size},
57+
customdata: uuids
58+
}
59+
];
60+
const layout = {
61+
xaxis: { title: 'X_sky' },
62+
yaxis: { title: 'Y_sky' },
63+
margin: { t: 8, b: 40, l: 40, r: 8 },
64+
autosize: true,
65+
};
66+
67+
const config = {
68+
69+
doubleClickDelay: 0,
70+
responsive: false
71+
};
72+
73+
Plotly.newPlot('myPlot', data, layout, config)
74+
75+
const plotDiv = document.getElementById('myPlot');
76+
77+
plotDiv.on('plotly_click', function(data) {
78+
const point = data.points[0];
79+
if (point) {
80+
const this_uuid = point.customdata;
81+
82+
if (this_uuid) {
83+
const url = new URL("{% url 'handle-plot-click' %}", window.location.origin);
84+
url.searchParams.append('uuid', this_uuid);
85+
86+
fetch(url, {
87+
headers:{
88+
'Accept': 'application/json',
89+
'X-Requested-With': 'XMLHttpRequest',
90+
},
91+
})
92+
.then(response => response.json())
93+
.then(responseData => {
94+
const paramsElement = document.getElementById('params');
95+
96+
// Store data globally
97+
window.detectorData = responseData;
98+
99+
// Create release selector and buttons
100+
let buttonsHTML = '<h3>Select a detector:</h3>';
101+
102+
// Add release selectbox with warning
103+
buttonsHTML += '<div style="margin-bottom: 15px; padding: 12px; background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px;">';
104+
buttonsHTML += '<label for="releaseSelect" style="margin-right: 10px; font-weight: bold;">Release (required to copy path):</label>';
105+
buttonsHTML += '<select id="releaseSelect" style="padding: 6px 10px; border-radius: 4px; border: 1px solid #ccc;">';
106+
buttonsHTML += '<option value="">-- Select a release --</option>';
107+
buttonsHTML += '<option value="IMo_vPostKDP2">v2.1 (PostKDP2 Fixed Feb 2026)</option>';
108+
buttonsHTML += '</select>';
109+
buttonsHTML += '</div>';
110+
111+
buttonsHTML += '<div style="display: flex; gap: 10px; flex-wrap: wrap;">';
112+
113+
Object.keys(responseData).forEach(uuid => {
114+
// Get the detector name from the metadata
115+
const metadata = responseData[uuid];
116+
const detectorName = metadata.name || metadata.detector || metadata.id || uuid.substring(0, 8);
117+
118+
buttonsHTML += `<div style="display: flex; gap: 5px; align-items: center;">
119+
<button onclick="openDetectorInfo('${uuid}')" style="padding: 8px 12px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 4px;">
120+
${detectorName}
121+
</button>
122+
<button onclick="copyToClipboard('${uuid}')" style="padding: 8px 10px; cursor: pointer; background-color: #6c757d; color: white; border: none; border-radius: 4px; font-size: 14px;" title="Copy UUID">
123+
📋
124+
</button>
125+
</div>`;
126+
});
127+
128+
129+
buttonsHTML += '</div>';
130+
buttonsHTML += '<div id="dataDisplay" style="margin-top: 20px;"></div>';
131+
132+
paramsElement.innerHTML = buttonsHTML;
133+
})
134+
135+
.catch(error => console.error('Error:', error))
136+
}
137+
}
138+
});
139+
140+
141+
142+
});
143+
function openDetectorInfo(uuid) {
144+
const url = `http://127.0.0.1:8000/browse/data_files/${uuid}`;
145+
window.open(url, '_blank');
70146
}
71-
});
72-
});
73-
</script>
74-
</body>
147+
148+
function copyToClipboard(uuid) {
149+
// Get the selected release
150+
const releaseSelect = document.getElementById('releaseSelect');
151+
const selectedRelease = releaseSelect ? releaseSelect.value : '';
152+
153+
// Check if a release is selected
154+
if (!selectedRelease) {
155+
alert('⚠️ Please select a release before copying.');
156+
return;
157+
}
158+
159+
const url = new URL("{% url 'get-full-path' %}", window.location.origin);
160+
url.searchParams.append('uuid', uuid);
161+
162+
fetch(url, {
163+
headers:{
164+
'Accept': 'application/json',
165+
'X-Requested-With': 'XMLHttpRequest',
166+
},
167+
})
168+
.then(response => response.json())
169+
.then(responseData => {
170+
// Append release to the path
171+
const fullPath = `${responseData}/${selectedRelease}`;
172+
173+
navigator.clipboard.writeText(fullPath).then(() => {
174+
alert('Full path copied to clipboard!');
175+
}).catch(err => {
176+
console.error('Failed to copy:', err);
177+
});
178+
})
179+
180+
.catch(error => console.error('Error:', error))
181+
}
182+
</script>
183+
</div>
184+
185+
<!-- Right Column -->
186+
<div
187+
style="
188+
flex: 1;
189+
padding: 10px;
190+
background-color: #f8f9fa;
191+
border: 1px solid #dee2e6;
192+
margin-left: 0;
193+
margin-right: 0;
194+
"
195+
>
196+
<p id="params">Select a pixel on the plot.</p>
197+
</div>
198+
</div>
75199
{% endblock %}

browse/views.py

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ def focal_plane_ui_view(request):
614614

615615
metadatas = DataFile.objects.filter(name='detector_info').values_list('metadata', flat=True)
616616
uuids = DataFile.objects.filter(name='detector_info').values_list('uuid', flat=True)
617-
uuids = [str(uuid) for uuid in uuids]
617+
uuids = np.array([str(uuid) for uuid in uuids])
618618
pointing_u_v_list = []
619619
channel=[]
620620
pol=[]
@@ -640,16 +640,98 @@ def focal_plane_ui_view(request):
640640
sizes.append(1000/ss[0])
641641

642642

643-
x_values = [point[0] for point in pointing_u_v_list]
644-
y_values = [point[1] for point in pointing_u_v_list]
643+
pol=np.array(pol)
644+
sizes=np.array(sizes)
645+
channel=np.array(channel)
646+
x_values = np.array([point[0] for point in pointing_u_v_list])
647+
y_values = np.array([point[1] for point in pointing_u_v_list])
648+
649+
coords = np.column_stack((x_values, y_values))
650+
651+
unique_coords, unique_indices, inverse_indices = np.unique(
652+
coords, axis=0, return_index=True, return_inverse=True
653+
)
654+
655+
coord_to_indices = {}
656+
for idx, coord_idx in enumerate(inverse_indices):
657+
coord_tuple = tuple(unique_coords[coord_idx])
658+
if coord_tuple not in coord_to_indices:
659+
coord_to_indices[coord_tuple] = []
660+
coord_to_indices[coord_tuple].append(idx)
661+
662+
x_unique = unique_coords[:, 0]
663+
y_unique = unique_coords[:, 1]
664+
665+
uuids_unique = np.empty(len(unique_coords), dtype=object)
666+
for i, coord in enumerate(unique_coords):
667+
coord_tuple = tuple(coord)
668+
indices = coord_to_indices[coord_tuple]
669+
uuids_unique[i] = uuids[indices]
670+
671+
672+
uuids_list = [list(uuid_array) for uuid_array in uuids_unique]
645673

646674
context = {
647-
'x_values': x_values,
648-
'y_values': y_values,
649-
'uuids': uuids,
650-
'channel': channel,
651-
'pol': pol,
652-
'size': sizes,
675+
'x_values': json.dumps(x_unique.tolist()),
676+
'y_values': json.dumps(y_unique.tolist()),
677+
'uuids': json.dumps(uuids_list),
678+
'channel': json.dumps(channel[unique_indices].tolist()),
679+
'pol': json.dumps(pol[unique_indices].tolist()),
680+
'size': json.dumps(sizes[unique_indices].tolist()),
653681
}
654682

683+
655684
return render(request, 'browse/focalplane_ui.html', context)
685+
686+
from django.http import JsonResponse
687+
688+
from django.http import JsonResponse
689+
import json
690+
import logging
691+
692+
logger = logging.getLogger(__name__)
693+
def handle_plot_click(request):
694+
try:
695+
uuid_string = request.GET.get('uuid')
696+
697+
if not uuid_string:
698+
return JsonResponse({'error': 'uuid parameter is required'}, status=400)
699+
700+
uuids = [u.strip() for u in uuid_string.split(',')]
701+
702+
data_queryset = DataFile.objects.filter(uuid__in=uuids).values('uuid', 'metadata')
703+
704+
if not data_queryset.exists():
705+
return JsonResponse({'error': 'No data found for these uuids'}, status=404)
706+
707+
result = {}
708+
for item in data_queryset:
709+
uuid_key = str(item['uuid'])
710+
metadata = item['metadata']
711+
712+
if isinstance(metadata, str):
713+
result[uuid_key] = json.loads(metadata)
714+
else:
715+
result[uuid_key] = metadata
716+
717+
return JsonResponse(result)
718+
719+
except Exception as e:
720+
logger.error(f"Error in handle_plot_click: {str(e)}")
721+
return JsonResponse({'error': str(e)}, status=500)
722+
723+
def get_full_path(request):
724+
try:
725+
uuid_string = request.GET.get('uuid')
726+
727+
if not uuid_string:
728+
return JsonResponse({'error': 'uuid parameter is required'}, status=400)
729+
730+
obj = DataFile.objects.filter(uuid=uuid_string)
731+
print(obj[0])
732+
733+
return JsonResponse(obj[0].full_path, safe=False)
734+
735+
except Exception as e:
736+
logger.error(f"Error in get_full_path: {str(e)}")
737+
return JsonResponse({'error': str(e)}, status=500)

0 commit comments

Comments
 (0)