diff --git a/docs/source/version_0.1.0/region_browser.ipynb b/docs/source/version_0.1.0/region_browser.ipynb
new file mode 100644
index 0000000..2897573
--- /dev/null
+++ b/docs/source/version_0.1.0/region_browser.ipynb
@@ -0,0 +1,1479 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "20650a50",
+ "metadata": {},
+ "source": [
+ "# Hake School Region Browser\n",
+ "\n",
+ "This notebook demonstrates how to use the interactive `region_browser` to view and edit acoustic regions on an echogram.\n",
+ "\n",
+ "The region data follows the [Echoregions](https://echoregions.readthedocs.io/) **Regions2D** standard format, which is widely used in the fisheries acoustics community for polygon annotations of sonar data."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "4c500ef8",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const version = '3.8.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.8.7/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.8.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.8.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.8.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.8.2.min.js\", \"https://cdn.holoviz.org/panel/1.8.7/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));",
+ "application/vnd.holoviews_load.v0+json": ""
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n",
+ "application/vnd.holoviews_load.v0+json": ""
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.holoviews_exec.v0+json": "",
+ "text/html": [
+ "
\n",
+ ""
+ ]
+ },
+ "metadata": {
+ "application/vnd.holoviews_exec.v0+json": {
+ "id": "9809e336-ccb3-4863-9f6e-98dda51a537c"
+ }
+ },
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = false;\n const version = '3.8.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.8.7/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.15.1/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));",
+ "application/vnd.holoviews_load.v0+json": ""
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n",
+ "application/vnd.holoviews_load.v0+json": ""
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = false;\n const version = '3.8.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.8.7/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.15.1/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));",
+ "application/vnd.holoviews_load.v0+json": ""
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n",
+ "application/vnd.holoviews_load.v0+json": ""
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "
<xarray.Dataset> Size: 6MB\n",
+ "Dimensions: (channel: 3, ping_time: 344, depth: 759)\n",
+ "Coordinates:\n",
+ " * channel (channel) <U37 444B 'GPT 18 kHz 009072058c8d 1-1 ES18...\n",
+ " * ping_time (ping_time) datetime64[ns] 3kB 2017-07-31T18:08:45 ......\n",
+ " * depth (depth) float64 6kB 0.0 1.0 2.0 3.0 ... 756.0 757.0 758.0\n",
+ "Data variables:\n",
+ " Sv (channel, ping_time, depth) float64 6MB dask.array<chunksize=(3, 344, 759), meta=np.ndarray>\n",
+ " frequency_nominal (channel) float64 24B dask.array<chunksize=(3,), meta=np.ndarray>\n",
+ " latitude (ping_time) float64 3kB dask.array<chunksize=(344,), meta=np.ndarray>\n",
+ " longitude (ping_time) float64 3kB dask.array<chunksize=(344,), meta=np.ndarray>\n",
+ "Attributes:\n",
+ " processing_function: commongrid.compute_MVBS\n",
+ " processing_level: Level 3A\n",
+ " processing_level_url: https://echopype.readthedocs.io/en/stable/p...\n",
+ " processing_software_name: echopype\n",
+ " processing_software_version: 0.9.0\n",
+ " processing_time: 2024-08-14T00:14:32Z
- channel: 3
- ping_time: 344
- depth: 759
channel
(channel)
<U37
'GPT 18 kHz 009072058c8d 1-1 ES...
- long_name :
- Vendor channel ID
array(['GPT 18 kHz 009072058c8d 1-1 ES18-11',\n",
+ " 'GPT 38 kHz 009072058146 2-1 ES38B',\n",
+ " 'GPT 120 kHz 00907205a6d0 4-1 ES120-7C'], dtype='<U37')
ping_time
(ping_time)
datetime64[ns]
2017-07-31T18:08:45 ... 2017-07-...
- axis :
- T
- long_name :
- Ping time
- standard_name :
- time
array(['2017-07-31T18:08:45.000000000', '2017-07-31T18:08:50.000000000',\n",
+ " '2017-07-31T18:08:55.000000000', ..., '2017-07-31T18:37:10.000000000',\n",
+ " '2017-07-31T18:37:15.000000000', '2017-07-31T18:37:20.000000000'],\n",
+ " dtype='datetime64[ns]')
depth
(depth)
float64
0.0 1.0 2.0 ... 756.0 757.0 758.0
- long_name :
- Range distance
- units :
- m
array([ 0., 1., 2., ..., 756., 757., 758.])
Sv
(channel, ping_time, depth)
float64
dask.array<chunksize=(3, 344, 759), meta=np.ndarray>
- actual_range :
- [-117.46, 9.12]
- binning_mode :
- physical units
- cell_methods :
- ping_time: mean (interval: 5 second comment: ping_time is the interval start) depth: mean (interval: 1.0 meter comment: depth is the interval start)
- long_name :
- Mean volume backscattering strength (MVBS, mean Sv re 1 m-1)
- ping_time_interval :
- 5s
- range_meter_interval :
- 1.0m
- units :
- dB
\n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " | | \n",
+ " Array | \n",
+ " Chunk | \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " | Bytes | \n",
+ " 5.98 MiB | \n",
+ " 5.98 MiB | \n",
+ " \n",
+ " \n",
+ " \n",
+ " | Shape | \n",
+ " (3, 344, 759) | \n",
+ " (3, 344, 759) | \n",
+ " \n",
+ " \n",
+ " | Dask graph | \n",
+ " 1 chunks in 2 graph layers | \n",
+ " \n",
+ " \n",
+ " | Data type | \n",
+ " float64 numpy.ndarray | \n",
+ " \n",
+ " \n",
+ " \n",
+ " | \n",
+ " \n",
+ " \n",
+ " | \n",
+ "
\n",
+ "
frequency_nominal
(channel)
float64
dask.array<chunksize=(3,), meta=np.ndarray>
- long_name :
- Transducer frequency
- standard_name :
- sound_frequency
- units :
- Hz
- valid_min :
- 0.0
\n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " | | \n",
+ " Array | \n",
+ " Chunk | \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " | Bytes | \n",
+ " 24 B | \n",
+ " 24 B | \n",
+ " \n",
+ " \n",
+ " \n",
+ " | Shape | \n",
+ " (3,) | \n",
+ " (3,) | \n",
+ " \n",
+ " \n",
+ " | Dask graph | \n",
+ " 1 chunks in 2 graph layers | \n",
+ " \n",
+ " \n",
+ " | Data type | \n",
+ " float64 numpy.ndarray | \n",
+ " \n",
+ " \n",
+ " \n",
+ " | \n",
+ " \n",
+ " \n",
+ " | \n",
+ "
\n",
+ "
latitude
(ping_time)
float64
dask.array<chunksize=(344,), meta=np.ndarray>
- history :
- 2024-08-14 00:14:28.387715 +00:00. Interpolated or propagated from Platform latitude/longitude.
- long_name :
- Platform latitude
- standard_name :
- latitude
- units :
- degrees_north
- valid_range :
- (-90.0, 90.0)
\n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " | | \n",
+ " Array | \n",
+ " Chunk | \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " | Bytes | \n",
+ " 2.69 kiB | \n",
+ " 2.69 kiB | \n",
+ " \n",
+ " \n",
+ " \n",
+ " | Shape | \n",
+ " (344,) | \n",
+ " (344,) | \n",
+ " \n",
+ " \n",
+ " | Dask graph | \n",
+ " 1 chunks in 2 graph layers | \n",
+ " \n",
+ " \n",
+ " | Data type | \n",
+ " float64 numpy.ndarray | \n",
+ " \n",
+ " \n",
+ " \n",
+ " | \n",
+ " \n",
+ " \n",
+ " | \n",
+ "
\n",
+ "
longitude
(ping_time)
float64
dask.array<chunksize=(344,), meta=np.ndarray>
- history :
- 2024-08-14 00:14:28.387715 +00:00. Interpolated or propagated from Platform latitude/longitude.
- long_name :
- Platform longitude
- standard_name :
- longitude
- units :
- degrees_east
- valid_range :
- (-180.0, 180.0)
\n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " | | \n",
+ " Array | \n",
+ " Chunk | \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " | Bytes | \n",
+ " 2.69 kiB | \n",
+ " 2.69 kiB | \n",
+ " \n",
+ " \n",
+ " \n",
+ " | Shape | \n",
+ " (344,) | \n",
+ " (344,) | \n",
+ " \n",
+ " \n",
+ " | Dask graph | \n",
+ " 1 chunks in 2 graph layers | \n",
+ " \n",
+ " \n",
+ " | Data type | \n",
+ " float64 numpy.ndarray | \n",
+ " \n",
+ " \n",
+ " \n",
+ " | \n",
+ " \n",
+ " \n",
+ " | \n",
+ "
\n",
+ "
- processing_function :
- commongrid.compute_MVBS
- processing_level :
- Level 3A
- processing_level_url :
- https://echopype.readthedocs.io/en/stable/processing-levels.html
- processing_software_name :
- echopype
- processing_software_version :
- 0.9.0
- processing_time :
- 2024-08-14T00:14:32Z
"
+ ],
+ "text/plain": [
+ " Size: 6MB\n",
+ "Dimensions: (channel: 3, ping_time: 344, depth: 759)\n",
+ "Coordinates:\n",
+ " * channel (channel) \n",
+ " frequency_nominal (channel) float64 24B dask.array\n",
+ " latitude (ping_time) float64 3kB dask.array\n",
+ " longitude (ping_time) float64 3kB dask.array\n",
+ "Attributes:\n",
+ " processing_function: commongrid.compute_MVBS\n",
+ " processing_level: Level 3A\n",
+ " processing_level_url: https://echopype.readthedocs.io/en/stable/p...\n",
+ " processing_software_name: echopype\n",
+ " processing_software_version: 0.9.0\n",
+ " processing_time: 2024-08-14T00:14:32Z"
+ ]
+ },
+ "execution_count": 1,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import xarray as xr\n",
+ "import fsspec\n",
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "import ast\n",
+ "import echoshader\n",
+ "\n",
+ "# S3 Connection\n",
+ "S3_URL = \"s3://agr230002-bucket01/hake_data/data_zarr/MVBS/2017/x0062_8_wt_20170731_180848_f0002.zarr\"\n",
+ "storage_options = {\"endpoint_url\": \"https://sdsc.osn.xsede.org\"}\n",
+ "fs = fsspec.filesystem(\"s3\", anon=True, client_kwargs=storage_options)\n",
+ "\n",
+ "# Load data\n",
+ "ds_ooi = xr.open_zarr(fs.get_mapper(S3_URL), consolidated=False)\n",
+ "\n",
+ "# Display the dataset structure\n",
+ "ds_ooi"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bab67383",
+ "metadata": {},
+ "source": [
+ "## Loading Region Data\n",
+ "\n",
+ "### About Echoregions\n",
+ "\n",
+ "[Echoregions](https://echoregions.readthedocs.io/) is a Python package that interfaces with water column sonar annotations. It provides tools to:\n",
+ "\n",
+ "- Parse manual annotations from Echoview software\n",
+ "- Create training datasets for machine learning models\n",
+ "- Generate masks for biomass estimation\n",
+ "- Integrate seamlessly with Echopype xarray datasets\n",
+ "\n",
+ "### The Regions2D Format\n",
+ "\n",
+ "The **Regions2D** format stores each polygon region as a row with three columns:\n",
+ "\n",
+ "- `region_id`: Unique identifier (integer)\n",
+ "- `time`: 1-D array of datetime64[ns] values defining polygon vertices\n",
+ "- `depth`: 1-D array of float values (in meters) for each vertex\n",
+ "\n",
+ "This format allows our Region Browser to:\n",
+ "- Load and edit regions interactively\n",
+ "- Export to CSV for use with Echoregions tools\n",
+ "- Integrate with ML training pipelines\n",
+ "- Work with existing Echoview workflows\n",
+ "\n",
+ "### Parsing the CSV\n",
+ "\n",
+ "We load regions from `sample_hake_regions.csv` and parse the array strings into numpy arrays. While our Region Browser is compatible with the Echoregions format, it does not require Echoregions to be installed.\n",
+ "\n",
+ "**Note:** If you have Echoregions installed, you can also load regions using:\n",
+ "```python\n",
+ "import echoregions as er\n",
+ "regions2d = er.read_regions_csv('sample_hake_regions.csv')\n",
+ "sample_df = regions2d.data[['region_id', 'time', 'depth']]\n",
+ "```\n",
+ "\n",
+ "For this tutorial, we parse the CSV directly to show how the format works.\n",
+ "\n",
+ "*You can also use the \"Load CSV\" button inside the browser to import files interactively!*\n",
+ "\n",
+ "For more details on the Regions2D format, see the [Echoregions Regions2D documentation](https://echoregions.readthedocs.io/en/stable/regions2d.html)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "cec7ced9",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " region_id | \n",
+ " time | \n",
+ " depth | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " 1 | \n",
+ " [2017-07-31T18:10:00.000000000, 2017-07-31T18:... | \n",
+ " [40.0, 40.0, 80.0, 80.0] | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " 2 | \n",
+ " [2017-07-31T18:20:00.000000000, 2017-07-31T18:... | \n",
+ " [100.0, 100.0, 150.0, 150.0] | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " 3 | \n",
+ " [2017-07-31T18:30:00.000000000, 2017-07-31T18:... | \n",
+ " [50.0, 50.0, 90.0, 90.0] | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " 4 | \n",
+ " [2017-07-31T18:12:00.000000000, 2017-07-31T18:... | \n",
+ " [60.0, 60.0, 100.0, 100.0] | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " region_id time \\\n",
+ "0 1 [2017-07-31T18:10:00.000000000, 2017-07-31T18:... \n",
+ "1 2 [2017-07-31T18:20:00.000000000, 2017-07-31T18:... \n",
+ "2 3 [2017-07-31T18:30:00.000000000, 2017-07-31T18:... \n",
+ "3 4 [2017-07-31T18:12:00.000000000, 2017-07-31T18:... \n",
+ "\n",
+ " depth \n",
+ "0 [40.0, 40.0, 80.0, 80.0] \n",
+ "1 [100.0, 100.0, 150.0, 150.0] \n",
+ "2 [50.0, 50.0, 90.0, 90.0] \n",
+ "3 [60.0, 60.0, 100.0, 100.0] "
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Load the CSV file\n",
+ "sample_df = pd.read_csv('sample_hake_regions.csv')\n",
+ "\n",
+ "# Parse the string representations back into numpy arrays\n",
+ "# The CSV stores arrays as strings, so we convert them back to proper data types\n",
+ "sample_df['time'] = sample_df['time'].apply(\n",
+ " lambda x: np.array(ast.literal_eval(x), dtype='datetime64[ns]')\n",
+ ")\n",
+ "sample_df['depth'] = sample_df['depth'].apply(\n",
+ " lambda x: np.array(ast.literal_eval(x), dtype='float64')\n",
+ ")\n",
+ "\n",
+ "# Display the parsed DataFrame\n",
+ "sample_df"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "33b74475",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = false;\n const version = '3.8.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.8.7/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.15.1/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));",
+ "application/vnd.holoviews_load.v0+json": ""
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n",
+ "application/vnd.holoviews_load.v0+json": ""
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.holoviews_exec.v0+json": "",
+ "text/html": [
+ "\n",
+ ""
+ ]
+ },
+ "metadata": {
+ "application/vnd.holoviews_exec.v0+json": {
+ "id": "a8ab5523-8638-44a1-b943-15c782e180f3"
+ }
+ },
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Pre-caching backgrounds...\n",
+ " Processed Region 1\n",
+ " Processed Region 2\n",
+ " Processed Region 3\n",
+ " Processed Region 4\n",
+ "Cached 4 backgrounds successfully.\n",
+ "Launching server at http://localhost:60933\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Reset Region 1 to baseline\n",
+ "Reset Region 1 to baseline\n",
+ "Reset Region 1 to baseline\n",
+ "Reset Region 1 to baseline\n",
+ "Reset Region 1 to baseline\n",
+ "Loaded 4 regions successfully.\n",
+ "Reset Region 1 to baseline\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Launch the Region Browser\n",
+ "browser = echoshader.region_browser(\n",
+ " ds=ds_ooi,\n",
+ " regions_df=sample_df,\n",
+ " cache_backgrounds=True\n",
+ ")\n",
+ "\n",
+ "browser.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f9c653aa",
+ "metadata": {},
+ "source": [
+ "## Using the Browser\n",
+ "\n",
+ "### Browse Mode\n",
+ "- Navigate between regions using the dropdown menu or Previous/Next buttons\n",
+ "- View the annotated regions overlaid on the echogram\n",
+ "\n",
+ "### Edit Mode\n",
+ "\n",
+ "**Editing Tools (in the right toolbar):**\n",
+ "- **Polygon Draw:** Click to add vertices, double-click last vertex to finish polygon. Drag to move entire polygon.\n",
+ "- **Polygon Edit:** Drag individual vertices to reposition them. Double-click a vertex to remove it.\n",
+ "- **Reset Region:** Restores the original polygon if you made a mistake.\n",
+ "\n",
+ "**Workflow:**\n",
+ "1. Toggle to **Edit** mode using the mode selector\n",
+ "2. Select the appropriate editing tool from the right toolbar (PolyDraw or PolyEdit)\n",
+ "3. Make your edits to the polygon\n",
+ "4. Click **Apply Edit** to update the region in memory\n",
+ "5. Edit additional regions as needed\n",
+ "6. Click **Export to CSV** to save all changes to disk\n",
+ "\n",
+ "\u26a0\ufe0f **Important:** Changes are only saved in memory until you export to CSV!\n",
+ "\n",
+ "### Working with Echoregions\n",
+ "\n",
+ "The exported CSV files are fully compatible with the Echoregions library. You can load them for further analysis:\n",
+ "```python\n",
+ "import echoregions as er\n",
+ "\n",
+ "# Load regions\n",
+ "regions2d = er.read_regions_csv('edited_regions_20240315_120000.csv')\n",
+ "\n",
+ "# Create masks for machine learning\n",
+ "mask_ds, region_points = regions2d.region_mask(\n",
+ " ds_ooi[\"Sv\"].isel(channel=1).drop_vars(\"channel\"),\n",
+ " region_class=\"Hake\"\n",
+ ")\n",
+ "\n",
+ "# Plot regions\n",
+ "regions2d.plot(region_class=\"Hake\", close_regions=True)\n",
+ "\n",
+ "# Select specific regions\n",
+ "hake_regions = regions2d.select_region(region_class=\"Hake\")\n",
+ "```\n",
+ "\n",
+ "For more information on Echoregions workflows, see the [Echoregions documentation](https://echoregions.readthedocs.io/)."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "echoshader-dev",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.14"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/source/version_0.1.0/sample_hake_regions.csv b/docs/source/version_0.1.0/sample_hake_regions.csv
new file mode 100644
index 0000000..534f9c8
--- /dev/null
+++ b/docs/source/version_0.1.0/sample_hake_regions.csv
@@ -0,0 +1,5 @@
+region_id,time,depth
+1,"['2017-07-31T18:10:00', '2017-07-31T18:15:00', '2017-07-31T18:15:00', '2017-07-31T18:10:00']","[40.0, 40.0, 80.0, 80.0]"
+2,"['2017-07-31T18:20:00', '2017-07-31T18:25:00', '2017-07-31T18:25:00', '2017-07-31T18:20:00']","[100.0, 100.0, 150.0, 150.0]"
+3,"['2017-07-31T18:30:00', '2017-07-31T18:35:00', '2017-07-31T18:35:00', '2017-07-31T18:30:00']","[50.0, 50.0, 90.0, 90.0]"
+4,"['2017-07-31T18:12:00', '2017-07-31T18:17:00', '2017-07-31T18:17:00', '2017-07-31T18:12:00']","[60.0, 60.0, 100.0, 100.0]"
diff --git a/echoshader/__init__.py b/echoshader/__init__.py
index 7b0b983..a318cb2 100644
--- a/echoshader/__init__.py
+++ b/echoshader/__init__.py
@@ -1,6 +1,8 @@
from .core import Echoshader
+from .region_browser import region_browser
-__all__ = ["Echoshader"]
+__all__ = ["Echoshader", "region_browser"]
-from ._echoshader_version import version as __version__ # noqa
+# from .echoshader_version import version as __version__ # noqa
+__version__ = "0.1.0" # Temporary version
diff --git a/echoshader/region_browser.py b/echoshader/region_browser.py
new file mode 100644
index 0000000..3dec765
--- /dev/null
+++ b/echoshader/region_browser.py
@@ -0,0 +1,642 @@
+"""
+Region Browser - Interactive annotation and editing tool for ocean sonar regions.
+
+Allows users to browse, edit, save, and export regions on echogram data.
+"""
+
+import logging
+import warnings
+
+import holoviews as hv
+import numpy as np
+import pandas as pd
+import panel as pn
+from holoviews.streams import PolyDraw, PolyEdit
+
+warnings.filterwarnings("ignore")
+logging.getLogger("root").setLevel(logging.ERROR)
+
+
+def region_browser(ds, regions_df, cache_backgrounds=True):
+ """
+ Create an interactive region browser for echogram data.
+
+ Parameters
+ ----------
+ ds : xarray.Dataset
+ MVBS dataset containing echogram data with required variables:
+ - Sv: backscatter data
+ - ping_time: time dimension
+ - echo_range or depth: vertical dimension
+ regions_df : pandas.DataFrame
+ DataFrame with regions in Echoregions format
+ Required columns: region_id, time, depth
+ cache_backgrounds : bool, optional
+ Pre-cache echogram backgrounds for faster navigation (default: True)
+
+ Returns
+ -------
+ panel.Row
+ Interactive region browser panel with Browse/Edit modes
+
+ Examples
+ --------
+ >>> import echoshader
+ >>> import xarray as xr
+ >>> import pandas as pd
+ >>>
+ >>> # Load your data
+ >>> ds = xr.open_zarr('path/to/data.zarr')
+ >>> regions = pd.DataFrame({
+ ... 'region_id': [1, 2, 3],
+ ... 'time': [...],
+ ... 'depth': [...]
+ ... })
+ >>>
+ >>> # Create browser
+ >>> browser = echoshader.region_browser(ds=ds, regions_df=regions)
+ >>> browser.show()
+ """
+
+ pn.extension()
+
+ def format_time(x):
+ """Format milliseconds to readable time"""
+ try:
+ dt = pd.to_datetime(x, unit="ms")
+ return dt.strftime("%H:%M:%S")
+ except Exception:
+ return f"{x:.0f}"
+
+ def parse_polygons_from_df(df):
+ """Parse polygon data from DataFrame"""
+ results = []
+ for _, row in df.iterrows():
+ try:
+ times = row["time"]
+ depths = row["depth"]
+ time_ms = times.astype("datetime64[ms]").astype(np.int64)
+ results.append(
+ {
+ "ping_time": time_ms.tolist(),
+ "depth": depths.tolist(),
+ "region_id": row["region_id"],
+ }
+ )
+ except Exception:
+ pass
+ return results
+
+ def validate_loaded_regions(loaded_df, echogram_data):
+ """Validate that loaded regions match the current echogram"""
+ try:
+ echogram_start = pd.Timestamp(echogram_data.ping_time.min().values)
+ echogram_end = pd.Timestamp(echogram_data.ping_time.max().values)
+
+ for idx, row in loaded_df.iterrows():
+ region_id = row["region_id"]
+ times = row["time"]
+
+ region_start = pd.Timestamp(times.min())
+ region_end = pd.Timestamp(times.max())
+
+ if region_start < echogram_start or region_end > echogram_end:
+ return (
+ False,
+ f"Validation error: Region {region_id} times "
+ f"({region_start} to {region_end}) fall outside the valid "
+ f"echogram range ({echogram_start} to {echogram_end}).",
+ )
+
+ return True, "Valid"
+
+ except Exception as e:
+ return False, f"Validation processing error: {e}"
+
+ # Store baseline for reset functionality
+ baseline_df = regions_df.copy()
+
+ sample_df = regions_df.copy()
+
+ poly_draw_stream = None
+ poly_edit_stream = None
+
+ time_dim = hv.Dimension("ping_time", label="Time", value_format=format_time)
+
+ background_cache = {}
+
+ if cache_backgrounds:
+ print("Pre-caching backgrounds...")
+
+ for region_id in sample_df["region_id"]:
+ current_df = sample_df[sample_df["region_id"] == region_id]
+ parsed = parse_polygons_from_df(current_df)
+
+ if parsed:
+ try:
+ time_values = np.array(parsed[0]["ping_time"])
+ start_t = pd.to_datetime(time_values.min(), unit="ms")
+ end_t = pd.to_datetime(time_values.max(), unit="ms")
+ buffer = pd.Timedelta(minutes=5)
+ ds_slice = ds.sel(ping_time=slice(start_t - buffer, end_t + buffer))
+
+ if len(ds_slice.ping_time) > 0:
+ ds_slice = ds_slice.assign_coords(
+ {
+ "ping_time": (
+ ("ping_time",),
+ ds_slice["ping_time"].data.astype("int64") // 10**6,
+ )
+ }
+ )
+ if "depth" in ds_slice.dims:
+ ds_slice = ds_slice.rename({"depth": "echo_range"})
+
+ background = ds_slice.eshader.echogram(
+ ds_slice.channel.values.tolist()
+ )()
+ background_cache[region_id] = background
+ print(f" Processed Region {region_id}")
+ except Exception:
+ pass
+
+ print(f"Cached {len(background_cache)} backgrounds successfully.")
+
+ region_ids = list(sample_df["region_id"])
+
+ mode_selector = pn.widgets.RadioButtonGroup(
+ name="Mode",
+ options=["Browse", "Edit"],
+ value="Browse",
+ button_type="primary",
+ width=200,
+ )
+
+ region_dropdown = pn.widgets.Select(
+ name="Select Region", options=region_ids, value=region_ids[0], width=200
+ )
+
+ prev_btn = pn.widgets.Button(name="Previous", button_type="light", width=95)
+ next_btn = pn.widgets.Button(name="Next", button_type="light", width=95)
+
+ reset_btn = pn.widgets.Button(name="Reset Region", button_type="warning", width=200)
+
+ apply_btn = pn.widgets.Button(name="Apply Edit", button_type="success", width=200)
+
+ export_btn = pn.widgets.Button(
+ name="Export to CSV", button_type="primary", width=200
+ )
+
+ load_btn = pn.widgets.FileInput(name="Load CSV", accept=".csv", width=200)
+
+ status = pn.pane.Markdown(
+ "**Browse Mode** - View Only",
+ styles={
+ "background": "#e3f2fd",
+ "padding": "10px 15px",
+ "border-radius": "8px",
+ "border-left": "4px solid #2196f3",
+ "margin": "10px 0",
+ },
+ )
+
+ actions_section = pn.Column(
+ pn.pane.Markdown(
+ "### Actions",
+ styles={"font-size": "14px", "color": "#666", "margin-bottom": "5px"},
+ ),
+ pn.pane.Markdown(
+ """
+ **Editing Workflow:**
+ 1. Edit polygon vertices
+ 2. Click "Apply Edit"
+ 3. Edit other regions as needed
+ 4. Click "Export to CSV" to save
+
+ ⚠️ *Changes are not saved to disk until exported!*
+ """,
+ styles={
+ "font-size": "11px",
+ "color": "#666",
+ "background": "#fff3cd",
+ "padding": "8px",
+ "border-radius": "5px",
+ "margin-bottom": "10px",
+ "border-left": "3px solid #ffc107",
+ },
+ ),
+ load_btn,
+ pn.Spacer(height=5),
+ reset_btn,
+ pn.Spacer(height=5),
+ apply_btn,
+ pn.Spacer(height=5),
+ export_btn,
+ visible=False,
+ )
+
+ def update_nav(event):
+ """Navigate between regions"""
+ idx = region_ids.index(region_dropdown.value)
+ if event.obj == next_btn:
+ region_dropdown.value = region_ids[(idx + 1) % len(region_ids)]
+ else:
+ region_dropdown.value = region_ids[(idx - 1) % len(region_ids)]
+
+ def on_mode_change(event):
+ """Toggle between Browse and Edit modes"""
+ is_edit = event.new == "Edit"
+ actions_section.visible = is_edit
+
+ if is_edit:
+ status.object = (
+ "**Edit Mode** - "
+ "**PolyDraw:** Click to add vertices, double-click last "
+ "vertex to finish. Drag to move entire polygon. "
+ "**PolyEdit:** Drag vertices to reposition, double-click "
+ "a vertex to delete it. "
+ "**Reset Region** restores the original."
+ )
+ status.styles = {
+ "background": "#e3f2fd",
+ "padding": "10px 15px",
+ "border-radius": "8px",
+ "border-left": "4px solid #2196f3",
+ "margin": "10px 0",
+ }
+ else:
+ status.object = "**Browse Mode** - View Only"
+ status.styles = {
+ "background": "#e3f2fd",
+ "padding": "10px 15px",
+ "border-radius": "8px",
+ "border-left": "4px solid #2196f3",
+ "margin": "10px 0",
+ }
+
+ def apply_edits(event):
+ """Apply edited polygon to DataFrame in memory"""
+ if poly_draw_stream is None:
+ status.object = "No edits to apply. Please ensure a region is selected."
+ return
+
+ try:
+ data = poly_draw_stream.data
+ if not data or len(data.get("xs", [])) == 0:
+ return
+
+ xs = data["xs"][0]
+ ys = data["ys"][0]
+ times_dt = pd.to_datetime(xs, unit="ms").values
+ depths_arr = np.array(ys, dtype=np.float64)
+
+ selected_id = region_dropdown.value
+ idx = sample_df[sample_df["region_id"] == selected_id].index[0]
+ sample_df.at[idx, "time"] = times_dt
+ sample_df.at[idx, "depth"] = depths_arr
+
+ status.object = (
+ f"**Applied!** Region {selected_id} updated. "
+ "Export to CSV to save to disk."
+ )
+ status.styles = {
+ "background": "#e8f5e9",
+ "padding": "10px 15px",
+ "border-radius": "8px",
+ "border-left": "4px solid #4caf50",
+ "margin": "10px 0",
+ }
+ except Exception as e:
+ status.object = (
+ f"Failed to apply edits to Region {selected_id}. "
+ f"Please check polygon format: {e}"
+ )
+ status.styles = {
+ "background": "#ffebee",
+ "padding": "10px 15px",
+ "border-radius": "8px",
+ "border-left": "4px solid #f44336",
+ "margin": "10px 0",
+ }
+
+ def reset_region(event):
+ """Reset current region to baseline version"""
+ try:
+ selected_id = region_dropdown.value
+
+ # Find baseline version
+ baseline_row = baseline_df[baseline_df["region_id"] == selected_id]
+
+ if baseline_row.empty:
+ status.object = f"No baseline found for Region {selected_id}."
+ status.styles = {
+ "background": "#ffebee",
+ "padding": "10px 15px",
+ "border-radius": "8px",
+ "border-left": "4px solid #f44336",
+ "margin": "10px 0",
+ }
+ return
+
+ # Reset to baseline
+ idx = sample_df[sample_df["region_id"] == selected_id].index[0]
+ sample_df.at[idx, "time"] = baseline_row.iloc[0]["time"].copy()
+ sample_df.at[idx, "depth"] = baseline_row.iloc[0]["depth"].copy()
+
+ # Force UI refresh
+ region_dropdown.param.trigger("value")
+
+ status.object = f"**Reset!** Region {selected_id} restored to original."
+ status.styles = {
+ "background": "#e8f5e9",
+ "padding": "10px 15px",
+ "border-radius": "8px",
+ "border-left": "4px solid #4caf50",
+ "margin": "10px 0",
+ }
+
+ print(f"Reset Region {selected_id} to baseline")
+
+ except Exception as e:
+ status.object = f"Reset failed: {e}"
+ status.styles = {
+ "background": "#ffebee",
+ "padding": "10px 15px",
+ "border-radius": "8px",
+ "border-left": "4px solid #f44336",
+ "margin": "10px 0",
+ }
+ print(f"Reset error: {e}")
+
+ def export_csv(event):
+ """Export edited regions to CSV"""
+ try:
+ from datetime import datetime
+
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ output_filename = f"edited_regions_{timestamp}.csv"
+
+ export_data = []
+
+ for _, row in sample_df.iterrows():
+ region_id = row["region_id"]
+ times = row["time"]
+ depths = row["depth"]
+
+ # Add single quotes around timestamps to avoid leading zero errors
+ time_str = "[" + ", ".join([f"'{str(t)}'" for t in times]) + "]"
+ depth_str = "[" + ", ".join([str(d) for d in depths]) + "]"
+
+ export_data.append(
+ {"region_id": region_id, "time": time_str, "depth": depth_str}
+ )
+
+ export_df = pd.DataFrame(export_data)
+ export_df.to_csv(output_filename, index=False)
+
+ status.object = (
+ f"**Exported Successfully!** All {len(sample_df)} regions "
+ f"saved to: {output_filename}"
+ )
+ print(f"CSV exported to: {output_filename}")
+
+ except Exception as e:
+ status.object = (
+ f"Export failed. Please ensure you have write permissions "
+ f"in this directory: {e}"
+ )
+ print(f"Export error: {e}")
+
+ def load_csv_file(event):
+ """Load regions from uploaded CSV"""
+ nonlocal sample_df, baseline_df
+
+ if load_btn.value is None:
+ status.object = "No file selected. Please choose a CSV file to load."
+ return
+
+ try:
+ import ast
+ import io
+
+ csv_data = io.BytesIO(load_btn.value)
+ loaded_df = pd.read_csv(csv_data)
+
+ required_columns = ["region_id", "time", "depth"]
+ if not all(col in loaded_df.columns for col in required_columns):
+ status.object = (
+ f"Invalid CSV format. Missing required columns. "
+ f"Required: {required_columns}"
+ )
+ return
+
+ parsed_times = []
+ parsed_depths = []
+
+ for idx, row in loaded_df.iterrows():
+ try:
+ # Try ast.literal_eval first (works for CSVs with quotes)
+ time_list = ast.literal_eval(str(row["time"]))
+ depth_list = ast.literal_eval(str(row["depth"]))
+
+ parsed_times.append(np.array(time_list, dtype="datetime64[ns]"))
+ parsed_depths.append(np.array(depth_list, dtype=np.float64))
+
+ except Exception:
+ # FALLBACK: For old CSVs without quotes, parse manually
+ try:
+ time_str = str(row["time"]).strip("[]")
+ time_list = [t.strip(" '\"") for t in time_str.split(",")]
+ parsed_times.append(np.array(time_list, dtype="datetime64[ns]"))
+
+ depth_str = str(row["depth"]).strip("[]")
+ depth_list = [float(d.strip()) for d in depth_str.split(",")]
+ parsed_depths.append(np.array(depth_list, dtype=np.float64))
+ except Exception as fallback_e:
+ status.object = (
+ f"Parse error in row {idx}. Please check data format "
+ f"matches Echoregions standard: {fallback_e}"
+ )
+ return
+
+ # Apply parsed arrays all at once
+ loaded_df["time"] = parsed_times
+ loaded_df["depth"] = parsed_depths
+
+ is_valid, message = validate_loaded_regions(loaded_df, ds)
+ if not is_valid:
+ status.object = f"**Validation Failed:** {message}"
+ status.styles = {
+ "background": "#ffebee",
+ "padding": "10px 15px",
+ "border-radius": "8px",
+ "border-left": "4px solid #f44336",
+ "margin": "10px 0",
+ }
+ return
+
+ sample_df = loaded_df.copy()
+
+ # Update baseline to loaded CSV (new reset point)
+ baseline_df = loaded_df.copy()
+
+ new_region_ids = list(sample_df["region_id"])
+ region_dropdown.options = new_region_ids
+ region_dropdown.value = new_region_ids[0]
+
+ status.object = (
+ f"**Loaded Successfully!** {len(loaded_df)} regions imported from CSV."
+ )
+ status.styles = {
+ "background": "#e8f5e9",
+ "padding": "10px 15px",
+ "border-radius": "8px",
+ "border-left": "4px solid #4caf50",
+ "margin": "10px 0",
+ }
+ print(f"Loaded {len(loaded_df)} regions successfully.")
+
+ except Exception as e:
+ status.object = f"Failed to load file: {e}"
+ print(f"Load error: {e}")
+
+ @pn.depends(region_dropdown.param.value, mode_selector.param.value)
+ def get_region_view(selected_id, mode):
+ """Generate region view with cached backgrounds"""
+ nonlocal poly_draw_stream, poly_edit_stream
+
+ current_df = sample_df[sample_df["region_id"] == selected_id]
+ if current_df.empty:
+ return pn.pane.Markdown("No data available for this region.")
+
+ parsed = parse_polygons_from_df(current_df)
+ if not parsed:
+ return pn.pane.Markdown("No polygon data for this region.")
+
+ # Check if background is cached, if not, generate it on-demand
+ if selected_id not in background_cache:
+ try:
+ time_values = np.array(parsed[0]["ping_time"])
+ start_t = pd.to_datetime(time_values.min(), unit="ms")
+ end_t = pd.to_datetime(time_values.max(), unit="ms")
+ buffer = pd.Timedelta(minutes=5)
+ ds_slice = ds.sel(ping_time=slice(start_t - buffer, end_t + buffer))
+
+ if len(ds_slice.ping_time) > 0:
+ ds_slice = ds_slice.assign_coords(
+ {
+ "ping_time": (
+ ("ping_time",),
+ ds_slice["ping_time"].data.astype("int64") // 10**6,
+ )
+ }
+ )
+ if "depth" in ds_slice.dims:
+ ds_slice = ds_slice.rename({"depth": "echo_range"})
+
+ background = ds_slice.eshader.echogram(
+ ds_slice.channel.values.tolist()
+ )()
+ background_cache[selected_id] = background
+ print(f"Cached background for Region {selected_id}")
+ else:
+ return pn.pane.Markdown("No echogram data in this time range.")
+ except Exception as e:
+ return pn.pane.Markdown(f"Error generating background: {e}")
+
+ # Retrieve cached background (fast)
+ background = background_cache[selected_id]
+
+ # Generate polygon (only happens on first view of this region+mode)
+ poly = hv.Polygons(
+ [
+ {
+ "ping_time": r["ping_time"],
+ "echo_range": r["depth"],
+ "region_id": r["region_id"],
+ }
+ for r in parsed
+ ],
+ kdims=[time_dim, hv.Dimension("echo_range", label="Depth (m)")],
+ vdims=["region_id"],
+ ).opts(color="red", fill_alpha=0.3, line_width=2)
+
+ if mode == "Edit":
+ # Attach streams - they automatically add tools when source=poly is set
+ poly_draw_stream = PolyDraw(
+ source=poly,
+ drag=True,
+ num_objects=1,
+ show_vertices=True,
+ vertex_style={"size": 10, "color": "red", "fill_alpha": 0.8},
+ )
+
+ poly_edit_stream = PolyEdit(
+ source=poly,
+ vertex_style={"size": 10, "color": "orange", "fill_alpha": 0.8},
+ shared=True,
+ )
+
+ # Create composed plot (streams auto-attach in Edit mode)
+ plot = background * poly
+ return plot
+
+ prev_btn.on_click(update_nav)
+ next_btn.on_click(update_nav)
+ mode_selector.param.watch(on_mode_change, "value")
+ reset_btn.on_click(reset_region)
+ apply_btn.on_click(apply_edits)
+ export_btn.on_click(export_csv)
+ load_btn.param.watch(load_csv_file, "value")
+
+ sidebar = pn.Column(
+ pn.pane.Markdown(
+ "## Controls", styles={"color": "#1976d2", "margin-bottom": "15px"}
+ ),
+ pn.pane.Markdown(
+ "### Mode",
+ styles={"font-size": "14px", "color": "#666", "margin-bottom": "5px"},
+ ),
+ mode_selector,
+ pn.Spacer(height=20),
+ pn.pane.Markdown(
+ "### Regions",
+ styles={"font-size": "14px", "color": "#666", "margin-bottom": "5px"},
+ ),
+ region_dropdown,
+ pn.Spacer(height=5),
+ pn.Row(prev_btn, next_btn),
+ pn.Spacer(height=20),
+ actions_section,
+ styles={
+ "background": "#f5f5f5",
+ "padding": "20px",
+ "border-radius": "8px",
+ "box-shadow": "2px 0 5px rgba(0,0,0,0.1)",
+ },
+ width=250,
+ )
+
+ main_content = pn.Column(
+ pn.pane.Markdown(
+ "# Hake School Region Browser",
+ styles={"color": "#1976d2", "margin-bottom": "5px"},
+ ),
+ pn.pane.Markdown(
+ "*Interactive region browser with Browse/Edit modes*",
+ styles={"color": "#666", "font-size": "14px", "margin-bottom": "15px"},
+ ),
+ status,
+ get_region_view,
+ styles={"padding": "20px"},
+ sizing_mode="stretch_width",
+ )
+
+ layout = pn.Row(
+ sidebar,
+ main_content,
+ styles={"background": "white"},
+ sizing_mode="stretch_width",
+ )
+
+ return layout