Skip to content

Commit

Permalink
Make PaletteSelect use stylehseets instead of inline styles (bokeh#…
Browse files Browse the repository at this point in the history
…14052)

* Make PaletteSelect use stylesheets instead of inline styles

* Make color2css() pass through CSS color strings

* Disable maximum image size checks in PIL
  • Loading branch information
mattpap authored Sep 6, 2024
1 parent 5036057 commit 4f93515
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 75 deletions.
6 changes: 6 additions & 0 deletions bokehjs/src/less/widgets/palette_select_item.less
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
gap: 0.5em;
}

.bk-swatch {
width: 100px;
height: auto;
align-self: stretch;
}

.bk-item {
--active-tool-highlight: #26aae1;

Expand Down
11 changes: 8 additions & 3 deletions bokehjs/src/lib/core/util/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,17 @@ function hex(v: uint8): string {
}

export function rgba2css([r, g, b, a]: RGBA): string {
return `rgba(${r}, ${g}, ${b}, ${a/255})`
const alpha = a == 255 ? "" : ` / ${a/255}`
return `rgb(${r} ${g} ${b}${alpha})`
}

export function color2css(color: Color | null, alpha?: number): string {
const [r, g, b, a] = color2rgba(color, alpha)
return rgba2css([r, g, b, a])
if (isString(color) && (alpha == null || alpha == 1.0)) {
return color // passthrough to persist color in its original form
} else {
const [r, g, b, a] = color2rgba(color, alpha)
return rgba2css([r, g, b, a])
}
}

export function color2hex(color: Color | null, alpha?: number): string {
Expand Down
118 changes: 53 additions & 65 deletions bokehjs/src/lib/models/widgets/palette_select.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type {Keys} from "core/dom"
import {div, canvas, empty, px} from "core/dom"
import type {StyleSheetLike} from "core/dom"
import {div, empty, px, InlineStyleSheet} from "core/dom"
import type {StyleSheetLike, Keys} from "core/dom"
import {DropPane} from "core/util/panes"
import type * as p from "core/properties"
import {enumerate} from "core/util/iterator"
import {color2css} from "core/util/color"
import {cycle} from "core/util/math"
import {linspace} from "core/util/array"
import {assert} from "core/util/assert"

import {InputWidget, InputWidgetView} from "./input_widget"
import * as inputs_css from "styles/widgets/inputs.css"
Expand All @@ -28,8 +28,11 @@ export class PaletteSelectView extends InputWidgetView {
protected _value_el: HTMLElement
protected _pane: DropPane

protected readonly _style = new InlineStyleSheet()
protected readonly _style_menu = new InlineStyleSheet()

override stylesheets(): StyleSheetLike[] {
return [...super.stylesheets(), palette_select_css.default, item_css.default, icons_css.default]
return [...super.stylesheets(), palette_select_css.default, item_css.default, icons_css.default, this._style]
}

override connect_signals(): void {
Expand All @@ -53,56 +56,12 @@ export class PaletteSelectView extends InputWidgetView {
this._pane.el.style.setProperty("--number-of-columns", `${ncols}`)
}

protected _render_image(item: Item): HTMLCanvasElement {
const [_name, colors] = item
const {swatch_width, swatch_height} = this.model

const width = swatch_width
const height = swatch_height == "auto" ? swatch_width : swatch_height

const img = canvas({width, height})
const ctx = img.getContext("2d")!

const n = colors.length
const dx = 100.0/n

for (const [color, i] of enumerate(colors)) {
ctx.beginPath()
ctx.rect(i*dx, 0, dx, 20)
const css_color = color2css(color)
ctx.strokeStyle = css_color
ctx.fillStyle = css_color
ctx.fill()
ctx.stroke()
}

return img
}

protected _render_item(item: Item): HTMLElement {
const [name, colors] = item
const {swatch_width, swatch_height} = this.model

const n = colors.length
const stops = linspace(0, 100, n + 1)
const color_map: string[] = []

for (const [color, i] of enumerate(colors)) {
const [from, to] = [stops[i], stops[i + 1]]
color_map.push(`${color2css(color)} ${from}% ${to}%`)
}

const img = div()
img.style.background = `linear-gradient(to right, ${color_map.join(", ")})`
img.style.width = px(swatch_width)
if (swatch_height == "auto") {
img.style.alignSelf = "stretch"
} else {
img.style.height = px(swatch_height)
}

const entry = div({class: item_css.entry}, img, name)
return entry
const [name] = item
const i = this.model.items.indexOf(item)
assert(i != -1)
const swatch = div({class: item_css.swatch, id: `item_${i}`})
return div({class: item_css.entry}, swatch, div(name))
}

protected _render_value(): HTMLElement | null {
Expand Down Expand Up @@ -134,8 +93,39 @@ export class PaletteSelectView extends InputWidgetView {
override render(): void {
super.render()

const item_els: HTMLElement[] = []
const {swatch_width, swatch_height} = this.model
this._style.replace(`
.${item_css.swatch} {
width: ${swatch_width}px;
height: ${swatch_height == "auto" ? "auto" : px(swatch_height)};
}
`)

for (const [item, i] of enumerate(this.model.items)) {
const [, colors] = item

const n = colors.length
const stops = linspace(0, 100, n + 1)
const color_map: string[] = []

for (const [color, i] of enumerate(colors)) {
const [from, to] = [stops[i], stops[i + 1]]
color_map.push(`${color2css(color)} ${from}% ${to}%`)
}

const gradient = color_map.join(", ")
this._style.append(`
#item_${i} {
background: linear-gradient(to right, ${gradient});
}
`)
}

// The widget and its menu are independent components, so they need
// to have their own stylesheets.
this._style_menu.replace(this._style.css)

const item_els: HTMLElement[] = []
for (const [item, i] of enumerate(this.model.items)) {
const entry_el = this._render_item(item)
const item_el = div({class: item_css.item, tabIndex: 0}, entry_el)
Expand Down Expand Up @@ -183,7 +173,7 @@ export class PaletteSelectView extends InputWidgetView {
this._pane = new DropPane(item_els, {
target: this.group_el,
prevent_hide: this.input_el,
extra_stylesheets: [item_css.default, pane_css.default],
extra_stylesheets: [item_css.default, pane_css.default, this._style_menu],
})

this._update_ncols()
Expand Down Expand Up @@ -253,9 +243,9 @@ export namespace PaletteSelect {
export type Props = InputWidget.Props & {
value: p.Property<string>
items: p.Property<Item[]>
ncols: p.Property<number>
swatch_width: p.Property<number>
swatch_height: p.Property<number | "auto">
ncols: p.Property<number>
}
}

Expand All @@ -272,14 +262,12 @@ export class PaletteSelect extends InputWidget {
static {
this.prototype.default_view = PaletteSelectView

this.define<PaletteSelect.Props>(({Int, Str, List, NonNegative, Positive, Or, Auto}) => {
return {
value: [ Str ],
items: [ List(Item) ],
swatch_width: [ NonNegative(Int), 100 ],
swatch_height: [ Or(Auto, NonNegative(Int)), "auto" ],
ncols: [ Positive(Int), 1 ],
}
})
this.define<PaletteSelect.Props>(({Int, Str, List, NonNegative, Positive, Or, Auto}) => ({
value: [ Str ],
items: [ List(Item) ],
ncols: [ Positive(Int), 1 ],
swatch_width: [ NonNegative(Int), 100 ],
swatch_height: [ Or(Auto, NonNegative(Int)), "auto" ],
}))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PaletteSelect bbox=[0, 0, 200, 56]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PaletteSelect bbox=[0, 0, 120, 31]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions bokehjs/test/integration/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,22 @@ describe("Widgets", () => {
await view.ready
})

it("with swatch_width=20px", async () => {
const obj = new PaletteSelect({value: "Magma", items, ncols: 3, swatch_width: 20})
const {view} = await display(obj, [500, 200])

await tap(view.input_el)
await view.ready
})

it("with swatch_height=50px", async () => {
const obj = new PaletteSelect({value: "Magma", items, ncols: 3, swatch_height: 50})
const {view} = await display(obj, [500, 400])

await tap(view.input_el)
await view.ready
})

it("with disabled=true", async () => {
const obj = new PaletteSelect({value: "Accent", items, disabled: true})
const {view} = await display(obj, [250, 50])
Expand Down
14 changes: 13 additions & 1 deletion bokehjs/test/unit/core/util/color.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {expect} from "assertions"

import {color2rgba, css4_parse, brightness, luminance} from "@bokehjs/core/util/color"
import {color2rgba, color2css, css4_parse, brightness, luminance} from "@bokehjs/core/util/color"

describe("core/util/color module", () => {
const halfgray = color2rgba("rgb(128, 128, 128)")
Expand Down Expand Up @@ -131,6 +131,18 @@ describe("core/util/color module", () => {
})
})

describe("implements color2css() function", () => {

it("which should support CSS colors and maintain their original form", () => {
expect(color2css("#00A1FF")).to.be.equal("#00A1FF")
expect(color2css("green")).to.be.equal("green")
expect(color2css("green", 0.2)).to.be.equal("rgb(0 128 0 / 0.2)")
expect(color2css(null)).to.be.equal("rgb(0 0 0 / 0)")
expect(color2css([255, 128, 0])).to.be.equal("rgb(255 128 0)")
expect(color2css([255, 128, 0, 0.2])).to.be.equal("rgb(255 128 0 / 0.2)")
})
})

describe("should support css4_parse() function", () => {
it("that supports 'transparent' keyword", () => {
expect(css4_parse("")).to.be.null
Expand Down
3 changes: 2 additions & 1 deletion examples/interaction/widgets/color_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
color_map0 = PaletteSelect(title="Choose palette:", value="Turbo", items=items0)
color_map1 = PaletteSelect(title="Choose palette (grid):", value="PuBu", items=items1, ncols=3)
color_map2 = PaletteSelect(title="Choose palette (disabled):", value="PuBu", items=items1, disabled=True)
color_map3 = PaletteSelect(title="Choose palette (large swatch):", value="PuBu", items=items1, ncols=3, swatch_height=50)

layout = column(color_map0, color_map1, color_map2)
layout = column(color_map0, color_map1, color_map2, color_map3)
show(layout)
20 changes: 15 additions & 5 deletions tests/unit/bokeh/io/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import sys
from typing import TYPE_CHECKING

# External imports
import PIL.Image

## External imports
if TYPE_CHECKING:
from selenium.webdriver.remote.webdriver import WebDriver
Expand Down Expand Up @@ -68,6 +71,13 @@ def webdriver_with_scale_factor(request: pytest.FixtureRequest):
finally:
webdriver_control.terminate(driver)

@pytest.fixture(scope="module", autouse=True)
def disable_max_image_pixels():
max_image_pixels = PIL.Image.MAX_IMAGE_PIXELS
PIL.Image.MAX_IMAGE_PIXELS = None
yield
PIL.Image.MAX_IMAGE_PIXELS = max_image_pixels

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -232,9 +242,9 @@ def plot(color: str):
'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="40" height="20">'
'<defs/>'
'<path fill="rgb(0,0,0)" stroke="none" paint-order="stroke" d="M 0 0 L 40 0 L 40 20 L 0 20 L 0 0 Z" fill-opacity="0"/>'
'<path fill="rgb(255,0,0)" stroke="none" paint-order="stroke" d="M 5.5 5.5 L 15.5 5.5 L 15.5 15.5 L 5.5 15.5 L 5.5 5.5 Z" fill-opacity="1"/>'
'<path fill="red" stroke="none" paint-order="stroke" d="M 5.5 5.5 L 15.5 5.5 L 15.5 15.5 L 5.5 15.5 L 5.5 5.5 Z"/>'
'<g transform="matrix(1, 0, 0, 1, 20, 0)">'
'<path fill="rgb(0,0,255)" stroke="none" paint-order="stroke" d="M 5.5 5.5 L 15.5 5.5 L 15.5 15.5 L 5.5 15.5 L 5.5 5.5 Z" fill-opacity="1"/>'
'<path fill="blue" stroke="none" paint-order="stroke" d="M 5.5 5.5 L 15.5 5.5 L 15.5 15.5 L 5.5 15.5 L 5.5 5.5 Z"/>'
'</g>'
'</svg>',
]
Expand Down Expand Up @@ -266,7 +276,7 @@ def p(color: str):
return plot

[svg] = bie.get_svg(row([p("red"), p("blue")]), driver=webdriver)
assert len(re.findall(r'fill="rgb\(47,63,79\)"', svg)) == 2
assert len(re.findall(r'fill="#2f3f4f"', svg)) == 2
finally:
state.reset()

Expand Down Expand Up @@ -298,11 +308,11 @@ def plot(color: str):
svgs2 = [
'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20">'
'<defs/>'
'<path fill="rgb(255,0,0)" stroke="none" paint-order="stroke" d="M 5.5 5.5 L 15.5 5.5 L 15.5 15.5 L 5.5 15.5 L 5.5 5.5 Z" fill-opacity="1"/>'
'<path fill="red" stroke="none" paint-order="stroke" d="M 5.5 5.5 L 15.5 5.5 L 15.5 15.5 L 5.5 15.5 L 5.5 5.5 Z"/>'
'</svg>',
'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20">'
'<defs/>'
'<path fill="rgb(0,0,255)" stroke="none" paint-order="stroke" d="M 5.5 5.5 L 15.5 5.5 L 15.5 15.5 L 5.5 15.5 L 5.5 5.5 Z" fill-opacity="1"/>'
'<path fill="blue" stroke="none" paint-order="stroke" d="M 5.5 5.5 L 15.5 5.5 L 15.5 15.5 L 5.5 15.5 L 5.5 5.5 Z"/>'
'</svg>',
]

Expand Down

0 comments on commit 4f93515

Please sign in to comment.