Skip to content

Make charts adhere to viewport constraints.

dcbr requested to merge dcbr/vtk:multi_viewport_charts_fix into master

Overview

This MR fixes the following two issues throughout the Charts API:

  • Properly set vtkContextMouseEvent.ScenePos and use these scene coordinates instead of screen coordinates in the Charts API. This resolves the interaction bug in the multiple viewport case (see #18763 (closed)).
  • Use the Scene size instead of the RenderWindow size throughout the Charts API. This ensures vtkChartBox and vtkChartParallelCoordinates respect the viewport (and scene) dimensions. Additionally, tooltips are properly kept within the viewport/scene.

Resolves #18763 (closed).

Details

  1. An extra Origin attribute is added to vtkContextScene, which is set in vtkContextActor (similar to the Geometry attribute) to match the Renderer's origin.
  2. In vtkContextInteractorStyle.ConstructMouseEvent this Origin attribute is used to calculate the correct ScenePos. Before this MR, the ScenePos was exactly the same as the ScreenPos, which caused the interaction bug described in #18763 (closed).
  3. All occurrences of vtkContextMouseEvent.GetScreenPos() are replaced by vtkContextMouseEvent.GetScenePos() to use the local scene coordinates instead. The only two exceptions are for vtkChartBoxData.ScreenPosition and vtkChartPlotData.ScreenPosition, which obviously still use the screen coordinates. I also haven't changed any tests accessing or setting the screen coordinates yet; this might still be necessary if they are broken after these changes.
  4. All occurrences of vtkContextScene.GetViewWidth and vtkContextScene.GetViewHeight are replaced by vtkContextScene.GetSceneWidth and vtkContextScene.GetSceneHeight. As they are no longer used now, they could be removed but I wasn't sure about that yet.

Example

The table below can be reproduced using this python script
import vtk


def create_xy_chart():
    data = [(1, 2), (2, 0), (3, 1)]
    chart = vtk.vtkChartXY()

    table = vtk.vtkTable()
    arrX = vtk.vtkFloatArray()
    arrX.SetName(f"X")
    arrY = vtk.vtkFloatArray()
    arrY.SetName(f"Y")
    table.AddColumn(arrX)
    table.AddColumn(arrY)

    numPoints = len(data)
    table.SetNumberOfRows(numPoints)
    for p in range(numPoints):
        table.SetValue(p, 0, data[p][0])
        table.SetValue(p, 1, data[p][1])

    plot = chart.AddPlot(vtk.vtkChart.POINTS)
    plot.SetInputData(table, 0, 1)
    return chart


def create_xyz_chart():
    data = [(1, 2, 3), (2, 0, 1), (3, 1, 2)]
    chart = vtk.vtkChartXYZ()

    table = vtk.vtkTable()
    arrX = vtk.vtkFloatArray()
    arrX.SetName(f"X")
    arrY = vtk.vtkFloatArray()
    arrY.SetName(f"Y")
    arrZ = vtk.vtkFloatArray()
    arrZ.SetName(f"Z")
    table.AddColumn(arrX)
    table.AddColumn(arrY)
    table.AddColumn(arrZ)

    numPoints = len(data)
    table.SetNumberOfRows(numPoints)
    for p in range(numPoints):
        table.SetValue(p, 0, data[p][0])
        table.SetValue(p, 1, data[p][1])
        table.SetValue(p, 2, data[p][2])

    plot = vtk.vtkPlotPoints3D()
    plot.SetInputData(table)
    chart.AddPlot(plot)
    chart.SetMargins(vtk.vtkVector4i(40, 40, 40, 40))
    return chart


def create_parallel_chart():
    data = [(1, 2, 3), (2, 0, 1), (3, 1, 2)]
    chart = vtk.vtkChartParallelCoordinates()

    table = vtk.vtkTable()
    numFields = len(data)
    for f in range(numFields):
        arr = vtk.vtkFloatArray()
        arr.SetName(f"Field{f}")
        table.AddColumn(arr)

    numPoints = len(data[0])
    table.SetNumberOfRows(numPoints)
    for f in range(numFields):
        for p in range(numPoints):
            table.SetValue(p, f, data[f][p])

    plot = chart.GetPlot(0)
    plot.SetInputData(table)
    return chart


def create_2dhist_chart():
    data = [[0, 1, 2], [1, 2, 3], [2, 3, 4]]
    chart = vtk.vtkChartHistogram2D()

    img = vtk.vtkImageData()
    img.SetExtent(0, len(data)-1, 0, len(data[0])-1, 0, 0)
    img.AllocateScalars(vtk.VTK_INT, 1)
    for i in range(len(data)):
        for j in range(len(data[0])):
            img.SetScalarComponentFromFloat(i, j, 0, 0, data[i][j])

    cs = vtk.vtkColorSeries()
    cs.SetColorScheme(vtk.vtkColorSeries.BREWER_DIVERGING_SPECTRAL_11)
    numColors = cs.GetNumberOfColors()
    colors = [c / 255 for i in range(numColors) for c in cs.GetColor(i)]
    tf = vtk.vtkColorTransferFunction()
    tf.BuildFunctionFromTable(0, 4, numColors, colors)

    chart.SetInputData(img)
    chart.SetTransferFunction(tf)
    return chart


def create_box_chart():
    data = [0, 1, 1, 2, 3, 3, 4]
    chart = vtk.vtkChartBox()

    dataTable = vtk.vtkTable()
    arrD = vtk.vtkIntArray()
    arrD.SetName(f"Data")
    dataTable.AddColumn(arrD)
    labels = vtk.vtkStringArray()
    labels.InsertNextValue(f"Data")

    numData = len(data)
    dataTable.SetNumberOfRows(numData)
    for p in range(numData):
        dataTable.SetValue(p, 0, data[p])
    quartiles = vtk.vtkComputeQuartiles()
    quartiles.SetInputData(dataTable)
    quartiles.Update()

    plot = chart.GetPlot(0)
    plot.SetInputData(quartiles.GetOutput())
    plot.SetLabels(labels)
    chart.SetColumnVisibilityAll(True)
    return chart


def create_pie_chart():
    data = [1, 2, 3, 4]
    chart = vtk.vtkChartPie()

    table = vtk.vtkTable()
    arrS = vtk.vtkFloatArray()
    arrS.SetName(f"Segments")
    table.AddColumn(arrS)
    labels = vtk.vtkStringArray()

    numPoints = len(data)
    table.SetNumberOfRows(numPoints)
    for p in range(numPoints):
        table.SetValue(p, 0, data[p])
        labels.InsertNextValue(f"Segment {p}")

    plot = chart.AddPlot(0)
    plot.SetInputData(table)
    plot.SetInputArray(0, f"Segments")
    plot.SetLabels(labels)
    return chart


if __name__ == "__main__":
    width, height = 600, 600
    viewport_min = (0.1, 0.1)
    viewport_max = (0.9, 0.9)
    chart_type = "xy"

    renwin = vtk.vtkRenderWindow()
    renwin.SetSize(width, height)
    iren = vtk.vtkRenderWindowInteractor()
    iren.SetRenderWindow(renwin)

    renderer = vtk.vtkRenderer()
    renderer.SetBackground([1, 1, 1])
    # x_min, y_min, x_max, y_max
    renderer.SetViewport(*viewport_min, *viewport_max)
    renwin.AddRenderer(renderer)

    chart_scene = vtk.vtkContextScene()
    chart_actor = vtk.vtkContextActor()
    chart_actor.SetScene(chart_scene)
    renderer.AddActor(chart_actor)
    chart_scene.SetRenderer(renderer)

    chart = {
        "xy": create_xy_chart,
        "xyz": create_xyz_chart,
        "parallel": create_parallel_chart,
        "2dhist": create_2dhist_chart,
        "box": create_box_chart,
        "pie": create_pie_chart
    }[chart_type]()
    chart_scene.AddItem(chart)

    style = vtk.vtkContextInteractorStyle()
    style.SetScene(chart_scene)
    iren.SetInteractorStyle(style)
    renwin.Render()
    iren.Start()
Chart Before After
vtkChartXY image image
vtkChartXYZ image image
vtkChartParallelCoordinates image image
vtkChartHistogram2D image image
vtkChartBox image image
vtkChartPie image image
Edited by dcbr

Merge request reports