Skip to content

Commit bb874fb

Browse files
committed
Improved electrode counter and dynamic download button appearance
1 parent 7c03c20 commit bb874fb

4 files changed

Lines changed: 91 additions & 122 deletions

File tree

channelmap_generator/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77

88
from .backend import *
99

10-
__version__ = "0.1.0"
10+
__version__ = "0.3.0"
1111
__author__ = "Maxime Beau"
1212
__email__ = "maxime@princeton.edu"

channelmap_generator/gui/gui.py

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
import pandas as pd
1616
import panel as pn
1717
import param
18+
19+
import logging
20+
from bokeh.util.logconfig import basicConfig
21+
basicConfig(level=logging.ERROR) # no warnings
1822
from bokeh import events
1923
from bokeh.models import (
2024
BoxSelectTool,
@@ -29,6 +33,7 @@
2933
)
3034
from bokeh.plotting import figure
3135

36+
from channelmap_generator import __version__
3237
from channelmap_generator.constants import PROBE_N, PROBE_TYPE_MAP, SUPPORTED_1shank_PRESETS, SUPPORTED_4shanks_PRESETS, WIRING_FILE_MAP
3338
from channelmap_generator.utils import imro
3439
from channelmap_generator.types import Electrode
@@ -98,6 +103,8 @@ class ChannelmapGUI(param.Parameterized):
98103

99104
# Parameters - will take their value as attributes after class initialization
100105
default_type = "2.0-4shanks"
106+
download_button_color = param.String(default="default")
107+
download_button_label = param.String(default="Select electrodes...")
101108

102109
probe_type = param.Selector(default=default_type, objects=list(PROBE_TYPE_MAP.keys()), doc="Neuropixels probe type")
103110

@@ -902,25 +909,12 @@ def create_widgets(self):
902909
name="IMRO or PDF filename (omit extension)",
903910
value=f"manual_selection_{self.probe_type}", # default filename defined here
904911
width=300,
912+
margin=(0, 0, 0, 30)
905913
)
906914

907-
self.download_imro_button = pn.widgets.FileDownload(
908-
callback=self.generate_imro_content,
909-
filename=f"{self.filename_input.value}.imro",
910-
button_type="success",
911-
width=140,
912-
icon="file-text",
913-
label="Download IMRO ⬇",
914-
)
915-
916-
self.download_pdf_button = pn.widgets.FileDownload(
917-
callback=self.generate_pdf_content,
918-
filename=f"{self.filename_input.value}.pdf",
919-
button_type="success",
920-
width=140,
921-
icon="file-type-pdf",
922-
label="Download PDF ⬇",
923-
)
915+
# Download buttons creation
916+
# (in their own function to handle their reactive appearance)
917+
self.create_download_buttons()
924918

