Repository: bokeh/bokeh

Issue:
[BUG] Typed Arrays returned as dicts
Run `examples/howto/events_app.py`. For a lasso selection the `"x"` and `"y"` arrays are returned as dicts:

```
selectiongeometry(geometry={
    "type":"poly",
    "sx":[544,533,531,527,526,526,531,534,545,555,564,570,570],
    "sy":[135,142,145,152,160,165,184,191,201,205,208,210,210],
    "x":{"0":99.67,"1":97.43,"2":97.02,"3":96.21,"4":96,"5":96,"6":97.02,"7":97.63,"8":99.88,"9":101.92,"10":103.75,"11":104.98,"12":104.98},
    "y":{"0":79.95,"1":78.6,"2":78.02,"3":76.68,"4":75.14,"5":74.17,"6":70.51,"7":69.16,"8":67.24,"9":66.47,"10":65.89,"11":65.5,"12":65.5}
}, final=false)
```

Have seen Typed Arrays get represented in this format it other contexts. I would not be surprised to learn that this is a general issue in other places as well. It would be nice to return as "ndarray" types but would also settle for plain lists. 

cc @mattpap 


Discussion:
The examples uses `JSON.stringify()` to "pretty-print" objects and this is the side effect, e.g.:
```ts
> JSON.stringify(new Float32Array([1,2,3]))
"{"0":1,"1":2,"2":3}"
```
When PR #10109 is merged, then one will be able to use `core/util/pretty` module instead, which produces much more readable results.
@mattpap it's more that that, the values come back to python callbacks in the `event` as dicts
Right, I've seen that in a few places in fact. Should have filed an issue earlier but I've actually added a utility to handle this case [in Panel](https://github.com/holoviz/panel/blob/master/panel/io/model.py#L81) and a conditional branch [in HoloViews](https://github.com/holoviz/holoviews/blob/master/holoviews/plotting/bokeh/callbacks.py#L1128).

Resolving patch:
diff --git a/examples/howto/events_app.py b/examples/howto/events_app.py
--- a/examples/howto/events_app.py
+++ b/examples/howto/events_app.py
@@ -20,17 +20,16 @@ def display_event(div, attributes=[]):
     """
     style = 'float: left; clear: left; font-size: 13px'
     return CustomJS(args=dict(div=div), code="""
-        var attrs = %s;
-        var args = [];
-        for (var i = 0; i<attrs.length; i++ ) {
-            var val = JSON.stringify(cb_obj[attrs[i]], function(key, val) {
-                return val.toFixed ? Number(val.toFixed(2)) : val;
-            })
+        const {to_string} = Bokeh.require("core/util/pretty")
+        const attrs = %s;
+        const args = [];
+        for (let i = 0; i<attrs.length; i++ ) {
+            const val = to_string(cb_obj[attrs[i]], {precision: 2})
             args.push(attrs[i] + '=' + val)
         }
-        var line = "<span style=%r><b>" + cb_obj.event_name + "</b>(" + args.join(", ") + ")</span>\\n";
-        var text = div.text.concat(line);
-        var lines = text.split("\\n")
+        const line = "<span style=%r><b>" + cb_obj.event_name + "</b>(" + args.join(", ") + ")</span>\\n";
+        const text = div.text.concat(line);
+        const lines = text.split("\\n")
         if (lines.length > 35)
             lines.shift();
         div.text = lines.join("\\n");


Test patch:
diff --git a/bokehjs/test/integration/regressions.ts b/bokehjs/test/integration/regressions.ts
--- a/bokehjs/test/integration/regressions.ts
+++ b/bokehjs/test/integration/regressions.ts
@@ -52,7 +52,7 @@ describe("Bug", () => {
   })
 
   describe("in issue #9703", () => {
-    it("disallows ImageURL glyph to set anchor and angle at the same time", async () => {
+    it.allowing(6)("disallows ImageURL glyph to set anchor and angle at the same time", async () => {
       const p = fig([300, 300], {x_range: [-1, 10], y_range: [-1, 10]})
 
       const svg = `\
diff --git a/bokehjs/test/unit/core/has_props.ts b/bokehjs/test/unit/core/has_props.ts
--- a/bokehjs/test/unit/core/has_props.ts
+++ b/bokehjs/test/unit/core/has_props.ts
@@ -6,6 +6,7 @@ import * as mixins from "@bokehjs/core/property_mixins"
 import * as p from "@bokehjs/core/properties"
 import {keys} from "@bokehjs/core/util/object"
 import {ndarray} from "@bokehjs/core/util/ndarray"
+import {BYTE_ORDER} from "@bokehjs/core/util/serialization"
 
 class TestModel extends HasProps {}
 
@@ -72,14 +73,7 @@ SubclassWithOptionalSpec.define<any>({
   baz: [ p.NumberSpec, {field: 'colname'} ],
 })
 
-describe("has_properties module", () => {
-
-  /*
-  before ->
-    Models.register('TestObject', TestModel)
-  after ->
-    Models.unregister('TestObject')
-  */
+describe("core/has_props module", () => {
 
   describe("creation", () => {
 
@@ -174,4 +168,48 @@ describe("has_properties module", () => {
       expect(struct.subtype).to.be.equal("bar")
     })
   })
+
+  describe("HasProps._value_to_json()", () => {
+    it("should support primitive values", () => {
+      expect(HasProps._value_to_json(1)).to.be.equal(1)
+      expect(HasProps._value_to_json("a")).to.be.equal("a")
+    })
+
+    it("should support HasProps instances", () => {
+      const obj0 = new TestModel()
+      expect(HasProps._value_to_json(obj0)).to.be.equal({id: obj0.id})
+
+      const obj1 = new SubSubclassWithProps({foo: 17})
+      expect(HasProps._value_to_json(obj1)).to.be.equal({id: obj1.id})
+    })
+
+    it("should support ndarrays", () => {
+      const nd0 = ndarray([1, 2, 3, 4, 5, 6], {dtype: "int32", shape: [2, 3]})
+
+      expect(HasProps._value_to_json(nd0)).to.be.equal({
+        __ndarray__: "AQAAAAIAAAADAAAABAAAAAUAAAAGAAAA",
+        order: BYTE_ORDER,
+        dtype: "int32",
+        shape: [2, 3],
+      })
+    })
+
+    it("should support typed arrays", () => {
+      expect(HasProps._value_to_json(new Float64Array([1, 2, 3]))).to.be.equal([1, 2, 3])
+    })
+
+    it("should support arrays and typed arrays", () => {
+      const obj0 = new TestModel()
+
+      expect(HasProps._value_to_json([1, 2, 3])).to.be.equal([1, 2, 3])
+      expect(HasProps._value_to_json([1, 2, obj0])).to.be.equal([1, 2, {id: obj0.id}])
+    })
+
+    it("should support plain objects", () => {
+      const obj0 = new TestModel()
+
+      expect(HasProps._value_to_json({a: 1, b: 2})).to.be.equal({a: 1, b: 2})
+      expect(HasProps._value_to_json({a: 1, b: obj0})).to.be.equal({a: 1, b: {id: obj0.id}})
+    })
+  })
 })
diff --git a/bokehjs/test/unit/core/util/types.ts b/bokehjs/test/unit/core/util/types.ts
--- a/bokehjs/test/unit/core/util/types.ts
+++ b/bokehjs/test/unit/core/util/types.ts
@@ -116,6 +116,9 @@ describe("core/util/types module", () => {
     expect(isTypedArray(new Int32Array())).to.be.true
     expect(isTypedArray(new Float32Array())).to.be.true
     expect(isTypedArray(new Float64Array())).to.be.true
+    const buffer = new ArrayBuffer(10)
+    expect(isTypedArray(new DataView(buffer))).to.be.false
+    expect(isTypedArray(buffer)).to.be.false
     expect(isTypedArray(new Map())).to.be.false
     expect(isTypedArray(new Set())).to.be.false
     expect(isTypedArray(new X())).to.be.false