diff --git a/src/panel_neuroglancer/__init__.py b/src/panel_neuroglancer/__init__.py
index c22d6ad..44d636a 100644
--- a/src/panel_neuroglancer/__init__.py
+++ b/src/panel_neuroglancer/__init__.py
@@ -1,3 +1,3 @@
-from panel_neuroglancer.neuroglancer_nb import NeuroglancerNb
+from panel_neuroglancer.neuroglancer import Neuroglancer
-__all__ = ["NeuroglancerNb"]
\ No newline at end of file
+__all__ = ["Neuroglancer"]
diff --git a/src/panel_neuroglancer/neuroglancer.py b/src/panel_neuroglancer/neuroglancer.py
new file mode 100644
index 0000000..873e22d
--- /dev/null
+++ b/src/panel_neuroglancer/neuroglancer.py
@@ -0,0 +1,165 @@
+from __future__ import annotations
+
+import neuroglancer
+import panel as pn
+import param
+
+from neuroglancer.viewer import Viewer
+from panel.custom import PyComponent
+
+
+DEMO_URL = "https://neuroglancer-demo.appspot.com/#!%7B%22dimensions%22%3A%7B%22x%22%3A%5B6.000000000000001e-9%2C%22m%22%5D%2C%22y%22%3A%5B6.000000000000001e-9%2C%22m%22%5D%2C%22z%22%3A%5B3.0000000000000004e-8%2C%22m%22%5D%7D%2C%22position%22%3A%5B5029.42333984375%2C6217.5849609375%2C1182.5%5D%2C%22crossSectionScale%22%3A3.7621853549999242%2C%22projectionOrientation%22%3A%5B-0.05179581791162491%2C-0.8017329573631287%2C0.0831851214170456%2C-0.5895944833755493%5D%2C%22projectionScale%22%3A4699.372698097029%2C%22layers%22%3A%5B%7B%22type%22%3A%22image%22%2C%22source%22%3A%22precomputed%3A%2F%2Fgs%3A%2F%2Fneuroglancer-public-data%2Fkasthuri2011%2Fimage%22%2C%22tab%22%3A%22source%22%2C%22name%22%3A%22original-image%22%7D%2C%7B%22type%22%3A%22image%22%2C%22source%22%3A%22precomputed%3A%2F%2Fgs%3A%2F%2Fneuroglancer-public-data%2Fkasthuri2011%2Fimage_color_corrected%22%2C%22tab%22%3A%22source%22%2C%22name%22%3A%22corrected-image%22%7D%2C%7B%22type%22%3A%22segmentation%22%2C%22source%22%3A%22precomputed%3A%2F%2Fgs%3A%2F%2Fneuroglancer-public-data%2Fkasthuri2011%2Fground_truth%22%2C%22tab%22%3A%22source%22%2C%22selectedAlpha%22%3A0.63%2C%22notSelectedAlpha%22%3A0.14%2C%22segments%22%3A%5B%223208%22%2C%224901%22%2C%2213%22%2C%224965%22%2C%224651%22%2C%222282%22%2C%223189%22%2C%223758%22%2C%2215%22%2C%224027%22%2C%223228%22%2C%22444%22%2C%223207%22%2C%223224%22%2C%223710%22%5D%2C%22name%22%3A%22ground_truth%22%7D%5D%2C%22layout%22%3A%224panel%22%7D"
+
+
+class Neuroglancer(PyComponent):
+ """
+ Neuroglancer is a Panel component for visualizing and interacting with a Neuroglancer Viewer.
+
+ The component can be initialized from a URL or a `neuroglancer.viewer.Viewer` instance.
+ """
+
+ show_state = param.Boolean(default=False, doc="""
+ Provides a collapsible card widget under the viewer that displays the viewer's state.""")
+
+ url = param.String(default=None, doc="""
+ The URL public URL of the Neuroglancer Viewer.""")
+
+ def __init__(
+ self,
+ source=None,
+ load_demo=False,
+ **params,
+ ):
+ viewer = None
+ if isinstance(source, str):
+ params['url'] = source
+ elif isinstance(source, Viewer):
+ params['url'] = source.get_viewer_url()
+ viewer = source
+ elif source is not None:
+ raise ValueError('Neuroglancer source must be a URL or neuroglancer Viewer instance')
+ self._source_not_provided = source is None
+ super().__init__(**params)
+ if viewer is None:
+ viewer = neuroglancer.Viewer()
+ self.viewer = viewer
+
+ self._setup_ui_components()
+ self._configure_viewer()
+ self._setup_callbacks()
+
+ if isinstance(source, str):
+ self._load_state_from_url(source)
+ if load_demo:
+ self._load_demo()
+
+ def _setup_ui_components(self):
+ self.url_input = pn.widgets.TextInput.from_param(
+ self.param.url,
+ placeholder="Enter a Neuroglancer URL and click Load",
+ name="Input URL",
+ width=700,
+ )
+
+ self.load_button = pn.widgets.Button(
+ name="Load", button_type="primary", width=75
+ )
+ self.demo_button = pn.widgets.Button(
+ name="Demo", button_type="warning", width=75
+ )
+
+ self.json_pane = pn.pane.JSON(
+ {}, theme="light", depth=2, name="Viewer State", sizing_mode='stretch_both'
+ )
+
+ self.shareable_url_pane = pn.pane.Markdown("**Shareable URL:**")
+ self.local_url_pane = pn.pane.Markdown("**Local URL:**")
+
+ self.iframe = pn.pane.HTML(
+ sizing_mode="stretch_both",
+ aspect_ratio=self.param.aspect_ratio,
+ margin=0,
+ min_height=800,
+ styles={"resize": "both", "overflow": "hidden"},
+ )
+
+ def _configure_viewer(self):
+ self._update_local_url()
+ self._update_iframe_with_local_url()
+
+ def _setup_callbacks(self):
+ self.load_button.on_click(self._load_url)
+ self.demo_button.on_click(self._load_demo)
+ self.viewer.shared_state.add_changed_callback(self._on_viewer_state_changed)
+
+ def _load_demo(self, event=None):
+ self.url = DEMO_URL
+ self._load_state_from_url(self.url)
+
+ def _load_url(self, event=None):
+ self._load_state_from_url(self.url)
+
+ def _load_state_from_url(self, url):
+ try:
+ new_state = self._parse_state_from_url(url)
+ self.viewer.set_state(new_state)
+ except Exception as e:
+ print(f"Error loading Neuroglancer state: {e}")
+
+ def _parse_state_from_url(self, url):
+ return neuroglancer.parse_url(url)
+
+ def _on_viewer_state_changed(self):
+ self._update_shareable_url()
+ self._update_json_pane()
+
+ def _update_shareable_url(self):
+ self.url = shareable_url = neuroglancer.to_url(self.viewer.state)
+ self.shareable_url_pane.object = self._generate_dropdown_markup(
+ "Shareable URL", shareable_url
+ )
+
+ def _update_local_url(self):
+ self.local_url_pane.object = self._generate_dropdown_markup(
+ "Local URL", self.viewer.get_viewer_url()
+ )
+
+ def _update_iframe_with_local_url(self):
+ iframe_style = (
+ 'frameborder="0" scrolling="no" marginheight="0" marginwidth="0" '
+ 'style="width:100%; height:100%; min-width:500px; min-height:500px;"'
+ )
+ self.iframe.object = (
+ f''
+ )
+
+ def _update_json_pane(self):
+ self.json_pane.object = self.viewer.state.to_json()
+
+ def _generate_dropdown_markup(self, title, url):
+ return f"""
+
+ {title}:
+ {url}
+
+ """
+
+ def __panel__(self):
+ controls = pn.Column(
+ pn.Row(self.demo_button, self.load_button, visible=self._source_not_provided),
+ pn.Row(self.url_input, visible=self._source_not_provided),
+ self.local_url_pane,
+ self.shareable_url_pane
+ )
+ state_widget = pn.Card(
+ self.json_pane,
+ title="State",
+ collapsed=False,
+ visible=self.param.show_state,
+ styles={"background": "WhiteSmoke"},
+ max_width=350
+ )
+ return pn.Column(
+ controls,
+ pn.Row(state_widget, self.iframe),
+ )
diff --git a/src/panel_neuroglancer/neuroglancer_nb.py b/src/panel_neuroglancer/neuroglancer_nb.py
deleted file mode 100644
index 76f0238..0000000
--- a/src/panel_neuroglancer/neuroglancer_nb.py
+++ /dev/null
@@ -1,174 +0,0 @@
-import panel as pn
-import neuroglancer
-
-pn.extension()
-class NeuroglancerNB(pn.viewable.Viewer):
- """
- A HoloViz Panel app for visualizing and interacting with Neuroglancer viewers
- within a Jupyter Notebook.
-
- This app supports loading from a parameterized Neuroglancer URL or an existing
- `neuroglancer.viewer.Viewer` instance.
- """
-
- DEMO_URL = "https://neuroglancer-demo.appspot.com/#!%7B%22dimensions%22%3A%7B%22x%22%3A%5B6.000000000000001e-9%2C%22m%22%5D%2C%22y%22%3A%5B6.000000000000001e-9%2C%22m%22%5D%2C%22z%22%3A%5B3.0000000000000004e-8%2C%22m%22%5D%7D%2C%22position%22%3A%5B5029.42333984375%2C6217.5849609375%2C1182.5%5D%2C%22crossSectionScale%22%3A3.7621853549999242%2C%22projectionOrientation%22%3A%5B-0.05179581791162491%2C-0.8017329573631287%2C0.0831851214170456%2C-0.5895944833755493%5D%2C%22projectionScale%22%3A4699.372698097029%2C%22layers%22%3A%5B%7B%22type%22%3A%22image%22%2C%22source%22%3A%22precomputed%3A%2F%2Fgs%3A%2F%2Fneuroglancer-public-data%2Fkasthuri2011%2Fimage%22%2C%22tab%22%3A%22source%22%2C%22name%22%3A%22original-image%22%7D%2C%7B%22type%22%3A%22image%22%2C%22source%22%3A%22precomputed%3A%2F%2Fgs%3A%2F%2Fneuroglancer-public-data%2Fkasthuri2011%2Fimage_color_corrected%22%2C%22tab%22%3A%22source%22%2C%22name%22%3A%22corrected-image%22%7D%2C%7B%22type%22%3A%22segmentation%22%2C%22source%22%3A%22precomputed%3A%2F%2Fgs%3A%2F%2Fneuroglancer-public-data%2Fkasthuri2011%2Fground_truth%22%2C%22tab%22%3A%22source%22%2C%22selectedAlpha%22%3A0.63%2C%22notSelectedAlpha%22%3A0.14%2C%22segments%22%3A%5B%223208%22%2C%224901%22%2C%2213%22%2C%224965%22%2C%224651%22%2C%222282%22%2C%223189%22%2C%223758%22%2C%2215%22%2C%224027%22%2C%223228%22%2C%22444%22%2C%223207%22%2C%223224%22%2C%223710%22%5D%2C%22name%22%3A%22ground_truth%22%7D%5D%2C%22layout%22%3A%224panel%22%7D"
-
- def __init__(
- self,
- source=None,
- aspect_ratio=2.75,
- show_state=False,
- load_demo=False,
- **params,
- ):
- """
- Initializes the NeuroglancerNB app.
-
- Parameters
- ----------
- source : str or neuroglancer.viewer.Viewer, optional
- Source for the initial state of the viewer, which can be a URL string or an existing `neuroglancer.viewer.Viewer` instance.
- If None, a new viewer will be initialized without a predefined state.
- aspect_ratio : float, optional
- The width-to-height ratio for the window-responsive Neuroglancer viewer. Default is 2.75.
- show_state : bool, optional
- Provides a collapsible card widget under the viewer that displays the viewer's state.
- Useful for debugging. Default is False.
- load_demo : bool, optional
- If True, loads the demo dataset upon initialization. Default is False.
- **params
- Additional parameters passed to the parent class.
- """
- super().__init__(**params)
- self.source_not_provided = False if source else True
- self.show_state = show_state
- self.viewer = (
- source
- if isinstance(source, neuroglancer.viewer.Viewer)
- else neuroglancer.Viewer()
- )
-
- self._setup_ui_components(aspect_ratio=aspect_ratio)
- self._configure_viewer()
- self._setup_callbacks()
-
- if source and not isinstance(source, neuroglancer.viewer.Viewer):
- self._initialize_viewer_from_url(source)
-
- if load_demo:
- self.demo_button.clicks += 1
-
- def _initialize_viewer_from_url(self, source: str):
- self.url_input.value = source
- self._load_state_from_url(source)
-
- def _setup_ui_components(self, aspect_ratio):
- self.url_input = pn.widgets.TextInput(
- placeholder="Enter a Neuroglancer URL and click Load",
- name="Input URL",
- width=700,
- )
-
- self.load_button = pn.widgets.Button(
- name="Load", button_type="primary", width=75
- )
- self.demo_button = pn.widgets.Button(
- name="Demo", button_type="warning", width=75
- )
-
- self.json_pane = pn.pane.JSON(
- {}, theme="light", depth=2, name="Viewer State", height=800, width=350
- )
-
- self.shareable_url_pane = pn.pane.Markdown("**Shareable URL:**")
- self.local_url_pane = pn.pane.Markdown("**Local URL:**")
-
- self.iframe = pn.pane.HTML(
- sizing_mode="stretch_both",
- aspect_ratio=aspect_ratio,
- min_height=800,
- styles={"resize": "both", "overflow": "hidden"},
- )
-
- def _configure_viewer(self):
- self._update_local_url()
- self._update_iframe_with_local_url()
-
- def _setup_callbacks(self):
- self.load_button.on_click(self._on_load_button_clicked)
- self.demo_button.on_click(self._on_demo_button_clicked)
- self.viewer.shared_state.add_changed_callback(self._on_viewer_state_changed)
-
- def _on_demo_button_clicked(self, event):
- self.url_input.value = self.DEMO_URL
- self._load_state_from_url(self.url_input.value)
-
- def _on_load_button_clicked(self, event):
- self._load_state_from_url(self.url_input.value)
-
- def _load_state_from_url(self, url):
- try:
- new_state = self._parse_state_from_url(url)
- self.viewer.set_state(new_state)
- except Exception as e:
- print(f"Error loading Neuroglancer state: {e}")
-
- def _parse_state_from_url(self, url):
- return neuroglancer.parse_url(url)
-
- def _on_viewer_state_changed(self):
- self._update_shareable_url()
- self._update_json_pane()
-
- def _update_shareable_url(self):
- shareable_url = neuroglancer.to_url(self.viewer.state)
- self.shareable_url_pane.object = self._generate_dropdown_markup(
- "Shareable URL", shareable_url
- )
-
- def _update_local_url(self):
- self.local_url_pane.object = self._generate_dropdown_markup(
- "Local URL", self.viewer.get_viewer_url()
- )
-
- def _update_iframe_with_local_url(self):
- iframe_style = (
- 'frameborder="0" scrolling="no" marginheight="0" marginwidth="0" '
- 'style="width:100%; height:100%; min-width:500px; min-height:500px;"'
- )
- self.iframe.object = (
- f''
- )
-
- def _update_json_pane(self):
- self.json_pane.object = self.viewer.state.to_json()
-
- def _generate_dropdown_markup(self, title, url):
- return f"""
-
- {title}:
- {url}
-
- """
-
- def __panel__(self):
- # only visible if no source is provided
- controls_layout = pn.Column(
- pn.Row(self.demo_button, self.load_button),
- pn.Row(self.url_input),
- visible=self.source_not_provided,
- )
- links_layout = pn.Column(self.local_url_pane, self.shareable_url_pane)
-
- state_widget = pn.Card(
- self.json_pane,
- title="State",
- collapsed=False,
- visible=self.show_state,
- styles={"background": "WhiteSmoke"},
- max_width=350
- )
- return pn.Column(
- controls_layout,
- links_layout,
- pn.Row(state_widget, self.iframe))