-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
pydeck and @deck.gl/jupyter-widget: Add support for dynamic deck.gl custom Layer registration #4233
Conversation
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.
@ajdubersteing This looks great, thank you very much for putting this in place.
I was assuming that that we would only support adding layers at initialization time, but this actually allows additional layer modules to be imported dynamically at run time, which certainly seems very flexible!
This seems to take care of the javascript side. Is there a corresponding "command" or function to be added on the pydeck/python side?
modules/jupyter-widget/src/widget.js
Outdated
@@ -125,9 +126,21 @@ export class DeckGLView extends DOMWidgetView { | |||
|
|||
this.model.on('change:json_input', this.valueChanged.bind(this), this); | |||
this.model.on('change:data_buffer', this.dataBufferChanged.bind(this), this); | |||
this.model.on('change:custom_layers', this.addNewCustomLayers.bind(this), this); |
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.
change:custom_layers
- We only support layers right now, not other types of configuration objects (e.g. constants).
I think it is fine to only support layers for now, though I wouldn't mind if we designed it with more generality/expandability in mind, e.g. change:add-configuration
, which takes a JSON configuration object, and renamed the method to this.add<Custom>JSONConfiguration()
.
(Completely generalizing things would seem to require conventions for how to bundle a module so the various types of exports are discoverable on dynamic import, and I don't think that would need to be solved in this PR, i.e. for now it is fine to only send configuration objects with a classes
field.)
// Opinionated choice, requires that the user load only one layer at a time | ||
// and that layer must be the sole default export of the library | ||
// TODO better choice here? | ||
jsonConverter.configuration.classes[className] = window[className].default; |
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.
You are updating an existing configuration. This code is a little hacky, I don't think it would be unreasonable to provide a JSONConverter.mergeConfiguration(newConfiguration)
API for merging in a new configuration object.
modules/jupyter-widget/src/widget.js
Outdated
this.dataBufferChanged(); | ||
} | ||
|
||
addNewCustomLayers() { | ||
if (this.model.get('custom_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.
Can we use the same naming as JSON config objects?
this.model.get('classes`) ...
// Opinionated choice, requires that the user load only one layer at a time | ||
// and that layer must be the sole default export of the library | ||
// TODO better choice here? | ||
const classConstructor = window[className].default; |
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.
How is the user supposed to bundle their code? Add an example/test.
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 test is addressed in #4267 and I'll add documentation/an example directory elsewhere
@@ -41,8 +42,13 @@ const jsonConverter = new deck.JSONConverter({ | |||
configuration: jsonConverterConfiguration | |||
}); | |||
|
|||
export function updateDeck(inputJSON, deckgl) { | |||
const results = jsonConverter.convert(inputJSON); | |||
export function updateClasses({className, resourceUri, onComplete}) { |
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.
Why do we need a function that directly calls another? Where is onComplete
used?
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.
onComplete is used in the test associated with this function
3b4dbea
to
0265914
Compare
@@ -9,4 +9,4 @@ | |||
module_name = "@deck.gl/jupyter-widget" | |||
# module_version is the current version of the module of the JS portion of the widget | |||
# It appears to be important only for JupyterLab and ignored for Jupyter Notebooks | |||
module_version = "^8.0.0" | |||
module_version = "8.1.0-alpha.1" |
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.
Can we not hardcode any version?
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.
Handled in a separate PR, this is only for development purposes, I'll change it back
}); | ||
} | ||
|
||
function loadExternalClasses({libraryName, resourceUri, onComplete}, loadResource = loadScript) { |
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.
Add a comment: loadResource
is for testing only
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.
Nit: I personally try to avoid mixing positional and named parameters.
modules/jupyter-widget/src/widget.js
Outdated
const customLibraries = this.model.get('classes'); | ||
for (const obj of customLibraries) { | ||
// obj is an object of {libraryName: <string>, url: <string>} | ||
const [libraryName, resourceUri] = Object.entries(obj)[0]; |
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.
obj.libraryName
,
obj.url
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.
They're arriving as a list of dictionaries from the server in something like this format:
[{"libraryName1": "url1"}, {"libraryName2": "url2"}]
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.
Don’t you have control of the format on the server 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.
I do but I'm personally partial to the more concise form of
[{"libraryName1": "url1"}, {"libraryName2": "url2"}]
instead of
[{"libraryName": "libraryName1", "resourceUri": "url1"}, {"libraryName": "libraryName2", "resourceUri": "url2"}]
It's anyone's choice, however–happy to switch it over.
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.
Think of each item as an entry in a database. Predictable data structured makes your code a lot more readable. Using the key name as the library name is implicit, and requires additional documentation in the source. Explicitly calling out libraryName
leaves no ambiguity.
If you insist on using keys as library names, which is fine, then there's no reason to have an array. All entries can be merged into one dictionary. Object.entries(obj)[0]
is a bad pattern because you are relying on the existence and uniqueness of the object's content. It will easily crash if the input is malformed.
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.
See inline comment, this seems to only support importing a single layer per module, (and no other types of symbols, such as enums etc).
From our use case, even with this limitation, this is a big step up. That said we will probably soon want to import custom modules with multiple layers, and perhaps also enums, constants etc.
For longer-term, we also have requests around being able to import alternative base map modules into pydeck, and doing that using a generalization of this dynamic injection mechanism could be very elegant. Of course we would need to solve a number of questions around how those are integrated (a discussion partially started in your pydeck multiview PR).
For a possible follow-up PR: long-term would it make sense to route all deck.gl layer module loading through this function, not just custom modules? That would make this code into a primary part of the library, instead of a "fragile" edge case.
}); | ||
} | ||
|
||
function loadExternalClasses({libraryName, resourceUri, onComplete}, loadResource = loadScript) { |
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.
Nit: I personally try to avoid mixing positional and named parameters.
bindings/pydeck/pydeck/settings.py
Outdated
one could load it into pydeck by doing the following: | ||
|
||
``` | ||
pydeck.settings.custom_classes = [{'TagmapLayer': 'https://demourl.libpath/bundle.js'}] |
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 suppose that we since we haven't yet implemented the configuration "back-channel" message back to Python (which would let Python about what JS classes are available) the user has to duplicate that information here.
It would be valuable to be able to import multiple classes per module (in fact it would be valuable to be able to import also constants etc).
Therefore I am wondering if a class-name-to-url
scheme is sufficient, or if it should be
pydeck.settings.custom_classes = {
'https://demourl.libpath/bundle1.js': [
classes: ['TagmapLayer', 'Layer2'],
enums: ...
],
'https://demourl.libpath/bundle2.js': [
classes: ['MyCustomLayer'],
]
};
Again, ideally long-term we should have the back-channel message and not require listing the contents of the packages on 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.
The latest implementation of this allows for multiple classes. I'll be updating the docstring here to read {"tagmapLibrary": ...url...}
instead.
@@ -10,19 +10,19 @@ import * as deck from './deck-bundle'; | |||
|
|||
import GL from '@luma.gl/constants'; | |||
|
|||
function extractClasses() { | |||
function extractClasses(library) { |
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.
Please add a more comprehensive header describing what this function does.
// Get classes for registration from standalone deck.gl
this makes it sound like this is for the top-level deck.gl module only
19b6855
to
7d3449b
Compare
075ce33
to
89090a5
Compare
Will merge after #4267 is merged |
For #4103
Background
Change List