Skip to content

Commit

Permalink
Add support for click-style pan tools (bokeh#14033)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpap authored Aug 23, 2024
1 parent a73f0bd commit 496611f
Show file tree
Hide file tree
Showing 19 changed files with 305 additions and 2 deletions.
21 changes: 21 additions & 0 deletions bokehjs/src/less/icons.less
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@
.tool-icon-mask(var(--bokeh-icon-y-pan));
}

.bk-tool-icon-pan-left {
.tool-icon-mask(var(--bokeh-icon-pan-left));
}

.bk-tool-icon-pan-right {
.tool-icon-mask(var(--bokeh-icon-pan-right));
}

.bk-tool-icon-pan-up {
.tool-icon-mask(var(--bokeh-icon-pan-up));
}

.bk-tool-icon-pan-down {
.tool-icon-mask(var(--bokeh-icon-pan-down));
}

.bk-tool-icon-range {
.tool-icon(--bokeh-icon-range, "Range");
}
Expand Down Expand Up @@ -245,6 +261,11 @@
--bokeh-icon-x-pan: data-uri("icons/x-pan.svg");
--bokeh-icon-y-pan: data-uri("icons/y-pan.svg");

--bokeh-icon-pan-left: data-uri("icons/pan_left.svg");
--bokeh-icon-pan-right: data-uri("icons/pan_right.svg");
--bokeh-icon-pan-up: data-uri("icons/pan_up.svg");
--bokeh-icon-pan-down: data-uri("icons/pan_down.svg");

--bokeh-icon-box-select: data-uri("icons/box-select.svg");
--bokeh-icon-x-box-select: data-uri("icons/x-box-select.svg");
--bokeh-icon-y-box-select: data-uri("icons/y-box-select.svg");
Expand Down
3 changes: 3 additions & 0 deletions bokehjs/src/less/icons/pan_down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions bokehjs/src/less/icons/pan_left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions bokehjs/src/less/icons/pan_right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions bokehjs/src/less/icons/pan_up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions bokehjs/src/lib/core/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ export type OutputBackend = typeof OutputBackend["__type__"]
export const PaddingUnits = Enum("percent", "absolute")
export type PaddingUnits = typeof PaddingUnits["__type__"]

export const PanDirection = Enum(
"left", "right", "up", "down",
"west", "east", "north", "south",
)
export type PanDirection = typeof PanDirection["__type__"]

export const Place = Enum("above", "below", "left", "right", "center")
export type Place = typeof Place["__type__"]

Expand Down
4 changes: 4 additions & 0 deletions bokehjs/src/lib/models/ranges/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export abstract class Range extends Model {
return isFinite(this.min) && isFinite(this.max)
}

get interval(): [number, number] {
return [this.start, this.end]
}

get span(): number {
return Math.abs(this.end - this.start)
}
Expand Down
108 changes: 108 additions & 0 deletions bokehjs/src/lib/models/tools/actions/click_pan_tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {PlotActionTool, PlotActionToolView} from "./plot_action_tool"
import {Float} from "core/kinds"
import {PanDirection} from "core/enums"
import type * as p from "core/properties"
import * as icons from "styles/icons.css"
import {update_ranges} from "../gestures/pan_tool"

export class ClickPanToolView extends PlotActionToolView {
declare model: ClickPanTool

doit(): void {
const direction = (() => {
switch (this.model.direction) {
case "left":
case "west":
return {x: -1, y: 0}
case "right":
case "east":
return {x: +1, y: 0}
case "up":
case "north":
return {x: 0, y: -1}
case "down":
case "south":
return {x: 0, y: +1}
}
})()

const {frame} = this.plot_view
const {factor} = this.model

const x_offset = direction.x*factor*frame.bbox.width
const y_offset = direction.y*factor*frame.bbox.height

const bbox = frame.bbox.translate(x_offset, y_offset)

const xrs = update_ranges(frame.x_scales, bbox.x0, bbox.x1)
const yrs = update_ranges(frame.y_scales, bbox.y0, bbox.y1)
this.plot_view.update_range({xrs, yrs}, {panning: true})
}
}

