diff --git a/flaskapp/static/img/model_thumbnails/BoatBase/graph-model.png b/flaskapp/static/img/model_thumbnails/BoatBase/graph-model.png
index e4d76ea5bb471ff3b1494bba3c6d563878725e4a..1b15b4fb7f07b63fb31416088f9e5fd42e8e935e 100644
Binary files a/flaskapp/static/img/model_thumbnails/BoatBase/graph-model.png and b/flaskapp/static/img/model_thumbnails/BoatBase/graph-model.png differ
diff --git a/flaskapp/static/img/model_thumbnails/Canoe/graph-model.png b/flaskapp/static/img/model_thumbnails/Canoe/graph-model.png
index 0fd207c398715f6b787d92e9d7af15ca2425e9ad..191faed786948313c9afcdc2accc743780d5b2e3 100644
Binary files a/flaskapp/static/img/model_thumbnails/Canoe/graph-model.png and b/flaskapp/static/img/model_thumbnails/Canoe/graph-model.png differ
diff --git a/flaskapp/static/img/model_thumbnails/CatFoil/graph-model.png b/flaskapp/static/img/model_thumbnails/CatFoil/graph-model.png
index 535e3a6a71b69be5fe7e7b61ae86fbf03dc72288..cac59e5a51c38761686a1fb3a67e72e28d8c29fc 100644
Binary files a/flaskapp/static/img/model_thumbnails/CatFoil/graph-model.png and b/flaskapp/static/img/model_thumbnails/CatFoil/graph-model.png differ
diff --git a/flaskapp/static/img/model_thumbnails/Catamaran/graph-model.png b/flaskapp/static/img/model_thumbnails/Catamaran/graph-model.png
index 95c1477ddf564bfbcf55ffb2961fac5c94126723..1c9bdf624c91f1bbf7abf1dc33dc8d58651a55c2 100644
Binary files a/flaskapp/static/img/model_thumbnails/Catamaran/graph-model.png and b/flaskapp/static/img/model_thumbnails/Catamaran/graph-model.png differ
diff --git a/flaskapp/static/img/model_thumbnails/Paperbot/graph-model.png b/flaskapp/static/img/model_thumbnails/Paperbot/graph-model.png
index 931d9b2fcca20aa119f69ae4ca9ddbaf1e85281a..5f7d9a961928b62b08819d730e4d6a80cad9a594 100644
Binary files a/flaskapp/static/img/model_thumbnails/Paperbot/graph-model.png and b/flaskapp/static/img/model_thumbnails/Paperbot/graph-model.png differ
diff --git a/flaskapp/static/img/model_thumbnails/RockerChair/graph-model.png b/flaskapp/static/img/model_thumbnails/RockerChair/graph-model.png
index caa9635e8c6732dbd80a18917d65e832e5d37911..84c183c4ecd6b29ff3189db11412825e55c2869e 100644
Binary files a/flaskapp/static/img/model_thumbnails/RockerChair/graph-model.png and b/flaskapp/static/img/model_thumbnails/RockerChair/graph-model.png differ
diff --git a/flaskapp/static/img/model_thumbnails/SimpleChair/graph-model.png b/flaskapp/static/img/model_thumbnails/SimpleChair/graph-model.png
index 0a78d5cd184b3e5f42f16e570303d2d825e2db11..ee1e8a2a5ab37de239d3f853e89da29247edf0aa 100644
Binary files a/flaskapp/static/img/model_thumbnails/SimpleChair/graph-model.png and b/flaskapp/static/img/model_thumbnails/SimpleChair/graph-model.png differ
diff --git a/flaskapp/static/img/model_thumbnails/SimpleTable/graph-model.png b/flaskapp/static/img/model_thumbnails/SimpleTable/graph-model.png
index 82916c2eca9324ddbd6fadf617ea56b5d9eab9cc..0efc9d26dc72b6b679b5e6f973a5d0837224fc68 100644
Binary files a/flaskapp/static/img/model_thumbnails/SimpleTable/graph-model.png and b/flaskapp/static/img/model_thumbnails/SimpleTable/graph-model.png differ
diff --git a/flaskapp/static/img/model_thumbnails/Stool/graph-model.png b/flaskapp/static/img/model_thumbnails/Stool/graph-model.png
index 9f349ed719d8281d3702b732d83cee4bee5bae5b..f9b3f6f5e6f9ea07062011ff7ec8889313120a47 100644
Binary files a/flaskapp/static/img/model_thumbnails/Stool/graph-model.png and b/flaskapp/static/img/model_thumbnails/Stool/graph-model.png differ
diff --git a/flaskapp/static/img/model_thumbnails/Trimaran/graph-model.png b/flaskapp/static/img/model_thumbnails/Trimaran/graph-model.png
index 83e008d2bfeafaf6c8c294191b851f54daa33e08..af5afcf6c454b3d0589d6d54e7ec8449709e68cb 100644
Binary files a/flaskapp/static/img/model_thumbnails/Trimaran/graph-model.png and b/flaskapp/static/img/model_thumbnails/Trimaran/graph-model.png differ
diff --git a/flaskapp/static/img/model_thumbnails/Tug/graph-model.png b/flaskapp/static/img/model_thumbnails/Tug/graph-model.png
index 2e4452331a93ecb29426563a2c9fb813dbc673f3..dbeb038a571243aaf2b7100ab8633ce9fa39f775 100644
Binary files a/flaskapp/static/img/model_thumbnails/Tug/graph-model.png and b/flaskapp/static/img/model_thumbnails/Tug/graph-model.png differ
diff --git a/requirements.txt b/requirements.txt
index 834bce3c0347430e312e7a5079a6a4e0302cdcb7..c46c73f30fb16772d08a0ddb460a5510de3d329d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,4 +13,5 @@ Flask-Markdown
 jinja2-highlight
 networkx
 numpy-stl