925919
# Prominent electrode counter (moved to top)
926920
self.electrode_counter = pn.pane.HTML(
@@ -932,7 +926,7 @@ def create_widgets(self):
932926
</div>
933927
""",
934928
width=300,
935-
margin=(10, 10),
929+
margin=(30, 10, 10, 10),
936930
align="center",
937931
)
938932

@@ -973,22 +967,65 @@ def create_widgets(self):
973967
)
974968
self.apply_uploaded_imro_button.on_click(lambda event: self.apply_uploaded_imro())
975969

970+
971+
def create_download_buttons(self):
972+
"""Create download buttons as a reactive pane"""
973+
# This method recreates the buttons when color changes
974+
self.download_imro_button = pn.widgets.FileDownload(
975+
callback=self.generate_imro_content,
976+
filename=f"{self.filename_input.value}.imro",
977+
button_type=self.download_button_color,
978+
width=140,
979+
icon="file-text",
980+
label=self.download_button_label,
981+
)
982+
983+
self.download_pdf_button = pn.widgets.FileDownload(
984+
callback=self.generate_pdf_content,
985+
filename=f"{self.filename_input.value}.pdf",
986+
button_type=self.download_button_color,
987+
width=140,
988+
icon="file-type-pdf",
989+
label=self.download_button_label.replace("IMRO", "PDF"),
990+
)
991+
992+
return pn.Row(
993+
self.download_imro_button,
994+
self.download_pdf_button,
995+
sizing_mode="stretch_width",
996+
margin=(0, 0, 0, 20),
997+
)
998+
999+
@pn.depends('download_button_color', 'download_button_label')
1000+
def get_download_buttons(self):
1001+
"""Reactive method that recreates buttons when color changes"""
1002+
return self.create_download_buttons()
1003+
9761004
def create_layout(self):
9771005
"""Create the main Panel layout"""
9781006

1007+
# Counter and Downloader (fixed on the right)
1008+
downloader = pn.Column(
1009+
pn.Column(
1010+
self.electrode_counter,
1011+
self.clear_button,
1012+
margin=(0, 0, -10, 20),
1013+
),
1014+
pn.pane.Markdown("## Export Channelmap", margin=(10, 0, -5, 30)),
1015+
self.filename_input,
1016+
self.get_download_buttons,
1017+
)
1018+
9791019
# Controls panel (fixed on left)
9801020
controls = pn.Column(
9811021
pn.pane.Markdown(
9821022
(
983-
"<div style='text-align: center; padding: 12px;'><strong>See project at:"
1023+
f"<div style='text-align: center; padding: 12px;'><strong>See project (v{__version__}) at:"
9841024
"<br><a href='https://github.com/m-beau/channelmap_generator' "
9851025
"target='_blank'>github.com/m-beau/channelmap_generator</a></strong></div>"
9861026
),
987-
margin=(0, 0, -10, 40),
1027+
margin=(0, 0, 0, 40),
9881028
),
989-
# Prominent electrode counter at top
990-
self.electrode_counter,
991-
self.clear_button,
9921029
pn.Column(
9931030
pn.pane.Markdown("## Probe and recording metadata", margin=(-5, 0, 0, 10)),
9941031
pn.pane.Markdown(
@@ -1022,13 +1059,7 @@ def create_layout(self):
10221059
self.imro_file_loader,
10231060
# pn.Spacer(height=30),
10241061
self.apply_uploaded_imro_button,
1025-
pn.pane.Markdown("## Export Channelmap", margin=(10, 0, -5, 10)),
1026-
self.filename_input,
1027-
pn.Row(
1028-
self.download_imro_button,
1029-
self.download_pdf_button,
1030-
sizing_mode="stretch_width",
1031-
),
1062+
10321063
pn.pane.Markdown("## Instructions", margin=(10, 0, -5, 10)),
10331064
pn.pane.HTML("""
10341065
<div style="font-size: 13px; line-height: 1.4; text-align: justify;">
@@ -1071,7 +1102,8 @@ def create_layout(self):
10711102
controls,
10721103
pn.Spacer(width=370), # Slightly wider than controls (350 + margin)
10731104
plot_container,
1074-
sizing_mode="stretch_width",
1105+
downloader,
1106+
sizing_mode="fixed",
10751107
)
10761108

10771109
return layout
@@ -1104,30 +1136,32 @@ def update_electrode_counter(self):
11041136
# n_remaining = max_allowed - n_selected
11051137

11061138
# Update electrode counter
1107-
if hasattr(self, "electrode_counter"):
1108-
if n_selected < max_allowed:
1109-
counter_html = f"""
1110-
<div style="background: #f0f8ff; border: 2px solid #4a90e2; border-radius: 8px;
1111-
padding: 12px; text-align: center; font-size: 16px; font-weight: bold;
1112-
color: #AA4A44;">
1113-
Selected Electrodes: {n_selected}/{max_allowed}
1114-
</div>
1115-
"""
1116-
else:
1117-
counter_html = f"""
1118-
<div style="background: #f0f8ff; border: 2px solid #4a90e2; border-radius: 8px;
1119-
padding: 12px; text-align: center; font-size: 16px; font-weight: bold;
1120-
color: #008000;">
1121-
Selected Electrodes: {n_selected}/{max_allowed}<br>
1122-
Ready for IMRO file generation!
1123-
</div>
1124-
"""
1125-
self.electrode_counter.object = counter_html
1126-
1139+
if n_selected < max_allowed:
1140+
counter_html = f"""
1141+
<div style="background: #f0f8ff; border: 2px solid #4a90e2; border-radius: 8px;
1142+
padding: 12px; text-align: center; font-size: 16px; font-weight: bold;
1143+
color: #AA4A44;">
1144+
Selected Electrodes: {n_selected}/{max_allowed}
1145+
</div>
1146+
"""
1147+
self.download_button_color = 'default'
1148+
self.download_button_label = "Select electrodes..."
1149+
else:
1150+
counter_html = f"""
1151+
<div style="background: #f0f8ff; border: 2px solid #4a90e2; border-radius: 8px;
1152+
padding: 12px; text-align: center; font-size: 16px; font-weight: bold;
1153+
color: #008000;">
1154+
Selected Electrodes: {n_selected}/{max_allowed}<br>
1155+
Ready for IMRO file generation!
1156+
</div>
1157+
"""
1158+
self.download_button_color = 'success'
1159+
self.download_button_label = "Download IMRO ⬇"
11271160

