Skip to content
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

[VTK] Add colorbar on vtk renderer #1270

Merged
merged 5 commits into from
Apr 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions panel/models/vtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -39,6 +40,8 @@ class AbstractVTKPlot(HTMLBox):

width = Override(default=300)

color_mappers = List(Instance(ColorMapper))


class VTKAxes(Model):
"""
Expand Down
118 changes: 118 additions & 0 deletions panel/models/vtk/vtk_colorbar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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()
}
}
64 changes: 63 additions & 1 deletion panel/models/vtk/vtk_layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,6 +18,62 @@ 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, () => {
Expand All @@ -25,6 +83,7 @@ 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 {
Expand Down Expand Up @@ -145,6 +204,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView{
)
this._set_camera_state()
this.model.renderer_el = this._vtk_renwin
this._add_colorbars()
}

after_layout(): void {
Expand All @@ -166,6 +226,7 @@ export namespace AbstractVTKPlot {
data: p.Property<string|VolumeType>
camera: p.Property<any>
orientation_widget: p.Property<boolean>
color_mappers: p.Property<ColorMapper[]>
}
}

Expand All @@ -189,6 +250,7 @@ export abstract class AbstractVTKPlot extends HTMLBox {
this.define<AbstractVTKPlot.Props>({
orientation_widget: [ p.Boolean, false ],
camera: [ p.Instance ],
color_mappers: [ p.Array, [] ],
})

this.override({
Expand Down
43 changes: 29 additions & 14 deletions panel/pane/vtk/vtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="""
Expand All @@ -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="""
Expand All @@ -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="""
Expand Down Expand Up @@ -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.

Expand All @@ -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):
Expand Down Expand Up @@ -384,22 +389,31 @@ 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
self._legend = construct_palettes(self.object)
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)
))
Expand Down Expand Up @@ -465,6 +479,7 @@ def _update(self, model):
self._vtkjs = None
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:
Expand Down