-matplotlib
+plotly
+kaleido
diff --git a/rocolib/utils/display.py b/rocolib/utils/display.py
index 826dd821a527b7afd37198c431702b2699c0a277..5cc74c8a4e84ddafc5b5a70463708e6ea6f6bafc 100644
--- a/rocolib/utils/display.py
+++ b/rocolib/utils/display.py
@@ -3,10 +3,7 @@ import math
 import numpy
 import logging
 from stl import mesh
-from mpl_toolkits import mplot3d
-from matplotlib import pyplot
-from matplotlib.colors import LightSource
-from matplotlib import cm
+import plotly.graph_objects as go
 
 from rocolib.api.composables.graph.Drawing import *
 from rocolib.api.composables.graph.DrawingEdge import *
@@ -14,63 +11,76 @@ from rocolib.api.composables.graph.DrawingEdge import *
 
 log = logging.getLogger(__name__)
 
-def display3D(fh, ph=None, show=False):
+### copied from https://chart-studio.plotly.com/~empet/15276/converting-a-stl-mesh-to-plotly-gomes/#/
+def stl2mesh3d(stl_mesh):
+    # this function extracts the unique vertices and the lists I, J, K to define a Plotly mesh3d
+    p, q, r = stl_mesh.vectors.shape #(p, 3, 3)
+    # the array stl_mesh.vectors.reshape(p*q, r) can contain multiple copies of the same vertex;
+    # extract unique vertices from all mesh triangles
+    vertices, ixr = numpy.unique(stl_mesh.vectors.reshape(p*q, r), return_inverse=True, axis=0)
+    I = numpy.take(ixr, [3*k for k in range(p)])
+    J = numpy.take(ixr, [3*k+1 for k in range(p)])
+    K = numpy.take(ixr, [3*k+2 for k in range(p)])
+    return vertices, I, J, K
+
+def plotlyFigure(stlmesh, color):
+    vertices, I, J, K = stl2mesh3d(stlmesh)
+    x, y, z = vertices.T
+    colorscale= [[0, color], [1, color]]
+    mesh3D = go.Mesh3d(
+            x=x,
+            y=y,
+            z=z,
+            i=I,
+            j=J,
+            k=K,
+            flatshading=True,
+            colorscale=colorscale,
+            intensity=z,
+            name='model',
+            showscale=False)
+    layout = go.Layout(
+                paper_bgcolor='rgba(0,0,0,0)',
+                plot_bgcolor='rgba(0,0,0,0)',
+                width=1024,
+                height=1024,
+                scene_xaxis_visible=False,
+                scene_yaxis_visible=False,
+                scene_zaxis_visible=False)
+
+    fig = go.Figure(data=[mesh3D], layout=layout)
+    fig.update_layout(margin=dict(r=0, l=0, b=0, t=0),
+                      scene_aspectmode='data', 
+                      width=1024)
+    fig.data[0].update(lighting=dict(ambient= 0.18,
+                                     diffuse= 1,
+                                     fresnel=  .1,
+                                     specular= 0,
+                                     roughness= .1,
+                                     facenormalsepsilon=0))
+    fig.data[0].update(lightposition=dict(x=3000,
+                                          y=3000,
+                                          z=10000));
+
+    return fig
+
+def display3D(fh, ph=None, show=False, color="#ccccff"):
     ### Modified from https://pypi.org/project/numpy-stl/ docs
