Bokeh Custom Tool in JavaScript throws error - bokeh

I am currently working on a bokeh application with continuous AND categorical data in the same Dataframe. Since the PointDrawTool of bokeh only comes with support for continuous data, I need to write a custom PointDrawTool myself. I followed the custom tools tutorial (https://docs.bokeh.org/en/2.4.1/docs/user_guide/extensions_gallery/tool.html) and changed the TypeScript code. I just copied the source code of the PointDrawTool (https://github.com/bokeh/bokeh/blob/branch-3.0/bokehjs/src/lib/models/tools/edit/point_draw_tool.ts) and fixed all the imports so that I do not run into any TypeScript Errors anymore. However, I always get a weird console error when loading the page. Additionally, the page remains completely blank and none of my widgets are showing.
Error in console: "Failed to repull session TypeError: this.properties[e] is undefined"
Code in draw_tool.py:
from bokeh.core.properties import Instance
from bokeh.io import output_file, show
from bokeh.models import ColumnDataSource, Tool
from bokeh.plotting import figure
from bokeh.util.compiler import TypeScript
output_file('tool.html')
TS_CODE = """
import {Keys} from "core/dom"
import {PanEvent, TapEvent, KeyEvent} from "core/ui_events"
import * as p from "core/properties"
import {GlyphRenderer} from "models/renderers/glyph_renderer"
import {EditTool, EditToolView, HasXYGlyph} from "models/tools/edit/edit_tool"
import {tool_icon_point_draw} from "styles/icons.css"
export class CustomDrawToolView extends EditToolView {
override model: CustomDrawTool
override _tap(ev: TapEvent): void {
const renderers = this._select_event(ev, this._select_mode(ev), this.model.renderers)
if (renderers.length || !this.model.add) {
return
}
const renderer = this.model.renderers[0]
const point = this._map_drag(ev.sx, ev.sy, renderer)
if (point == null)
return
// Type once dataspecs are typed
const glyph: any = renderer.glyph
const cds = renderer.data_source
const [xkey, ykey] = [glyph.x.field, glyph.y.field]
const [x, y] = point
this._pop_glyphs(cds, this.model.num_objects)
if (xkey) cds.get_array(xkey).push(x)
if (ykey) cds.get_array(ykey).push(y)
this._pad_empty_columns(cds, [xkey, ykey])
const {data} = cds
cds.setv({data}, {check_eq: false}) // XXX: inplace updates
}
override _keyup(ev: KeyEvent): void {
if (!this.model.active || !this._mouse_in_frame)
return
for (const renderer of this.model.renderers) {
if (ev.keyCode === Keys.Backspace) {
this._delete_selected(renderer)
} else if (ev.keyCode == Keys.Esc) {
renderer.data_source.selection_manager.clear()
}
}
}
override _pan_start(ev: PanEvent): void {
if (!this.model.drag)
return
this._select_event(ev, "append", this.model.renderers)
this._basepoint = [ev.sx, ev.sy]
}
override _pan(ev: PanEvent): void {
if (!this.model.drag || this._basepoint == null)
return
this._drag_points(ev, this.model.renderers)
}
override _pan_end(ev: PanEvent): void {
if (!this.model.drag)
return
this._pan(ev)
for (const renderer of this.model.renderers)
this._emit_cds_changes(renderer.data_source, false, true, true)
this._basepoint = null
}
}
export namespace CustomDrawTool {
export type Attrs = p.AttrsOf<Props>
export type Props = EditTool.Props & {
add: p.Property<boolean>
drag: p.Property<boolean>
num_objects: p.Property<number>
renderers: p.Property<(GlyphRenderer & HasXYGlyph)[]>
}
}
export interface CustomDrawTool extends CustomDrawTool.Attrs {}
export class CustomDrawTool extends EditTool {
override properties: CustomDrawTool.Props
override __view_type__: CustomDrawToolView
override renderers: (GlyphRenderer & HasXYGlyph)[]
constructor(attrs?: Partial<CustomDrawTool.Attrs>) {
super(attrs)
}
static {
this.prototype.default_view = CustomDrawToolView
this.define<CustomDrawTool.Props>(({Boolean, Int}) => ({
add: [ Boolean, true ],
drag: [ Boolean, true ],
num_objects: [ Int, 0 ],
}))
}
override tool_name = "Point Draw Tool XX"
tool_icon = tool_icon_point_draw
override event_type = ["tap" as "tap", "pan" as "pan", "move" as "move"]
override default_order = 2
}
"""
class CustomDrawTool(Tool):
__implementation__ = TypeScript(TS_CODE)
source = Instance(ColumnDataSource)
Usage in main.py
from custom_tools.draw_tool import CustomDrawTool
tools = config.TOOLS + [CustomDrawTool(source=data_model.data_X)]
p = figure(height=config.PLOT_HEIGHT, width=config.PLOT_WIDTH, tools=tools, output_backend="webgl",
**kw_figure)
Note: data_model.data_X is just a ColumnDataSource.
Hopefully someone can give me a hint on how to fix the problem and continue with writing my own Tool.
Thanks & best regards

Related

Value of type '(params: any) => CSSProperties' has no properties in common with type 'Properties<string | number>'. Did you mean to call it?

Why does this property in react CSS not work if it is of type CSSProperties? How can I get it to work with Properties<string | number> ?
export const fields: GridFieldsConfiguration[] = [
{
...defaultColDefs,
field: 'amInitials',
displayNameRule: 'Asset Manager',
flex: 1.1,
minWidth: 75,
cellStyle: (params: any): any => {
getCellStyle(params, 'amInactive')
}
}
];
const isDisabledbStyle = {
color: '#FF0000'
};
const getCellStyle = ((params: any, inactiveCol: string): CSSProperties => {
console.log(params);
if (params?.api?.getValue(inactiveCol, params.node) === true) {
return isDisabledbStyle;
} else {
return isDisabledbStyle;
}
}
);
Here are the types. cellStyle comes from CSSProperties which is an extension of CSS.Properties<string | number>.
export interface GridFieldConfiguration extends FieldConfiguration {
cellStyle?: CSSProperties;
}
export interface CSSProperties extends CSS.Properties<string | number> {
/**
* The index signature was removed to enable closed typing for style
* using CSSType. You're able to use type assertion or module augmentation
* to add properties or an index signature of your own.
*
* For examples and more information, visit:
* https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
*/
}
Here is Properties
export interface Properties<TLength = string | 0> extends StandardProperties<TLength>, VendorProperties<TLength>, ObsoleteProperties<TLength>, SvgProperties<TLength> {}
Sounds like you are using AG Grid, and trying to configure a cellStyleFunc for the cellStyle option of column definitions?
As shown in the linked documentation, it is indeed possible to provide a function that takes a params argument.
But it looks like in your case, you have an intermediate GridFieldsConfiguration custom type, that expects cellStyle to be of type Properties<string | number> (which is very probably actually React.CSSProperties), which does not accepts the function form, hence the error message.
If the rest of your code that handles GridFieldsConfiguration really expects CSSProperties and not the function form, then you would have to refactor it first, so that it can handle that form.
If all it does is to pass the cellStyle option to AG Grid, then you just need to improve the definition of GridFieldsConfiguration type. You can re-use the actual types grom AG Grid, e.g.:
import { AgGridColumnProps as ColDef } from "ag-grid-react";
export interface GridFieldsConfiguration {
cellStyle?: ColDef["cellStyle"];
}
But note that CSSProperties is actually not type-compatible with cellStyle. To fix it, simply remove the return type assertion on your getCellStyle function. If you want to ensure that the returned objects still resembles a CSS object, you can use the new satisfies operator:
const isDisabledbStyle = {
color: '#FF0000'
} satisfies CSSProperties;
const getCellStyle = (params: any, inactiveCol: string) => {
return isDisabledbStyle;
};
Playground Link

How do you get the currently active notebook name in JupyterLab?

I'm working on creating a server-side extension in JupyterLab and have been searching for quite a while for a way to get the currently active notebook name inside my index.ts file. I found this existing extension that gets the currently active tab name with ILabShell and sets the browser tab to have the same name. In my case I'll be triggering the process with a button on the notebook toolbar so the active tab will always be a notebook. However, when I try to use the code in the activate section of my extension, I get TypeError: labShell.currentChanged is undefined where labShell is an instance of ILabShell. That extension doesn't support JupyterLab 3.0+ so I believe that's part of the problem. However, currentChanged for ILabShell is clearly defined here. What if anything can I change to make it work? Is there another way to accomplish what I'm trying to do? I'm aware of things like this to get the notebook name inside the notebook but that's not quite what I'm trying to do.
Windows 10,
Node v14.17.0,
npm 6.14.13,
jlpm 1.21.1,
jupyter lab 3.0.14
I'm using this example server extension as a template: https://github.com/jupyterlab/extension-examples/tree/master/server-extension
index.ts file from the existing extension to get the current tab name:
import {
ILabShell,
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '#jupyterlab/application';
import { Title, Widget } from '#lumino/widgets';
/**
* Initialization data for the jupyterlab-active-as-tab-name extension.
*/
const extension: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-active-as-tab-name',
autoStart: true,
requires: [ILabShell],
activate: (app: JupyterFrontEnd, labShell: ILabShell) => {
const onTitleChanged = (title: Title<Widget>) => {
console.log('the JupyterLab main application:', title);
document.title = title.label;
};
// Keep the session object on the status item up-to-date.
labShell.currentChanged.connect((_, change) => {
const { oldValue, newValue } = change;
// Clean up after the old value if it exists,
// listen for changes to the title of the activity
if (oldValue) {
oldValue.title.changed.disconnect(onTitleChanged);
}
if (newValue) {
newValue.title.changed.connect(onTitleChanged);
}
});
}
};
export default extension;
My index.ts file:
import {
ILabShell,
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '#jupyterlab/application';
import { ICommandPalette } from '#jupyterlab/apputils';
import { ILauncher } from '#jupyterlab/launcher';
import { requestAPI } from './handler';
import { ToolbarButton } from '#jupyterlab/apputils';
import { DocumentRegistry } from '#jupyterlab/docregistry';
import { INotebookModel, NotebookPanel } from '#jupyterlab/notebook';
import { IDisposable } from '#lumino/disposable';
import { Title, Widget } from '#lumino/widgets';
export class ButtonExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
constructor(app: JupyterFrontEnd) {
this.app = app;
}
readonly app: JupyterFrontEnd
createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
// dummy json data to test post requests
const data2 = {"test message" : "message"}
const options = {
method: 'POST',
body: JSON.stringify(data2),
headers: {'Content-Type': 'application/json'
}
};
// Create the toolbar button
let mybutton = new ToolbarButton({
label: 'Measure Energy Usage',
onClick: async () => {
// POST request to Jupyter server
const dataToSend = { file: 'nbtest.ipynb' };
try {
const reply = await requestAPI<any>('hello', {
body: JSON.stringify(dataToSend),
method: 'POST'
});
console.log(reply);
} catch (reason) {
console.error(
`Error on POST /jlab-ext-example/hello ${dataToSend}.\n${reason}`
);
}
// sample POST request to svr.js
fetch('http://localhost:9898/api', options);
}
});
// Add the toolbar button to the notebook toolbar
panel.toolbar.insertItem(10, 'MeasureEnergyUsage', mybutton);
console.log("MeasEnerUsage activated");
// The ToolbarButton class implements `IDisposable`, so the
// button *is* the extension for the purposes of this method.
return mybutton;
}
}
/**
* Initialization data for the server-extension-example extension.
*/
const extension: JupyterFrontEndPlugin<void> = {
id: 'server-extension-example',
autoStart: true,
optional: [ILauncher],
requires: [ICommandPalette, ILabShell],
activate: async (
app: JupyterFrontEnd,
palette: ICommandPalette,
launcher: ILauncher | null,
labShell: ILabShell
) => {
console.log('JupyterLab extension server-extension-example is activated!');
const your_button = new ButtonExtension(app);
app.docRegistry.addWidgetExtension('Notebook', your_button);
// sample GET request to jupyter server
try {
const data = await requestAPI<any>('hello');
console.log(data);
} catch (reason) {
console.error(`Error on GET /jlab-ext-example/hello.\n${reason}`);
}
// get name of active tab
const onTitleChanged = (title: Title<Widget>) => {
console.log('the JupyterLab main application:', title);
document.title = title.label;
};
// Keep the session object on the status item up-to-date.
labShell.currentChanged.connect((_, change) => {
const { oldValue, newValue } = change;
// Clean up after the old value if it exists,
// listen for changes to the title of the activity
if (oldValue) {
oldValue.title.changed.disconnect(onTitleChanged);
}
if (newValue) {
newValue.title.changed.connect(onTitleChanged);
}
});
}
};
export default extension;
There are two ways to fix it and one way to improve it. I recommend using (2) and (3).
Your order of arguments in activate is wrong. There is no magic matching of argument types to signature function; instead arguments are passed in the order given in requires and then optional. This means that you will receive:
...[JupyterFrontEnd, ICommandPalette, ILabShell, ILauncher]
but what you are expecting is:
...[JupyterFrontEnd, ICommandPalette, ILauncher, ILabShell]
In other words, optionals are always at the end. There is no static type check so this is a common source of mistakes - just make sure you double check the order next time (or debug/console.log to see what you are getting).
Actually, don't require ILabShell as a token. Use the ILabShell that comes in app.shell instead. This way your extension will be also compatible with other frontends built using JupyterLab components.
shell = app.shell as ILabShell
(optional improvement) install RetroLab as a development-only requirement and use import type (this way it is not a runtime requirement) to ensure compatibility with RetroLab:
import type { IRetroShell } from '#retrolab/application';
// ... and then in `activate()`:
shell = app.shell as ILabShell | IRetroShell
to be clear: not doing so would not make your extension incompatible; what it does is ensures you do not make it incompatible by depending on lab-specific behaviour of the ILabShell in the future.
So in total it would look like:
import {
ILabShell,
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '#jupyterlab/application';
import type { IRetroShell } from '#retrolab/application';
// ...
const extension: JupyterFrontEndPlugin<void> = {
id: 'server-extension-example',
autoStart: true,
optional: [ILauncher],
requires: [ICommandPalette],
activate: async (
app: JupyterFrontEnd,
palette: ICommandPalette,
launcher: ILauncher | null
) => {
let shell = app.shell as ILabShell | IRetroShell ;
shell.currentChanged.connect((_, change) => {
console.log(change);
// ...
});
}
};
export default extension;

Aurelia: Stylesheet loaded but not removed

In an aurelia project I have several components that import additional stylesheets, e.g. from semantic-ui. After leaving the components page, the stylesheet is still active and not removed. Is it possible to 'unload' the stylesheets?
Update (2018-03-27):
I submitted a PR to enable this as an opt-in, you can keep track of it here: https://github.com/aurelia/templating-resources/pull/344
Original answer:
A word of warning, this is untested and aurelia-internals-hacky.
What you could do is override the default CSSViewEngineHooks and CSSResource classes to keep track of the style elements it injects, and then add an beforeUnbind hook to remove the styles again.. before unbind (right after detached)
Unfortunately the CSSResource class is not exported from aurelia-templating-resources so we need to go one layer deeper and overwrite the existing style loader plugins that returns instances of CSSResource.
Here's how:
First we grab the code from aurelia-templating-resources/src/css-resource.js, put it in our own src/css-resource.js/ts and make a few tweaks to it (don't think too much of the size, it's just a copy-paste with a few small tweaks, annotated with comments):
import {ViewResources, resource, ViewCompileInstruction} from 'aurelia-templating';
import {Loader} from 'aurelia-loader';
import {Container} from 'aurelia-dependency-injection';
import {relativeToFile} from 'aurelia-path';
import {DOM, FEATURE} from 'aurelia-pal';
let cssUrlMatcher = /url\((?!['"]data)([^)]+)\)/gi;
function fixupCSSUrls(address, css) {
if (typeof css !== 'string') {
throw new Error(`Failed loading required CSS file: ${address}`);
}
return css.replace(cssUrlMatcher, (match, p1) => {
let quote = p1.charAt(0);
if (quote === '\'' || quote === '"') {
p1 = p1.substr(1, p1.length - 2);
}
return 'url(\'' + relativeToFile(p1, address) + '\')';
});
}
class CSSResource {
constructor(address: string) {
this.address = address;
this._scoped = null;
this._global = false;
this._alreadyGloballyInjected = false;
}
initialize(container: Container, target: Function): void {
this._scoped = new target(this);
}
register(registry: ViewResources, name?: string): void {
if (name === 'scoped') {
registry.registerViewEngineHooks(this._scoped);
} else {
this._global = true;
}
}
load(container: Container): Promise<CSSResource> {
return container.get(Loader)
.loadText(this.address)
.catch(err => null)
.then(text => {
text = fixupCSSUrls(this.address, text);
this._scoped.css = text;
if (this._global) {
this._alreadyGloballyInjected = true;
// DOM.injectStyles(text); <- replace this
// this is one of the two possible moments where the style is injected
// _scoped is the CSSViewEngineHooks instance, and we handle the removal there
this._scoped.styleNode = DOM.injectStyles(text);
}
});
}
}
class CSSViewEngineHooks {
constructor(owner: CSSResource) {
this.owner = owner;
this.css = null;
}
beforeCompile(content: DocumentFragment, resources: ViewResources, instruction: ViewCompileInstruction): void {
if (instruction.targetShadowDOM) {
DOM.injectStyles(this.css, content, true);
} else if (FEATURE.scopedCSS) {
let styleNode = DOM.injectStyles(this.css, content, true);
styleNode.setAttribute('scoped', 'scoped');
} else if (this._global && !this.owner._alreadyGloballyInjected) {
// DOM.injectStyles(this.css); <- replace this
// save a reference to the node so we can easily remove it later
this.styleNode = DOM.injectStyles(this.css);
this.owner._alreadyGloballyInjected = true;
}
}
// this is the hook we add, here we remove the node again
beforeUnbind(): void {
if (this._global && this.owner._alreadyGloballyInjected) {
DOM.removeNode(this.styleNode);
this.owner._alreadyGloballyInjected = false;
}
}
}
export function _createCSSResource(address: string): Function {
#resource(new CSSResource(address))
class ViewCSS extends CSSViewEngineHooks {}
return ViewCSS;
}
Then, in our main.ts/js we do the same thing aurelia-templating-resources.js does, but with our own version.
So we do this after the call to aurelia.use.standardConfiguration() etc, to override the existing one
let viewEngine = config.container.get(ViewEngine);
let styleResourcePlugin = {
fetch(address) {
return { [address]: _createCSSResource(address) };
}
};
['.css', '.less', '.sass', '.scss', '.styl'].forEach(ext => viewEngine.addResourcePlugin(ext, styleResourcePlugin));
And that should pretty much do the trick.. :)
I have found a plugin to resolve the issue:
https://github.com/jbockle/aurelia-useable-style-loader
But for the latest Webpack webpack.config.js should be a little bit different than in a plugin readme.
You should load .css files this way:
use: [
{ loader: 'style-loader', options: { injectType: 'lazyStyleTag' } },
'css-loader'
]
Instead of this:
use: ['style-loader/useable', 'css-loader']

Binding to 'style.grid' throws 'sanitizing unsafe style value' warning

I'm creating a mechanism for defining and calculating my own reusable grids. Here's an example of what's returned
[left-bleed-start] 10.245000000001024px [left-bleed-end content-col-start] 1331.8500000001332px [content-col-end right-bleed-start] 10.245000000001024px [right-bleed-end]/[top-bleed-start] 10.245000000001024px [top-bleed-end link-row-start] 81.9600000000082px [content-row-end footer-row-start] 163.9200000000164px [footer-row-end bottom-bleed-start] 10.245000000001024px [bottom-bleed-end]
When applying it like this
<div class="appCompGrid" [style.grid]="Grid.GridCode"></div>
I get the santization warning. However if I copy and paste the value in like this
<div class="appCompGrid" style="grid: (same code);"></div>
everything works. The css class is where I define the display as grid seeing that it'll be consistent no matter what size the screen is. The only thing I could think to do was go back into the function and add + ';' to the end of where the grid code is put together figuring maybe that was throwing something off but it still gives the same error. I tried applying display: grid; inline to see if maybe there was a problem with it reading both from a css class and inline for some odd reason.
I'm using #HostListener to re-calculate the grid as the size or orientation changes, so far I haven't run into a problem with Angular functioning in this manner so I don't understand where to begin with figuring out why this is happening. Here's how I have my component classes set up.
Base Class
export class GridBuilder {
Settings : GridInit = new GridInit();
GridData : Array<GridType> = new Array();
Grid : GridOutput = new GridOutput();
constructor() { this.GridData = GridDefs; }
public buildGrid() {
const coreSettings : GridInit = this.Settings;
const gridData : GridType[] = this.GridData;
const w: number = multiply( coreSettings.Size.Width, coreSettings.Size.PixelRatio );
const h: number = multiply( coreSettings.Size.Height, coreSettings.Size.PixelRatio );
const o: string = checkOrientation( w, h );
const c: CellSpecs = calcCell( o, w );
const t: GridType = gridData.find( a => a.GridName == coreSettings.GridStyle );
const cols: string = calcArea( t.Columns, c );
const rows: string = calcArea( t.Rows, c );
this.Grid.GridCode = cols + '/' + rows + ';';
this.Grid.GridAreas = t.Areas;
}
}
Secondary class for app component/ any top tier container
export class SiteGrid extends GridBuilder {
constructor(){
super();
this.applySizeSettings();
}
applySizeSettings(){
this.Settings.Size.Width = window.innerWidth;
this.Settings.Size.Height = window.innerHeight;
this.Settings.Size.PixelRatio = window.devicePixelRatio;
}
}
the AppComponent
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent extends SiteGrid {
title = 'app';
#HostListener( 'window: resize', [ '$event' ] )onResize( event ){ this.applySizeSettings(); this.buildGrid(); }
constructor(){
super();
this.Settings.GridStyle = 'SiteGridA';
this.buildGrid();
}
}
I don't know how relevant this may be in helping figure out the solution but thought I'd show how things are flowing just incase. Anyone know why this warning is occurring?
You need to implement a sanitizer to cleanse your css, or bypass it...
constructor(private sanitizer: DomSanitizer) {
this.sanitizedCSS = sanitizer.bypassSecurityTrustStyle(Grid.GridCode) ;
}
As for why, this blog explains it pretty well, as does the DomSanitizer documentation.
DomSanitizer helps preventing Cross Site Scripting Security bugs (XSS) by sanitizing values to be safe to use in the different DOM contexts.

Properties of exported const object map are not defined

I am trying to simulate map-like behaviour in TypeScript and also get code completion of possible values. I am limited to TypeScript 1.8.
catalog.ts
export declare type CATALOG = 'CATALOG1' | 'CATALOG2' | 'CATALOG3';
export const CATALOGS: { [catalog: string]: CATALOG } = {
CATALOG1: 'CATALOG1',
CATALOG2: 'CATALOG2',
CATALOG3: 'CATALOG3'
};
example.ts
import { CATALOGS } from './catalog';
class MyClass {
catalogs = CATALOGS;
constructor() {
CATALOGS.CATALOG1; // error
this.catalogs.CATALOG1; // error
}
}
This results in following error:
Property 'CATALOG1' does not exist on type { [catalog: string]:
"CATALOG1" | "CATALOG2" | "CATALOG3" }
Can someone elaborate?
What you're describing isn't a "map-like behaviour", with a map you'll do something like:
CATALOGS.get("CATALOG1");
And that's basically what you defined with: { [catalog: string]: CATALOG }.
You can access it like this:
let a = CATALOGS["CATALOG1"];
If you want to access it as you did, then it should be:
interface Catalogs {
CATALOG1: CATALOG;
CATALOG2: CATALOG;
CATALOG3: CATALOG;
}
export const CATALOGS: Catalogs = {
CATALOG1: 'CATALOG1',
CATALOG2: 'CATALOG2',
CATALOG3: 'CATALOG3'
};
let a = CATALOGS.CATALOG1;
(code in playground)

Resources