The map position is sometimes reset to its initial value when I add programmatically a trace to the map.
If you pan and and zoom and want to add a trace at that location than the trace is added at that location but sometimes the view is immediately reset to its initial position.
It happens in the code below (python 3.12):
# Demo app to show the issue of map repositioning to initial position and zoom level
# after adding a trace
import abc
import logging
import math
import traceback
from collections.abc import Callable
from functools import wraps
import panel as pn
import plotly.express as px # type: ignore
import plotly.graph_objects as go # type: ignore
from typing import Any
pn.extension('plotly')
logger = logging.getLogger(__name__)
# Logging utilities
def LogException(err: BaseException) -> None:
logger.error('Exception occured')
for line in traceback.format_exception(err):
logger.error(line[:-1])
def ExceptionLoggingDec[**P, R](func: Callable[P, R]) -> Callable[P, R | None]:
# Preserve function metadata with wraps
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
result = None
try:
result = func(*args, **kwargs)
except BaseException as err:
LogException(err)
return result
return wrapper
# Logging classes
class LoggingControlInterface(metaclass=abc.ABCMeta):
@classmethod
def __subclasshook__(cls: Any, subclass: Any) -> bool:
return (hasattr(subclass, 'Add') and
callable(subclass.Add))
@abc.abstractmethod
def Add(self, message: str) -> None:
"""Add message to output"""
raise NotImplementedError
class LogHandler(logging.Handler):
def __init__(self, output_ctrl: LoggingControlInterface) -> None:
super().__init__()
self._output_ctrl = output_ctrl
def emit(self, record: logging.LogRecord) -> None:
self._output_ctrl.Add(super().format(record))
class LoggingControl(LoggingControlInterface):
ROW_COUNT:int = 20
MESSAGE_COUNT:int = 1000
def __init__(self) -> None:
self._handlers = [LogHandler(self)]
logging.basicConfig(handlers=self._handlers,
level=logging.INFO,
format='%(message)s',
force=True)
self._messages: list[str] = []
# Build ui
self._log_window = pn.widgets.TextAreaInput(cols=1,
rows=self.ROW_COUNT,
disabled=True,
resizable='width',
styles={'width': '100%', 'max-width': 'stretch'})
self._ui = pn.Column(self._log_window,
styles={'background': 'WhiteSmoke', 'border': '1px solid black', 'width': '100%', 'max-width': 'stretch'})
self.Add("Logging initialised")
def GetUi(self) -> pn.Column:
return self._ui
# From LoggingControlInterface
def Add(self, message: str) -> None:
self._messages.insert(0, message)
self._messages = self._messages[:self.MESSAGE_COUNT]
txt = ''
for msg in self._messages:
if txt:
txt += '\n'
txt += msg
print(txt)
self._log_window.value = txt
class MapControl():
# (lon, lat) (easting, northing)
AMSTERDAM_CENTER = {'lon': 4.897070, 'lat': 52.377956}
INITIAL_ZOOM_LEVEL = 15
def __init__(self) -> None:
self._trace_id = 0
self._fig = px.scatter_map(lat=[],
lon=[],
center=self.AMSTERDAM_CENTER,
zoom=self.INITIAL_ZOOM_LEVEL,
width=700,
height=700,
map_style='open-street-map')
plotly_pane = pn.pane.Plotly(self._fig, height=700, width=700)
add_trace_btn = pn.widgets.Button(name='Add rectange trace', button_type='primary')
add_trace_btn.on_click(lambda button, self=self: self._AddRectangleTrace()) # type: ignore
self._ui = pn.Column(plotly_pane, add_trace_btn)
def GetUi(self) -> None:
return self._ui
@ExceptionLoggingDec
def _AddRectangleTrace(self) -> None:
global trace_id
center_current = self._fig.layout.map.center
zoom_current = self._fig.layout.map.zoom
logger.info(f'Before add_trace(), center: (lon: {center_current['lon']}, lat: {center_current['lat']}), zoom: {zoom_current}, id: {self._trace_id}')
lon_c = center_current['lon']
lat_c = center_current['lat']
half_width = 0.00369
half_height = 0.002275
lon_left = lon_c - half_width
lon_right = lon_c + half_width
lat_top = lat_c + half_height
lat_bot = lat_c - half_height
square_lons = [lon_left, lon_right, lon_right, lon_left, lon_left]
square_lats = [lat_bot, lat_bot, lat_top, lat_top, lat_bot]
#self._fig.update_layout(map_center=center_current, map_zoom=zoom_current, overwrite=True)
self._fig.add_trace(go.Scattermap(name='Trace_' + str(self._trace_id),
mode = "lines+markers",
lat = square_lats,
lon = square_lons,
hovertemplate=('%{customdata}' +'<extra></extra>'),
marker=go.scattermap.Marker(symbol='circle', color='red', size=10),
line_width=1,
line_color='red'))
center_current = self._fig.layout.map.center
zoom_current = self._fig.layout.map.zoom
logger.info(f'After add_trace(), center: (lon: {center_current['lon']}, lat: {center_current['lat']}), zoom: {zoom_current}, id: {self._trace_id}')
self._fig.update_layout(map_center=center_current, map_zoom=zoom_current, overwrite=True)
self._trace_id = self._trace_id + 1
logging_ctrl = LoggingControl()
map_ctrl = MapControl()
ui = pn.Column(map_ctrl.GetUi(), logging_ctrl.GetUi())
ui.servable()
Note: If you are trying to reproduce this:
It is does not happen every time.
Is this a bug or is there a flag to avoid this effect ?
Note 2: Double clicking will always return the map to its initial position. That is by design.