Edit Tool Data Source Manipulation

Hi, I’m completely new to this in many ways, so hopefully I’ve given enough to work with here and it’s not too basic a question. Also not sure if this belongs here or under the development section.

I’m trying to manipulate my data source in a custom edit tool extension. Specifically, I’m trying to modify _drag_points from https://github.com/bokeh/bokeh/blob/branch-2.2/bokehjs/src/lib/models/tools/edit/edit_tool.ts. However, I’m not sure what’s going on here (lines 123-125): if (ykey && (dim == "height" || dim == "both")) { cds.data[ykey][index] += dy }

After some debugging, it looks like ykey is just a string ‘y’. Besides not understanding this being used in a conditional, how does this work with my CDS data? My CDS doesn’t have a column named ‘y’ although this code works fine. This becomes a problem because my glyph draws x and y values from two columns in my CDS (I can manipulate these two columns no problem), but I want to manipulate other glyphs that draw from the same CDS but different columns. I’ve tried to do some tracing on my own with the bokehjs source but have run into a dead end.

Thanks in advance for any help!

The value of xkey and ykey come from the glyph that the edit tool controls:

const [xkey, ykey] = [glyph.x.field, glyph.y.field]

These can be null or undefined if the glyph is configured to a fixed constant value, but if it is pointed at a CDS column, then the value is the configured column name.

Right, so I have no problem just accessing these keys for the current glyph being dragged as well as other glyphs in these same two x and y columns. But does that mean there’s no way to drag a glyph with source columns x and y and use that to edit columns a and b in the same CDS for example? I can try to provide a minimal example if my request seems kind of odd but haven’t posted one yet to avoid the headache of analyzing a lengthy custom TS implementation.

@thekillertomato I’m not sure what you are asking. The built-in edit tool was written to edit the coordinates of a glyph, so it edits the columns corresponding to those coordinates. If the glyph x-coordinate is configure to use the “foo” column of the CDS, then the tool will edit the “foo” column.

I’m simply trying to edit columns ‘bar’ and ‘baz’ instead of only ‘foo’ in my custom extension and don’t know how to access these columns. I can access ‘foo’ in your example with cds.data[xkey][0] += dx, but cds.data['bar'][0] += dx doesn’t do anything.

Please provide a minimal reproducible example.

This is honestly the shortest I can get it while still being runnable. I’ve commented ‘why does this work on its own’ and ‘but this just glitches?’ at the relevant lines and included the data table visual to hopefully clarify my issue with an absolute minimum data source. Thanks again if you get a chance to look at it.

from bokeh.plotting import figure, show, Column
from bokeh.io import curdoc
from bokeh.models import ColumnDataSource, CDSView, IndexFilter, Tool, PointDrawTool, DataTable, TableColumn, EditTool, \
    Drag, LineEditTool
from bokeh.models.callbacks import CustomJS
from bokeh.events import PointEvent, Tap, MouseMove
from bokeh.core.properties import Instance, Enum
from bokeh.util.compiler import TypeScript
from bokeh.models.renderers import GlyphRenderer
from bokeh.core.enums import Dimensions
from bokeh.core.validation import error

TS_CODE = """
import {LineEditTool, LineEditToolView} from "models/tools/edit/line_edit_tool"
import {UIEvent} from "core/ui_events"
import {GlyphRenderer} from "models/renderers/glyph_renderer"
import {XYGlyph} from "models/glyphs/xy_glyph"
import {Dimensions} from "core/enums"
import * as p from "core/properties"

export interface HasXYGlyph {
  glyph: XYGlyph
}

export class TestToolView extends LineEditToolView {
  model: TestTool
  _drag_points(ev: UIEvent, renderers: (GlyphRenderer & HasXYGlyph)[], dim: Dimensions = "both"): void {
    if (this._basepoint == null)
      return
    const [bx, by] = this._basepoint
    for (const renderer of renderers) {
      const basepoint = this._map_drag(bx, by, renderer)
      const point = this._map_drag(ev.sx, ev.sy, renderer)
      if (point == null || basepoint == null) {
        continue
      }
      const [x, y] = point
      const [px, py] = basepoint
      const [dx, dy] = [x-px, y-py]
      // Type once dataspecs are typed
      const glyph: any = renderer.glyph
      const cds = renderer.data_source
      const [xkey, ykey] = [glyph.x.field, glyph.y.field]
      for (const index of cds.selected.indices) {
        if (xkey && (dim == "width" || dim == "both")) {
          cds.data[xkey][index] += dx
        }
        if (ykey && (dim == "height" || dim == "both")) {
          cds.data[ykey][index] += dy //why does this work on its own
          cds.data['y1'][2] = 5 //but this just glitches?
        }
      }
      cds.change.emit()
    }
    this._basepoint = [ev.sx, ev.sy]
  }
}

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

  export type Props = LineEditTool.Props
}

export interface TestTool extends TestTool.Attrs {}

export class TestTool extends LineEditTool {
  properties: TestTool.Props
  __view_type__: TestToolView

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

  tool_name = "Test Tool"

  static init_TestTool(): void {
    this.prototype.default_view = TestToolView

    this.define<TestTool.Props>({
    })
  }
}

"""

class TestTool(LineEditTool):
    __implementation__ = TypeScript(TS_CODE)

plot = figure()

x = [1, 2, 3]
y1 = [1, 2, 3]
y2 = [3, 2, 1]
data = {'x': x, 'y1': y1, 'y2': y2}
source = ColumnDataSource(data)

testLine1 = plot.line(x = 'x', y = 'y1', source = source)
testLine2 = plot.line(x = 'x', y = 'y2', source = source)
circleGlyph = plot.circle([], [], size = 10)
testTool = TestTool(renderers = [testLine1, testLine2], intersection_renderer = circleGlyph)
plot.add_tools(testTool)

columns = [TableColumn(field='x', title='x'),
           TableColumn(field='y1', title='y1'),
           TableColumn(field='y2', title='y2')]
table = DataTable(source=source, columns=columns, editable=True, height=200)
show(Column(plot, table))

The cds.data['y1'][2] = 5 line doesn’t work because cds.data doesn’t have the y1 attribute. And the reason for this is that _drag_points receives a CDS for the intersection_renderer glyph. In your case, you don’t create a data source for the circles, so it’s created for you with the default columns x and y.

Thanks for the reply, that does make sense. My knowledge of the connections between Python and Typescript may be running short here, but do you happen to know how I would supply my source CDS to _drag_points? I know the CustomJS method but am unfamiliar with this one.

As I said: " _drag_points receives a CDS for the intersection_renderer glyph".

Just provide an explicit CDS for that call to plot.circle.

1 Like

Great solution, I didn’t realize that the CDS was tied to the intersection_renderer itself rather than through the JS side.