3D surface plots using Bokeh in Python

Bokeh is a Python library used for creating interactive visualizations in a web browser. It provides powerful tools that offer flexibility, interactivity, and scalability for exploring various data insights.

How are 3D plots possible in Bokeh?

Bokeh directly does not offer 3D visuals; however, it allows integrating the external code in a code block that can use the bokeh modules, create 3D models and display them using the Bokeh functionalities.

We can use the vis.js library and write the code for the model using TypeScript. TypeScript code is wrapped in the Python Bokeh code and helps it to display the models to the user.

The java script code is wrapped in Python code.
The java script code is wrapped in Python code.

What is vis.js?

vis.js is a dynamic data visualization library solely based on the browser. It is user-friendly and allows users to handle lengthy dynamic data efficiently and also provides interactivity features. It consists of various components, including the Graph3d, which we will use in this code.

What is TypeScript?

TypeScript is a strongly typed programming language that builds on JavaScript that uses a type interface to offer better tooling without extra code. It incorporates additional syntax to JavaScript to improve the integration with the editor.

Summary

Used in code block

What is it?

vis.js

A dynamic, browser based library for visuals.

TypeScript

A strongly typed programming language that builds on JavaScript,

Required imports

We import modules from the Bokeh library in the Python code and then import the required modules from them in the TypeScript code block.

import numpy as np
from bokeh.io import output_file, save, show
from bokeh.core.properties import Instance, String
from bokeh.models import ColumnDataSource, LayoutDOM
from bokeh.util.compiler import TypeScript

  • numpy: To generate data for visualizations and array manipulation.

  • bokeh.io: To control the output and display of the plots. We specifically import output_file, save and show methods.

  • bokeh.core.properties: To assign properties of the model. We specifically import Instance to specify the data source type and String to specify x and y types and properties of z.

  • bokeh.models: To create highly customized visualizations in Bokeh. We specifically import ColumnDataSource and LayoutDOM.

  • bokeh.util.compiler: To integrate custom code block in the Bokeh plot. We specifically import TypeScript to define the TypeScript implementation in the code.

import {LayoutDOM, LayoutDOMView} from "models/layouts/layout_dom"
import {ColumnDataSource} from "models/sources/column_data_source"
import * as p from "core/properties"

  • LayoutDOM:  Import from the Bokeh model class to define custom layout components through subclassing and adding additional properties.

  • LayoutDOM:  Import from the Bokeh model class to customize the rendering and interaction behaviors of the layout component.

  • ColumnDataSource:  Import from the Bokeh column data source model to store and provide the data for the 3D surface plot. It allows the plot to access and visualize the x, y, and z coordinates of the surface.

  • p:  Import all from the Bokeh properties class to define the plot properties inside TypeScript.

Expected output

  • A 3D surface plot is created on the x, y, and z axis and represents three-dimensional coordinate points.

A 3D surface plot.
A 3D surface plot.

  • Allows an ariel view of the plot to interpret the coordinate densities on the three-dimensional grid.

Ariel view of a 3D surface plot.
Ariel view of a 3D surface plot.

  • Allow to zoom in on specific coordinates for a more precise visual knowledge of the three-dimensional grid.

Zoomed view of a 3D surface plot.
Zoomed view of a 3D surface plot.

Let's code this 3D surface block using the above-mentioned libraries and languages.

Example code

In this code, we use the Bokeh modules and wrap TypeScript code inside the Python code to create a 3D surface plot.

import numpy as np
from bokeh.io import output_file, save, show
from bokeh.core.properties import Instance, String
from bokeh.models import ColumnDataSource, LayoutDOM
from bokeh.util.compiler import TypeScript

CODE = """
import {LayoutDOM, LayoutDOMView} from "models/layouts/layout_dom"
import {ColumnDataSource} from "models/sources/column_data_source"
import * as p from "core/properties"

declare namespace vis {
  class Graph3d {
    constructor(el: HTMLElement | DocumentFragment, data: object, OPTIONS: object)
    setData(data: vis.DataSet): void
  }

  class DataSet {
    add(data: unknown): void
  }
}

const OPTIONS = {
  width: '650px',
  height: '600px',
  style: 'surface',
  showPerspective: true,
  showGrid: true,
  keepAspectRatio: true,
  verticalRatio: 1.0,
  cameraPosition: {
    horizontal: -0.35,
    vertical: 0.22,
    distance: 1.8,
  },
}

export class Surface3dView extends LayoutDOMView {
  declare model: Surface3d

  private _graph: vis.Graph3d

  initialize(): void {
    super.initialize()

    const url = "https://cdnjs.cloudflare.com/ajax/libs/vis/4.16.1/vis.min.js"
    const script = document.createElement("script")
    script.onload = () => this._init()
    script.async = false
    script.src = url
    document.head.appendChild(script)
  }

  private _init(): void {

    this._graph = new vis.Graph3d(this.shadow_el, this.get_data(), OPTIONS)

    this.connect(this.model.data_source.change, () => {
      this._graph.setData(this.get_data())
    })
  }

  get_data(): vis.DataSet {
    const data = new vis.DataSet()
    const source = this.model.data_source
    for (let i = 0; i < source.get_length()!; i++) {
      data.add({
        x: source.data[this.model.x][i],
        y: source.data[this.model.y][i],
        z: source.data[this.model.z][i],
      })
    }
    return data
  }

  get child_models(): LayoutDOM[] {
    return []
  }
}

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

  export type Props = LayoutDOM.Props & {
    x: p.Property<string>
    y: p.Property<string>
    z: p.Property<string>
    data_source: p.Property<ColumnDataSource>
  }
}

export interface Surface3d extends Surface3d.Attrs {}

export class Surface3d extends LayoutDOM {
  declare properties: Surface3d.Props
  declare __view_type__: Surface3dView

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

  static __name__ = "Surface3d"

  static {
    
    this.prototype.default_view = Surface3dView


    this.define<Surface3d.Props>(({String, Ref}) => ({
      x:[ String ],
      y:[ String ],
      z:[ String ],
      data_source:[Ref(ColumnDataSource)],
    }))
  }
}
"""