export namespace ClickPanTool {
export type Attrs = p.AttrsOf<Props>

export type Props = PlotActionTool.Props & {
direction: p.Property<PanDirection>
factor: p.Property<number>
}
}

export interface ClickPanTool extends ClickPanTool.Attrs {}

export class ClickPanTool extends PlotActionTool {
declare properties: ClickPanTool.Props
declare __view_type__: ClickPanToolView

constructor(attrs?: Partial<ClickPanTool.Attrs>) {
super(attrs)
}

static {
this.prototype.default_view = ClickPanToolView

this.define<ClickPanTool.Props>(() => ({
direction: [ PanDirection ],
factor: [ Float, 0.1 ],
}))

this.register_alias("pan_left", () => new ClickPanTool({direction: "left"}))
this.register_alias("pan_right", () => new ClickPanTool({direction: "right"}))
this.register_alias("pan_up", () => new ClickPanTool({direction: "up"}))
this.register_alias("pan_down", () => new ClickPanTool({direction: "down"}))

this.register_alias("pan_west", () => new ClickPanTool({direction: "west"}))
this.register_alias("pan_east", () => new ClickPanTool({direction: "east"}))
this.register_alias("pan_north", () => new ClickPanTool({direction: "north"}))
this.register_alias("pan_south", () => new ClickPanTool({direction: "south"}))
}

override tool_name = "Click Pan"

override get tooltip(): string {
return `Pan ${this.direction}`
}

override get computed_icon(): string {
const icon = super.computed_icon
if (icon != null) {
return icon
} else {
switch (this.direction) {
case "left":
case "west":
return `.${icons.tool_icon_pan_left}`
case "right":
case "east":
return `.${icons.tool_icon_pan_right}`
case "up":
case "north":
return `.${icons.tool_icon_pan_up}`
case "down":
case "south":
return `.${icons.tool_icon_pan_down}`
}
}
}
}
3 changes: 2 additions & 1 deletion bokehjs/src/lib/models/tools/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ export {CopyTool} from "./copy_tool"
export {CustomAction} from "./custom_action"
export {FullscreenTool} from "./fullscreen_tool"
export {HelpTool} from "./help_tool"
export {ExamineTool} from "./examine_tool"
export {ExamineTool} from "./examine_tool"
export {ClickPanTool} from "./click_pan_tool"
export {RedoTool} from "./redo_tool"
export {ResetTool} from "./reset_tool"
export {SaveTool} from "./save_tool"
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
FlexDiv bbox=[0, 0, 300, 262]
FlexDiv bbox=[0, 0, 300, 296]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions bokehjs/test/unit/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe("default model resolver", () => {
"CheckboxGroup",
"Circle",
"ClickButton",
"ClickPanTool",
"CloseDialog",
"ColorBar",
"ColorMapper",
Expand Down
4 changes: 4 additions & 0 deletions bokehjs/test/unit/core/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ describe("enums module", () => {
expect([...enums.PaddingUnits]).to.be.equal(["percent", "absolute"])
})

it("should have PanDirection", () => {
expect([...enums.PanDirection]).to.be.equal(["left", "right", "up", "down", "west", "east", "north", "south"])
})

it("should have Place", () => {
expect([...enums.Place]).to.be.equal(["above", "below", "left", "right", "center"])
})
Expand Down
90 changes: 90 additions & 0 deletions bokehjs/test/unit/models/tools/actions/click_pan_tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {expect} from "assertions"
import {display} from "../../../_util"

import {ClickPanTool} from "@bokehjs/models/tools/actions/click_pan_tool"
import {Range1d} from "@bokehjs/models/ranges/range1d"
import {Plot} from "@bokehjs/models/plots/plot"
import type {PanDirection} from "@bokehjs/core/enums"

describe("ClickPanTool", () => {

async function mkplot(direction: PanDirection) {
const plot = new Plot({
x_range: new Range1d({start: 0, end: 1}),
y_range: new Range1d({start: 0, end: 1}),
})
const tool = new ClickPanTool({direction, factor: 0.2})
plot.add_tools(tool)
const {view: plot_view} = await display(plot)
const tool_view = plot_view.owner.get_one(tool)
return {plot_view, tool_view}
}

it("should translate x-range left", async () => {
const {plot_view, tool_view} = await mkplot("left")
const {x_range, y_range} = plot_view.frame

tool_view.doit()
expect(x_range.interval).to.be.similar([-0.2, 0.8])
expect(y_range.interval).to.be.similar([ 0.0, 1.0])

tool_view.doit()
expect(x_range.interval).to.be.similar([-0.4, 0.6])
expect(y_range.interval).to.be.similar([ 0.0, 1.0])

tool_view.doit()
expect(x_range.interval).to.be.similar([-0.6, 0.4])
expect(y_range.interval).to.be.similar([ 0.0, 1.0])
})

it("should translate x-range right", async () => {
const {plot_view, tool_view} = await mkplot("right")
const {x_range, y_range} = plot_view.frame

tool_view.doit()
expect(x_range.interval).to.be.similar([0.2, 1.2])
expect(y_range.interval).to.be.similar([0.0, 1.0])

tool_view.doit()
expect(x_range.interval).to.be.similar([0.4, 1.4])
expect(y_range.interval).to.be.similar([0.0, 1.0])

tool_view.doit()
expect(x_range.interval).to.be.similar([0.6, 1.6])
expect(y_range.interval).to.be.similar([0.0, 1.0])
})

it("should translate y-range up", async () => {
const {plot_view, tool_view} = await mkplot("up")
const {x_range, y_range} = plot_view.frame

tool_view.doit()
expect(x_range.interval).to.be.similar([0.0, 1.0])
expect(y_range.interval).to.be.similar([0.2, 1.2])

tool_view.doit()
expect(x_range.interval).to.be.similar([0.0, 1.0])
expect(y_range.interval).to.be.similar([0.4, 1.4])

tool_view.doit()
expect(x_range.interval).to.be.similar([0.0, 1.0])
expect(y_range.interval).to.be.similar([0.6, 1.6])
})

it("should translate y-range down", async () => {
const {plot_view, tool_view} = await mkplot("down")
const {x_range, y_range} = plot_view.frame

tool_view.doit()
expect(x_range.interval).to.be.similar([ 0.0, 1.0])
expect(y_range.interval).to.be.similar([-0.2, 0.8])

tool_view.doit()
expect(x_range.interval).to.be.similar([ 0.0, 1.0])
expect(y_range.interval).to.be.similar([-0.4, 0.6])

tool_view.doit()
expect(x_range.interval).to.be.similar([ 0.0, 1.0])
expect(y_range.interval).to.be.similar([-0.6, 0.4])
})
})
1 change: 1 addition & 0 deletions docs/bokeh/source/docs/releases/3.6.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Bokeh version ``3.6.0`` (??? 2024) is a minor milestone of Bokeh project.
* Improved streaming corner cases and added NumPy to ``bokeh info`` (:bokeh-pull:`14007`)
* Added support for "open in a new tab" mode to ``SaveTool`` (:bokeh-pull:`14031`)
* Allowed values of any type in ``GroupFilter.group`` and improved equality checking (:bokeh-pull:`14034`)
* Added support for click-style pan tools (:bokeh-pull:`14033`)
5 changes: 5 additions & 0 deletions src/bokeh/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ class MyModel(Model):
'OutputBackend',
'PaddingUnits',
'Palette',
'PanDirection',
'Place',
'RegionSelectionMode',
'RenderLevel',
Expand Down Expand Up @@ -486,6 +487,10 @@ def enumeration(*values: Any, case_sensitive: bool = True, quote: bool = False)
PaddingUnitsType = Literal["percent", "absolute"]
PaddingUnits = enumeration(PaddingUnitsType)

#: Which direction click pan tool acts on.
PanDirection = Literal["left", "right", "up", "down", "west", "east", "north", "south"]
PanDirection = enumeration(PanDirection)

#: Specify the name of a palette from :ref:`bokeh.palettes`
Palette = enumeration(*palettes.__palettes__)

Expand Down
Loading

0 comments on commit 496611f

Please sign in to comment.