From ec0441df3911aa19e3307cceccba4ef1d4de38bc Mon Sep 17 00:00:00 2001 From: Xavier Artusi Date: Tue, 14 Apr 2020 17:17:19 +0200 Subject: [PATCH 1/5] [VTK] Add colorbar on vtk renderer --- panel/models/vtk.py | 7 +- panel/models/vtk/vtk_colorbar.ts | 130 +++++++++++++++++++++++++++++++ panel/models/vtk/vtk_layout.ts | 67 +++++++++++++++- panel/pane/vtk/vtk.py | 29 +++++-- 4 files changed, 222 insertions(+), 11 deletions(-) create mode 100644 panel/models/vtk/vtk_colorbar.ts diff --git a/panel/models/vtk.py b/panel/models/vtk.py index 972fe07fb9..b903bfc6be 100644 --- a/panel/models/vtk.py +++ b/panel/models/vtk.py @@ -3,10 +3,11 @@ Defines custom VTKPlot bokeh model to render VTK objects. """ from bokeh.core.properties import (String, Bool, Dict, Any, Override, - Instance, Int, Float, PositiveInt, Enum) + Instance, Int, Float, PositiveInt, Enum, + List) from bokeh.core.has_props import abstract from bokeh.core.enums import enumeration -from bokeh.models import HTMLBox, Model +from bokeh.models import HTMLBox, Model, ColorMapper vtk_cdn = "https://unpkg.com/vtk.js" @@ -39,6 +40,8 @@ class AbstractVTKPlot(HTMLBox): width = Override(default=300) + color_mappers = List(Instance(ColorMapper)) + class VTKAxes(Model): """ diff --git a/panel/models/vtk/vtk_colorbar.ts b/panel/models/vtk/vtk_colorbar.ts new file mode 100644 index 0000000000..92b7b096ae --- /dev/null +++ b/panel/models/vtk/vtk_colorbar.ts @@ -0,0 +1,130 @@ +import {ColorMapper, ContinuousColorMapper, LinearColorMapper} from "@bokehjs/models/mappers" +import {range, linspace} from "@bokehjs/core/util/array" + +export declare type ColorBarOptions = { + ticksNum?: number + ticksSize?: number + fontFamily?: string + fontSize?: string + height?: string +} + +export class VTKColorBar { + public canvas: HTMLCanvasElement + private ctx: CanvasRenderingContext2D + + constructor( + private parent: HTMLElement, + private mapper: ColorMapper, + private options: ColorBarOptions = {} + ) { + if (!options.ticksNum) options.ticksNum = 5 + if (!options.fontFamily) options.fontFamily = "Arial" + if (!options.fontSize) options.fontSize = "12px" + if (!options.ticksSize) options.ticksSize = 2 + this.canvas = document.createElement("canvas") + this.canvas.style.width = "100%" + this.parent.appendChild(this.canvas) + this.ctx = this.canvas.getContext("2d")! + this.ctx.font = `${this.options.fontSize} ${this.options.fontFamily}` + this.ctx.lineWidth = options.ticksSize + if (!options.height) + options.height = `${(this.font_height+1) * 4}px` //title/ticks/colorbar + this.canvas.style.height = options.height + this.draw_colorbar() + } + + get values(): number[] { + const {high, low} = this.mapper as ContinuousColorMapper + return linspace(low, high, this.options.ticksNum!) + } + + get ticks(): string[] { + return this.values.map((v) => v.toExponential(3)) + } + + get title(): string { + return this.mapper.name ? this.mapper.name : "scalars" + } + + get font_height(): number { + let font_height = 0 + this.values.forEach((val) => { + const { + actualBoundingBoxAscent, + actualBoundingBoxDescent, + } = this.ctx.measureText(`${val}`) + const height = actualBoundingBoxAscent + actualBoundingBoxDescent + if (font_height < height) { + font_height = height + } + }) + return font_height + } + + draw_colorbar() { + this.canvas.width = this.canvas.clientWidth + this.canvas.height = this.canvas.clientHeight + const {palette} = this.mapper + this.ctx.font = `${this.options.fontSize} ${this.options.fontFamily}` + const font_height = this.font_height + + this.ctx.save() + //colorbar + const image = document.createElement("canvas") + const h = 1 + const w = palette.length + image.width = w + image.height = h + const image_ctx = image.getContext("2d")! + const image_data = image_ctx.getImageData(0, 0, w, h) + const cmap = new LinearColorMapper({palette}).rgba_mapper + const buf8 = cmap.v_compute(range(0, palette.length)) + image_data.data.set(buf8) + image_ctx.putImageData(image_data, 0, 0) + this.ctx.drawImage( + image, + 0, + 2 * (this.font_height + 1) + 1, + this.canvas.width, + this.canvas.height + ) + this.ctx.restore() + this.ctx.save() + //title + this.ctx.textAlign = 'center' + this.ctx.fillText(this.title, this.canvas.width/2, font_height+1) + this.ctx.restore() + this.ctx.save() + //ticks + const tick_x_positions = linspace(0, this.canvas.width, 5) + tick_x_positions.forEach((xpos, idx) => { + let xpos_tick = xpos + if (idx == 0) { + xpos_tick = xpos + Math.ceil(this.ctx.lineWidth / 2) + this.ctx.textAlign = "left" + } else if (idx == tick_x_positions.length - 1) { + xpos_tick = xpos - Math.ceil(this.ctx.lineWidth / 2) + this.ctx.textAlign = "right" + } else { + this.ctx.textAlign = "center" + } + this.ctx.moveTo(xpos_tick, 2*(font_height+1)) + this.ctx.lineTo(xpos_tick, 2*(font_height+1)+5) + this.ctx.stroke() + this.ctx.fillText(`${this.ticks[idx]}`, xpos, 2*(font_height+1)) + }) + this.ctx.restore() + + } +} + +// .wrapper { +// display: table-cell; +// vertical-align: bottom; +// height: 200px; +// } +// .content { +// max-height: 200px; +// overflow: auto; +// } diff --git a/panel/models/vtk/vtk_layout.ts b/panel/models/vtk/vtk_layout.ts index 241980ab0f..e89c50927e 100644 --- a/panel/models/vtk/vtk_layout.ts +++ b/panel/models/vtk/vtk_layout.ts @@ -3,10 +3,12 @@ import * as p from "@bokehjs/core/properties" import {div} from "@bokehjs/core/dom" import {clone} from "@bokehjs/core/util/object" import {HTMLBox} from "@bokehjs/models/layouts/html_box" +import {ColorMapper} from "@bokehjs/models/mappers/color_mapper" import {PanelHTMLBoxView, set_size} from "../layout" -import {vtkns, VolumeType, majorAxis} from "./vtk_utils" +import {vtkns, VolumeType, majorAxis} from "./vtk_utils" +import {VTKColorBar} from "./vtk_colorbar" export abstract class AbstractVTKView extends PanelHTMLBoxView{ model: AbstractVTKPlot @@ -16,6 +18,63 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView{ protected _widgetManager: any protected _setting_camera: boolean = false + _add_colorbars(): void { + //construct colorbars + const old_info_div = this.el.querySelector(".vtk_info") + if (old_info_div){ + this.el.removeChild(old_info_div) + } + if (this.model.color_mappers.length < 1) return + + const info_div = document.createElement("div") + const expand_width = "350px" + const collapsed_width = "30px" + info_div.classList.add('vtk_info') + info_div.style.width = expand_width + info_div.style.padding = "0px 2px 0px 2px" + info_div.style.maxHeight = "150px" + info_div.style.height = "auto" + info_div.style.backgroundColor = "rgba(255, 255, 255, 0.4)" + info_div.style.borderRadius = "10px" + info_div.style.margin = "2px" + info_div.style.boxSizing = "border-box" + info_div.style.overflow = "hidden" + info_div.style.overflowY = "auto" + info_div.style.transition = "width 0.1s linear" + info_div.style.bottom = "0px" + info_div.style.position = "absolute" + this.el.appendChild(info_div) + + //construct colorbars + const colorbars: VTKColorBar[] = [] + this.model.color_mappers.forEach((mapper) => { + const cb = new VTKColorBar(info_div, mapper) + colorbars.push(cb) + }) + + //content when collapsed + const dots = document.createElement('div'); + dots.style.textAlign = "center" + dots.style.fontSize = "20px" + dots.innerText = "..." + + info_div.addEventListener('click', () => { + if(info_div.style.width === collapsed_width){ + info_div.removeChild(dots) + info_div.style.height = "auto" + info_div.style.width = expand_width + colorbars.forEach((cb) => info_div.appendChild(cb.canvas)) + } else { + colorbars.forEach((cb) => info_div.removeChild(cb.canvas)) + info_div.style.height = collapsed_width + info_div.style.width = collapsed_width + info_div.appendChild(dots) + } + }) + + info_div.click() + } + connect_signals(): void { super.connect_signals() this.connect(this.model.properties.data.change, () => { @@ -25,8 +84,9 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView{ this._orientation_widget_visibility(this.model.orientation_widget) }) this.connect(this.model.properties.camera.change, () => this._set_camera_state()) + this.connect(this.model.properties.color_mappers.change, () => this._add_colorbars()) } - + _orientation_widget_visibility(visibility: boolean): void { this._orientationWidget.setEnabled(visibility) if(visibility) @@ -145,6 +205,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView{ ) this._set_camera_state() this.model.renderer_el = this._vtk_renwin + this._add_colorbars() } after_layout(): void { @@ -166,6 +227,7 @@ export namespace AbstractVTKPlot { data: p.Property camera: p.Property orientation_widget: p.Property + color_mappers: p.Property } } @@ -189,6 +251,7 @@ export abstract class AbstractVTKPlot extends HTMLBox { this.define({ orientation_widget: [ p.Boolean, false ], camera: [ p.Instance ], + color_mappers: [ p.Array, [] ], }) this.override({ diff --git a/panel/pane/vtk/vtk.py b/panel/pane/vtk/vtk.py index a3e01aa982..4b6149e744 100644 --- a/panel/pane/vtk/vtk.py +++ b/panel/pane/vtk/vtk.py @@ -331,6 +331,10 @@ class VTK(PaneBase): Define if the object serialization occurs at panel instantiation or when the panel is displayed.""") + color_mappers = param.List(doc=""" + List of color_mapper which will be display with colorbars in the + panel.""") + _updates = True _rerender_params = ['object', 'serialize_on_instantiation'] @@ -343,6 +347,7 @@ def __init__(self, object=None, **params): self._vtkjs = None if self.serialize_on_instantiation: self._vtkjs = self._get_vtkjs() + self.color_mappers = self._construct_color_mappers() @classmethod def applies(cls, obj): @@ -384,7 +389,7 @@ def _update_object(self, ref, doc, root, parent, comm): self._legend = None super(VTK, self)._update_object(ref, doc, root, parent, comm) - def construct_colorbars(self, orientation='horizontal'): + def _construct_color_mappers(self): if self._legend is None: try: from .vtkjs_serializer import construct_palettes @@ -392,14 +397,23 @@ def construct_colorbars(self, orientation='horizontal'): except Exception: self._legend = {} if self._legend: - from bokeh.models import Plot, LinearColorMapper, ColorBar, FixedTicker + from bokeh.models import LinearColorMapper + return [LinearColorMapper(name=k, low=v['low'], high=v['high'], palette=v['palette']) + for k, v in self._legend.items()] + else: + return [] + + def construct_colorbars(self, orientation='horizontal'): + color_mappers = self._construct_color_mappers() + if len(color_mappers)>0: + from bokeh.models import Plot, ColorBar, FixedTicker if orientation == 'horizontal': cbs = [] - for k, v in self._legend.items(): - ticks = np.linspace(v['low'], v['high'], 5) + for color_mapper in color_mappers: + ticks = np.linspace(color_mapper.low, color_mapper.high, 5) cbs.append(ColorBar( - color_mapper=LinearColorMapper(low=v['low'], high=v['high'], palette=v['palette']), - title=k, + color_mapper=color_mapper, + title=color_mapper.name, ticker=FixedTicker(ticks=ticks), label_standoff=5, background_fill_alpha=0, orientation='horizontal', location=(0, 0) )) @@ -463,8 +477,9 @@ def _get_vtkjs(self): def _update(self, model): self._vtkjs = None - vtkjs = self._get_vtkjs() + vtkjs = self._get_vtkjs() model.data = base64encode(vtkjs) if vtkjs is not None else vtkjs + self.color_mappers = self._construct_color_mappers() def export_vtkjs(self, filename='vtk_panel.vtkjs'): with open(filename, 'wb') as f: From 9c99d763b373970529105ffb8afcd8e7db207c97 Mon Sep 17 00:00:00 2001 From: xavArtley Date: Wed, 15 Apr 2020 13:46:11 +0200 Subject: [PATCH 2/5] Update panel/models/vtk/vtk_colorbar.ts Co-Authored-By: Philipp Rudiger --- panel/models/vtk/vtk_colorbar.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/panel/models/vtk/vtk_colorbar.ts b/panel/models/vtk/vtk_colorbar.ts index 92b7b096ae..c70c000a86 100644 --- a/panel/models/vtk/vtk_colorbar.ts +++ b/panel/models/vtk/vtk_colorbar.ts @@ -55,9 +55,8 @@ export class VTKColorBar { actualBoundingBoxDescent, } = this.ctx.measureText(`${val}`) const height = actualBoundingBoxAscent + actualBoundingBoxDescent - if (font_height < height) { + if (font_height < height) font_height = height - } }) return font_height } From 6f562a940a5a06acb611f82fa04a45354779d532 Mon Sep 17 00:00:00 2001 From: xavArtley Date: Wed, 15 Apr 2020 13:46:22 +0200 Subject: [PATCH 3/5] Update panel/models/vtk/vtk_layout.ts Co-Authored-By: Philipp Rudiger --- panel/models/vtk/vtk_layout.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/panel/models/vtk/vtk_layout.ts b/panel/models/vtk/vtk_layout.ts index e89c50927e..caa5afecde 100644 --- a/panel/models/vtk/vtk_layout.ts +++ b/panel/models/vtk/vtk_layout.ts @@ -21,9 +21,8 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView{ _add_colorbars(): void { //construct colorbars const old_info_div = this.el.querySelector(".vtk_info") - if (old_info_div){ + if (old_info_div) this.el.removeChild(old_info_div) - } if (this.model.color_mappers.length < 1) return const info_div = document.createElement("div") From 18b42577581104339d6524debd7a96a834b8ae94 Mon Sep 17 00:00:00 2001 From: xavArtley Date: Wed, 15 Apr 2020 13:46:31 +0200 Subject: [PATCH 4/5] Update panel/models/vtk/vtk_layout.ts Co-Authored-By: Philipp Rudiger --- panel/models/vtk/vtk_layout.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/panel/models/vtk/vtk_layout.ts b/panel/models/vtk/vtk_layout.ts index caa5afecde..9dd75448c8 100644 --- a/panel/models/vtk/vtk_layout.ts +++ b/panel/models/vtk/vtk_layout.ts @@ -85,7 +85,6 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView{ this.connect(this.model.properties.camera.change, () => this._set_camera_state()) this.connect(this.model.properties.color_mappers.change, () => this._add_colorbars()) } - _orientation_widget_visibility(visibility: boolean): void { this._orientationWidget.setEnabled(visibility) if(visibility) From fab0dc67f9c0b17cd701eb680aa6c18fc81160ff Mon Sep 17 00:00:00 2001 From: Xavier Artusi Date: Tue, 14 Apr 2020 19:03:35 +0200 Subject: [PATCH 5/5] minor fixes --- panel/models/vtk/vtk_colorbar.ts | 11 ----------- panel/models/vtk/vtk_layout.ts | 3 ++- panel/pane/vtk/vtk.py | 28 ++++++++++++++-------------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/panel/models/vtk/vtk_colorbar.ts b/panel/models/vtk/vtk_colorbar.ts index c70c000a86..85fcf3c6a7 100644 --- a/panel/models/vtk/vtk_colorbar.ts +++ b/panel/models/vtk/vtk_colorbar.ts @@ -114,16 +114,5 @@ export class VTKColorBar { this.ctx.fillText(`${this.ticks[idx]}`, xpos, 2*(font_height+1)) }) this.ctx.restore() - } } - -// .wrapper { -// display: table-cell; -// vertical-align: bottom; -// height: 200px; -// } -// .content { -// max-height: 200px; -// overflow: auto; -// } diff --git a/panel/models/vtk/vtk_layout.ts b/panel/models/vtk/vtk_layout.ts index 9dd75448c8..7368bac1df 100644 --- a/panel/models/vtk/vtk_layout.ts +++ b/panel/models/vtk/vtk_layout.ts @@ -56,7 +56,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView{ dots.style.textAlign = "center" dots.style.fontSize = "20px" dots.innerText = "..." - + info_div.addEventListener('click', () => { if(info_div.style.width === collapsed_width){ info_div.removeChild(dots) @@ -85,6 +85,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView{ this.connect(this.model.properties.camera.change, () => this._set_camera_state()) this.connect(this.model.properties.color_mappers.change, () => this._add_colorbars()) } + _orientation_widget_visibility(visibility: boolean): void { this._orientationWidget.setEnabled(visibility) if(visibility) diff --git a/panel/pane/vtk/vtk.py b/panel/pane/vtk/vtk.py index 4b6149e744..0a98764293 100644 --- a/panel/pane/vtk/vtk.py +++ b/panel/pane/vtk/vtk.py @@ -42,7 +42,7 @@ class VTKVolume(PaneBase): Name of the colormap used to transform pixel value in color.""") diffuse = param.Number(default=0.7, step=1e-2, doc=""" - Value to control the diffuse Lighting. It relies on both the + Value to control the diffuse Lighting. It relies on both the light direction and the object surface normal.""") display_volume = param.Boolean(default=True, doc=""" @@ -51,11 +51,11 @@ class VTKVolume(PaneBase): display_slices = param.Boolean(default=False, doc=""" If set to true, the orthgonal slices in the three (X, Y, Z) - directions are displayed. Position of each slice can be + directions are displayed. Position of each slice can be controlled using slice_(i,j,k) parameters.""") edge_gradient = param.Number(default=0.4, bounds=(0, 1), step=1e-2, doc=""" - Parameter to adjust the opacity of the volume based on the + Parameter to adjust the opacity of the volume based on the gradient between voxels.""") interpolation = param.Selector(default='fast_linear', objects=['fast_linear','linear','nearest'], doc=""" @@ -82,18 +82,18 @@ class VTKVolume(PaneBase): The value must be specified as an hexadecimal color string.""") rescale = param.Boolean(default=False, doc=""" - If set to True the colormap is rescaled beween min and max + If set to True the colormap is rescaled beween min and max value of the non-transparent pixel, otherwise the full range of the pixel values are used.""") shadow = param.Boolean(default=True, doc=""" - If set to False, then the mapper for the volume will not - perform shading computations, it is the same as setting + If set to False, then the mapper for the volume will not + perform shading computations, it is the same as setting ambient=1, diffuse=0, specular=0.""") sampling = param.Number(default=0.4, bounds=(0, 1), step=1e-2, doc=""" - Parameter to adjust the distance between samples used for - rendering. The lower the value is the more precise is the + Parameter to adjust the distance between samples used for + rendering. The lower the value is the more precise is the representation but it is more computationally intensive.""") spacing = param.Tuple(default=(1, 1, 1), length=3, doc=""" @@ -317,6 +317,10 @@ class VTK(PaneBase): camera = param.Dict(doc="State of the rendered VTK camera.") + color_mappers = param.List(doc=""" + List of color_mapper which will be display with colorbars in the + panel.""") + enable_keybindings = param.Boolean(default=False, doc=""" Activate/Deactivate keys binding. @@ -331,10 +335,6 @@ class VTK(PaneBase): Define if the object serialization occurs at panel instantiation or when the panel is displayed.""") - color_mappers = param.List(doc=""" - List of color_mapper which will be display with colorbars in the - panel.""") - _updates = True _rerender_params = ['object', 'serialize_on_instantiation'] @@ -398,7 +398,7 @@ def _construct_color_mappers(self): self._legend = {} if self._legend: from bokeh.models import LinearColorMapper - return [LinearColorMapper(name=k, low=v['low'], high=v['high'], palette=v['palette']) + return [LinearColorMapper(name=k, low=v['low'], high=v['high'], palette=v['palette']) for k, v in self._legend.items()] else: return [] @@ -477,7 +477,7 @@ def _get_vtkjs(self): def _update(self, model): self._vtkjs = None - vtkjs = self._get_vtkjs() + vtkjs = self._get_vtkjs() model.data = base64encode(vtkjs) if vtkjs is not None else vtkjs self.color_mappers = self._construct_color_mappers()