class Surface3d(LayoutDOM):

    __implementation__ = TypeScript(CODE)

    data_source = Instance(ColumnDataSource)

    x = String()
    y = String()
    z = String()


x = np.arange(0, 300, 10)
y = np.arange(0, 300, 10)

xx, yy = np.meshgrid(x, y)
xx = xx.ravel()
yy = yy.ravel()

value = np.sin(xx / 50) * np.cos(yy / 50) * 50 + 50

source = ColumnDataSource(data=dict(x=xx, y=yy, z=value))

surface = Surface3d(x="x", y="y", z="z", 
                    data_source=source, 
                    width=680, 
                    height=600)

#display output
output_file("output.html")
show(surface)
Creating a 3D surface plot.

Code explanation for Python code

  • Lines 1–5: Import all the necessary libraries and modules.

  • Line 7: Create a CODE variable that contains the TypeScript code required to create the interactive plot using the vis.js library.

Note: The TypeScript code in the CODE block is explained below.

  • Line 119: Define the Surface3d class as a subclass of LayoutDOM that serves as the interface between Python and TypeScript implementation.

  • Lines 121–123: Assign the TypeScript code defined in the CODE variable and Instance defined in the ColumnDataSource to the __implementation__ and data_source attributes of the Surface3d class respectively.

  • Lines 125–127: Assign the String() instance to x, y, and z.

  • Lines 130–131: Create x and y arrays and use numpy to specify the range for the x and y coordinates, respectively.

  • Lines 133–135: Create xx and yy arrays, use meshgrid() from numpy to generate the surface plot grid for the x and y values and use ravel() to convert them into 1D arrays.

  • Lines 137: Compute the value array using the values from the xx and yy arrays.

  • Lines 139: Assign the ColumnDataSource, containing a data dictionary with x, y, and z arrays, to the source.

  • Lines 141–144: Create a Surface3d class object and pass the x, y, and z values, data_source, width and height as parameters.

  • Lines 147–148: Set the output to output.html to specify the endpoint where the display will appear and use show() to display the 3D surface plot.

Code explanation for TypeScript code

  • Lines 8–10: Import the bokeh modules from the Python code required in the TypeScript code block.

  • Lines 12–19: Define a namespace named vis that encapsulates the Graph3d class containing setData that takes a parameter of type via.DataSet, and the DataSet class containing add method that takes data as a parameter and add to the dataset.

  • Lines 23–34: Create an OPTIONS object and set the properties of the plot that control the appearance to customize visuals as required.

  • Lines 38–39: Define Surface3dView class, which extends the LayoutDOMView class and declare model attribute in it to reference the Surface3d model.

  • Lines 41: Create a _graph attribute that holds Graph3d instance, which represents the 3D surface plot rendered on the page.

  • Lines 43–51: Override the initialize() method, and inside it, save the CDN linklink for CDN that points to 14.16.1 version of the vis.js library on url attribute and save the dynamically created script element on script attribute.

  • Lines 54–59: Define the _init() method that initializes the _graph by creating a vis.Graph3d object and passing shadow, data, and OPTIONS as parameters, and set a listener using the connect() method to process and set the new data when there is a change in the Bokeh data source.

  • Lines 63–73: Define the get_data() method that converts model.data_source into the vis library format and returns data. It creates vis.DataSet instance iterates over the data, and use add() to assign individual data points with x, y, and z coordinates.

  • Lines 76–77: Override the child_models method that returns an empty array depicting the Surface3dView does not have any child models.

  • Line 81: Define a namespace named Surface3d that encapsulates the properties and attributes specific to the Surface3d plot.

  • Lines 82–88: Use LayoutDOM.Props to assign the inherited properties from it to Props and specify the attributes and additional properties for x, y, z, and data_source.

  • Line 92: Define the Surface3d interface to ensure the attributes conform to the Surface3d.Attrs type.

  • Lines 94–96: Define Surface3d class, which extends the LayoutDOM class and create properties to specify the properties and view_type to associate the model with Surface3dView.

  • Lines 98–102: Define a constructor to initialize the Surface3d model with the provided attributes and set its name Surface3d to __name__ attribute.

  • Lines 104–113: Define a static block as a default view and use define(), with Surface3d.Props type to define the model properties on x, y and z coordinates.

Code output

It displays a 3D surface plot with a mesh surface created by integrating the TypeScript code block.

Common query

Question

How can we run this TypeScript code block in our Python code?

Show Answer
Copyright ©2024 Educative, Inc. All rights reserved