-    ### and https://stackoverflow.com/questions/56864378/how-to-light-and-shade-a-poly3dcollection
-
-    # Create a new plot
-    figure = pyplot.figure()
-    axes = mplot3d.Axes3D(figure)
 
     # Load the STL mesh
     stlmesh = mesh.Mesh.from_file(None, fh=fh)
     # Back to units of mm
     stlmesh.vectors *= 1000
-    stlmesh.update_normals()
-    polymesh = mplot3d.art3d.Poly3DCollection(stlmesh.vectors)
-    if not len(stlmesh.points):
-        log.error("No points in STL mesh, skipping display3D entirely")
-        return
-
-    # Apply light source
-    try:
-        ls = LightSource(azdeg=225, altdeg=45)
-        # Darkest shadowed surface, in rgba
-        dk = numpy.array([0.3, 0.1, 0.2, 1])
-        # Brightest lit surface, in rgba
-        lt = numpy.array([0.7, 0.8, 1.0, 1])
-        # Interpolate between the two, based on face normal
-        shade = lambda s: (lt-dk) * s + dk
-
-        sns = ls.shade_normals(stlmesh.get_unit_normals(), fraction=1.0)
-        rgba = numpy.array([shade(s) for s in sns])
-        polymesh.set_facecolor(rgba)
-    except Exception as e:
-        log.error("Couldn't shade normals, skipping")
-        log.error(repr(e))
-
-    axes.add_collection3d(polymesh)
-
-    # Adjust limits of axes to fill the mesh, but keep 1:1:1 aspect ratio
-    try:
-        pts = stlmesh.points.reshape(-1,3)
-        ptp = max(numpy.ptp(pts, 0))/2.5  ### Should be 2; this is a bit bigger because margins
-        ctrs = [(min(pts[:,i]) + max(pts[:,i]))/2 for i in range(3)]
-        lims = [[ctrs[i] - ptp, ctrs[i] + ptp] for i in range(3)]
-        axes.auto_scale_xyz(*lims)
-    except Exception as e:
-        log.error("Couldn't rescale axes, skipping")
-        log.error(repr(e))
+
+    fig = plotlyFigure(stlmesh, color)
 
     if ph is not None:
-        axes.axis('off')
-        pyplot.savefig(ph, format='png', dpi=300, transparent=True, bbox_inches='tight', pad_inches=0)
+        fig.write_image(ph)
     if show:
-        # Show the plot to the screen
-        axes.axis('on')
-        pyplot.show()
-
-#def displayTkinter(dwg, height = 500, width = 700, showFlats = True):
+        fig.update_layout(scene_xaxis_visible=True,
+                          scene_yaxis_visible=True,
+                          scene_zaxis_visible=True)
+        fig.show()
 
 class DisplayApp:
     def __init__(self, dwg, height = 500, width = 700, showFlats = True):