1128-
## App creation utilities
1161+
self.electrode_counter.object = counter_html
11291162

11301163

1164+
## App creation utilities
11311165
def find_free_port(start_port=5007):
11321166
"""Find next available port starting from start_port"""
11331167
for port in range(start_port, start_port + 100):

channelmap_generator/types.py

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,77 +3,9 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6-
from enum import StrEnum
7-
from typing import Self
8-
9-
import numpy as np
106

117
# Note: frozen attributes (@dataclass(frozen=True)) allows hashability
128
# (thus usability as dictionnary keys or set elements)
13-
14-
class ProbeType(StrEnum):
15-
"""Supported Neuropixels probe types."""
16-
17-
NEUROPIXELS_1_0 = "1.0"
18-
NEUROPIXELS_2_0_1SHANK = "2.0-1shank"
19-
NEUROPIXELS_2_0_4SHANKS = "2.0-4shanks"
20-
NEUROPIXELS_NXT = "NXT"
21-
22-
@classmethod
23-
def from_subtype(cls, subtype: int) -> Self:
24-
"""Get probe type from SpikeGLX subtype number."""
25-
from .constants import PROBE_TYPE_MAP
26-
27-
for probe_type, subtypes in PROBE_TYPE_MAP.items():
28-
if subtype in subtypes:
29-
return cls(probe_type)
30-
raise ValueError(f"Unknown probe subtype: {subtype}")
31-
32-
33-
class ReferenceType(StrEnum):
34-
"""Reference electrode types."""
35-
36-
EXTERNAL = "ext"
37-
TIP = "tip"
38-
GROUND = "gnd"
39-
40-
41-
@dataclass(frozen=True)
42-
class ElectrodePosition:
43-
"""Represents an electrode position on a probe."""
44-
45-
electrode: Electrode
46-
x: float
47-
y: float
48-
49-
50-
@dataclass
51-
class ChannelEntry:
52-
"""Represents a single channel entry in IMRO format."""
53-
54-
channel: int
55-
shank_id: int | None = None
56-
bank: int | None = None
57-
bank_mask: int | None = None
58-
ref: int | None = None
59-
ap_gain: int | None = None
60-
lf_gain: int | None = None
61-
hp_filter: int | None = None
62-
electrode_id: int | None = None
63-
64-
65-
@dataclass
66-
class ParsedIMRO:
67-
"""Parsed IMRO data structure."""
68-
69-
selected_electrodes: np.ndarray # Shape: (n_electrodes, 2) - [[shank_id, electrode_id], ...]
70-
probe_type: ProbeType
71-
probe_subtype: int
72-
reference_id: str
73-
ap_gain: int | None = None
74-
lf_gain: int | None = None
75-
hp_filter: int | None = None
76-
779
@dataclass(frozen=True)
7810
class Electrode:
7911
"""Neuropixels electrode electrode_id on shank shank_id."""

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "channelmap-generator"
7-
version = "0.2.1"
7+
dynamic = ["version"]
88
description = "Browser-based Neuropixels channelmap generator."
99
readme = "README.md"
1010
license = {text = "MIT"}
@@ -42,6 +42,9 @@ Documentation = "https://github.com/m-beau/channelmap-generator#readme"
4242
Repository = "https://github.com/m-beau/channelmap-generator"
4343
Issues = "https://github.com/m-beau/channelmap-generator/issues"
4444

45+
[tool.setuptools.dynamic]
46+
version = {attr = "channelmap_generator.__version__"}
47+
4548
[tool.setuptools.packages.find]
4649
where = ["."]
4750
include = ["channelmap_generator*"]

0 commit comments

Comments
 (0)