-
Notifications
You must be signed in to change notification settings - Fork 39
feat: Table of contents #836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Thanks! I'd like to be more attentive to PRs than before.
Perhaps we could make this a method on the It's a code smell to me to have both I think we should also brainstorm more on what this should be called. Instead of TOC maybe Widget and Control are becoming confusing terminology. A lot of JS web maps like Mapbox GL JS called "Control" the extra map elements, like distance/compass. Deck.gl calls those Widgets. While in Python, ipywidgets-based widgets have the opposite meaning of "widget". In particular, what do we call something appearing on the map itself (from JS) vs something separately in the notebook page that communicates with the map element. We should solidify this terminology.
We should decide whether a layer's title is a property of the map or of the layer itself. In particular, a layer can be rendered in multiple maps. Should the layer always have the same name? It's more flexible to allow each instance of the layer to have a different name. In that case, it wouldn't be an attribute of the layer but instead the Additionally, we should default the title of a layer to the name of the class. So the default title for a polygon layer would be
See inline note.
Perhaps we should remove color options from this PR and work on that in a follow up PR? I figure that most of the time a user will add some sort of colormap? Or, if a layer is using a custom colormap (anything other than a scalar color), then we can just print "custom" in the legend for that layer. |
In the original code I wrote I had one function and it had a parameter for I don't have any strong feeling either way on the name of it, I do like layer control, but your idea of why to not use control is valid. In my experiences making map products and geospatial visuals I've not needed to make one layer and use it in different maps with a different name unless I also needed to change other parts of the layer as well (colors, line widths, popups something like that), so in that case you'd need to make a new layer anyway. default title, that's a good idea! I think I like the idea of "custom" or something along those lines for the colors that aren't just one color (I'd rather not throw that part out totally for now, because what I have right now works great for single colors), I'll try to carve out some time this weekend and see what I can come up with. |
…ap, and protected against RGBA values and implemented handling of properties of arrays as "Custom" things that the layer_control cannot change.
I've got a And I realized I wasn't understanding properly what ruff was telling me about the boolean in the function, it just wanted me to make it a non-positional argument, so now there's just one method on the map to make the layer control with an argument for |
Ruff only complains about positional boolean arguments. If you make it a keyword-only argument then ruff won't complain.
👍 |
yeah, once I reread it and looked at the example, it made a lot more sense! that's how I've got it now 😎 |
@ATL2001 did you plan more changes here before asking for my review? Or is this ready for an in-depth review? Should we try to get it in for the next release? |
I actually do want to make one more small tweak to the widget I have linked color accessor (I'd like to add another widget to control the alpha similar to how it works in the panel app demo) I'll see if I can wire it up tonight |
I've still got a kink to work out, it'll likely be the weekend before I can iron it out 😞 |
No rush, we can make another release then. I think I want to release #846 soon, and along with the python 3.9 deprecation it needs a minor release. In contrast to Rust, it's relatively low-downside to making minor releases, so we can make more releases soon in the future. |
… into table_of_contents
… into table_of_contents
ok, ready for review now! 👍 |
lonboard/_layer.py
Outdated
@@ -247,7 +247,7 @@ def _add_extension_traits(self, extensions: Sequence[BaseExtension]) -> None: | |||
|
|||
highlight_color = VariableLengthTuple( | |||
t.Int(), | |||
default_value=None, | |||
default_value=[0, 0, 128, 128], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is unrelated?
There's a question of where defaults should live: in JS code or in Python code. In this case, we don't override the upstream deck.gl default, so leaving this as None
just means "refer to the underlying deck.gl default". I think that might be a better option than copying all the default values into Python code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did set that intentionally, because it was giving me trouble when it was None, but looking at it right now with eyes from a different day, I think I may be able to change some other stuff in the _make_color_picker_widget
to make it work with it not being set on the base layer. I'll see what I can do there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh I think I may have actually come across a bug in the existing code when I was doing this and thought it was something I was doing. when I try to access the highlight_color property of a layer with None as the default value with
boundary_layer.highlight_color
I'm getting a Trait Error:
TraitError: The 'highlight_color' trait of a PolygonLayer instance must be of length 3 <= L <= 4, but a value of [] was specified.
can you re-create that error on your end?
lonboard/_layer.py
Outdated
@@ -281,6 +281,13 @@ def _add_extension_traits(self, extensions: Sequence[BaseExtension]) -> None: | |||
for an example. | |||
""" | |||
|
|||
title = t.CUnicode("Layer", allow_none=False).tag(sync=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
title = t.CUnicode("Layer", allow_none=False).tag(sync=True) | |
title = t.Unicode("Layer", allow_none=False).tag(sync=True) |
I can't remember but I think the CUnicode
trait is legacy from the Python 2/3 transition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's the casting variant of the Unicode trait, I thought it would be safer in case someone set the layer's title to 1 instead of "1" then it would automatically be changed to "1". I'm happy to change it if you'd like, but that was the reason I used CUnicode
lonboard/controls.py
Outdated
def _hex2rgb(hex_color: str) -> list[int]: | ||
"""Convert a hex color code to RGB.""" | ||
hex_color = hex_color.lstrip("#") | ||
rgb_color = [] | ||
for i in (0, 2, 4): | ||
rgb_color.append(int(hex_color[i : i + 2], 16)) | ||
return rgb_color |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have a hex -> rgb converter that we vendored from matplotlib:
def _to_rgba_no_colorcycle(c, alpha: float | None = None) -> Tuple[float, ...]: |
examples/layer_control.ipynb
Outdated
" \"https://naciscdn.org/naturalearth/10m/cultural/ne_10m_roads_north_america.zip\",\n", | ||
" ),\n", | ||
" (\n", | ||
" \"geoBoundariesCGAZ_ADM1.parquet\",\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trying to render the geo boundaries file crashes VSCode for me and doesn't display well in Jupyter Lab. We should find a smaller file to work with, or just use the other two layers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, I'm sorry! I'll do something to limit what gets displayed
lonboard/_layer.py
Outdated
@@ -581,6 +581,12 @@ def _weighted_centroid(self) -> WeightedCentroid: | |||
# image should represent. | |||
return WeightedCentroid(x=center_x, y=center_y, num_items=100) | |||
|
|||
title = t.CUnicode("BitmapLayer", allow_none=False).tag(sync=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love the idea of repeating each layer name another time. Ideally we could use self.cls.__name__
here, but we don't have access to cls
from this class attribute. Perhaps it would be better to leave this as None
and let the default get set from the JS side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's a good idea, I hadn't considered setting it on the JS side, I'll see if i can do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I failed miserably on the js side to set it,
I moved the title back to the BaseLayer and then in the init method I set it to check if the title is None, and if so use the _layer_type to give it a default title. if that's not a workable solution I'll need a hand on the JS end :(
"""Make layer control box for the layers in the Map. | ||
|
||
Args: | ||
include_settings: If `False` The controler will only contain a checkbox for each layer, which controls layer visibility in the Lonboard map. If `True` The controller will also have a settings button, which when clicked will expose properties for the layer which can be changed. If a layer's property is None when the layer is added to the control, a widget for controling that property will not be created. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The settings button is pretty small, and the drop down isn't expanded by default; is there a reason not to show the settings button all the time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking that the layer control could be used in some applications where the designer of the app would want the user to be able to set the visibility of the layer, but not have access to control the other properties. In that case they would set include_settings=False
. If they wanted the user to be able to change the other properties then they set include_settings=True
and then the setting gear is there, so they can access the layer properties.
my reason for having it as a drop down is just to save space and make it less intrusive
lonboard/controls.py
Outdated
def _link_rgba_and_alpha_traits( | ||
rgb_object: Any, | ||
rgb_trait_name: str, | ||
alpha_object: Any, | ||
alpha_trait_name: str, | ||
) -> None: | ||
"""Make links between two objects/traits that hold RBGA and an alpha int.""" | ||
|
||
def handle_alpha_color_change(change: traitlets.utils.bunch.Bunch) -> None: | ||
new_alpha = 255 if len(change.get("new")) == 3 else change.get("new")[3] | ||
alpha_object.set_trait(alpha_trait_name, new_alpha) | ||
|
||
rgb_object.observe(handle_alpha_color_change, rgb_trait_name, "change") | ||
|
||
def handle_alpha_change(change: traitlets.utils.bunch.Bunch) -> None: | ||
new_alpha = change.get("new") | ||
rgb_value = getattr(rgb_object, rgb_trait_name).copy() | ||
if len(rgb_value) == 3: | ||
rgb_value.append(new_alpha) | ||
else: | ||
rgb_value[3] = new_alpha | ||
rgb_object.set_trait(rgb_trait_name, rgb_value) | ||
|
||
alpha_object.observe(handle_alpha_change, alpha_trait_name, "change") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like you're manually creating the RGBA color from the opacity
field of the control? We don't need to do that; it's easier to pass directly to the opacity
field of the layer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh wow, you are spot on there!, I spent a lot of time wiring that up, but you're totally correct, it's entirely unnecessary, i'll rip that out 😆
lonboard/controls.py
Outdated
indent=False, | ||
) | ||
visibility_w.layout = ipywidgets.Layout(width="196px") | ||
ipywidgets.dlink((layer, "title"), (visibility_w, "description")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to make the name in the layer selector editable? Then you wouldn't have to choose a new name from Python.
… into table_of_contents
This isn't yet a fully working solution, but figured it would be good to put it out there to get feedback on what it does so far
I've resurrected the old code I wrote to make the TOC, and fixed it up to work with the new linting rules.
I've added two new public functions to lonboard.controls
make_toc
andmake_toc_with_settings
(I'm very open to renaming those if you have better ideas) and a lot of other private helper functions that users won't need.The
make_toc
function will make a simple table of contents that can control layer visibilityThe
make_toc_with_settings
function will make a table of contents which can control the layer visibility, and also has a button, which when pressed, will allow changes to be made to the layer's properties. The layer traits that are None when the TOC is created are not included in the settings because I don't know of a way to make the ipywidgets widgets work with None (I don't believe it's possible, but would welcome being wrong about that)I've also added a new
title
trait toBaseLayer
so the layer's title can be shown in the TOC which is defaulted to 'layer'Additionally, I set the default value on
BaseLayer.highlight_color
to be [0, 0, 128, 128] to match what is in the docstring for the default value. (when the make_toc_with_settings function was trying to access the trait without the default it was throwing an error)what's not working:
The current implementation blows up when you create a TOC with settings and a layer uses a continuous colormap (like what is in the examples/map_challenge 6-asia notebook) (and I assume the same will happen for a categorical colormap) I haven't thought much about how to handle those situations yet, but if you have any ideas, I'm all ears.