diff --git a/scipost_django/graphs/templates/graphs/graphs.html b/scipost_django/graphs/templates/graphs/graphs.html
index 1df059edee14d12ca939b69f0fa0a5095a565572..d05984c88deb69d0eafe2c706a3d94de960bbdb0 100644
--- a/scipost_django/graphs/templates/graphs/graphs.html
+++ b/scipost_django/graphs/templates/graphs/graphs.html
@@ -3,14 +3,18 @@
 {% block content %}
   <h1>SciPost Graphs</h1>
 
-  <div id="plot-container" class="row" hx-include="this">
+  <div id="plot-container" class="row h-100" hx-include="this">
     <div class="col-12 col-lg-3 d-flex flex-column"
          hx-post="{% url "graphs:plot_options_form" %}"
-         hx-trigger="load, change from:#plot-container"></div>
-    <div id="plot"
-         class="col d-flex flex-column align-items-center"
-         hx-get="{% url "graphs:plot" %}"
-         hx-params="not csrfmiddlewaretoken"
-         hx-trigger="change from:#plot-container"></div>
+         hx-trigger="load, change from:#plot-container, click from:#plot-refresh">
+    </div>
+
+    <div class="col d-flex flex-column">
+      <div id="plot"
+           class="h-100 w-100"
+           hx-get="{% url "graphs:plot" %}"
+           hx-params="not csrfmiddlewaretoken"
+           hx-trigger="change from:#plot-container"></div>
+    </div>
   </div>
 {% endblock content %}
diff --git a/scipost_django/graphs/templates/graphs/plot.html b/scipost_django/graphs/templates/graphs/plot.html
index 339a2fc3458b412be682dce220c9783e3bed31d6..45991aceffa695e1b32199e637a9d1821c624a4a 100644
--- a/scipost_django/graphs/templates/graphs/plot.html
+++ b/scipost_django/graphs/templates/graphs/plot.html
@@ -1,3 +1,21 @@
-{% if plot_svg %}
-  {{ plot_svg|safe }}
-{% endif %}
+{% if plot_svg %}{{ plot_svg|safe }}{% endif %}
+
+
+<div id="plot-controls" class="my-2 d-flex gap-2 justify-content-end">
+  <button id="plot-refresh" class="btn btn-primary">Refresh</button>
+
+  <button id="plot-download"
+          type="button"
+          class="btn btn-primary dropdown-toggle"
+          data-bs-toggle="dropdown"
+          aria-expanded="false">Download</button>
+  <ul class="dropdown-menu">
+    <li><a class="dropdown-item" href="{{ request.get_full_path }}&download=svg" >SVG</a></li>
+    <li><a class="dropdown-item" href="{{ request.get_full_path }}&download=pdf" >PDF</a></li>
+    <li><a class="dropdown-item" href="{{ request.get_full_path }}&download=png" >PNG</a></li>
+    <li><a class="dropdown-item" href="{{ request.get_full_path }}&download=jpg" >JPG</a></li>
+    <li><hr class="dropdown-divider" /></li>
+    <li><a class="dropdown-item" href="{{ request.get_full_path }}&download=csv">CSV</a></li>
+  </ul>
+
+</div>
diff --git a/scipost_django/graphs/views.py b/scipost_django/graphs/views.py
index aa401b6e1060b83ff82a188f190c23e85e896690..d69a3036e94ac61e877ca30b2c20eb28436d6578 100644
--- a/scipost_django/graphs/views.py
+++ b/scipost_django/graphs/views.py
@@ -4,7 +4,7 @@ __license__ = "AGPL v3"
 
 import io
 from django.contrib.auth.decorators import login_required, permission_required
-from django.shortcuts import render
+from django.shortcuts import HttpResponse, render
 from django.template.response import TemplateResponse
 from django.utils.decorators import method_decorator
 from django.views import View
@@ -42,7 +42,7 @@ class PlotView(View):
     def get(self, request):
         form = PlotOptionsForm(request.GET)
 
-        if not form.is_valid():
+        if not form.is_valid() and request.GET.get("widget"):
             return HTMXResponse(
                 "Invalid plot options: " + str(form.errors), tag="danger"
             )
@@ -67,8 +67,55 @@ class PlotView(View):
                 else:
                     self.plot_options["generic"][option] = value
 
+        if request.GET.get("download"):
+            return self.download(request.GET.get("download", "svg"))
+
         return self.render_to_response(self.get_context_data())
 
+    def download(self, file_type):
+
+        figure = self.render_figure()
+        bytes_io = io.BytesIO()
+
+        match file_type:
+            case "svg":
+                figure.savefig(bytes_io, format="svg")
+                response = HttpResponse(
+                    bytes_io.getvalue(), content_type="image/svg+xml"
+                )
+
+            case "png":
+                figure.savefig(bytes_io, format="png", dpi=300)
+                response = HttpResponse(bytes_io.getvalue(), content_type="image/png")
+
+            case "pdf":
+                figure.savefig(bytes_io, format="pdf")
+                response = HttpResponse(
+                    bytes_io.getvalue(), content_type="application/pdf"
+                )
+
+            case "jpg":
+                figure.savefig(bytes_io, format="jpg", dpi=300)
+                response = HttpResponse(bytes_io.getvalue(), content_type="image/jpg")
+
+            case "csv":
+                x, y = self.kind.get_data()
+
+                # Write the data to a CSV file
+                csv = io.StringIO()
+                csv.write("x,y\n")
+                for i in range(len(x)):
+                    csv.write(f"{x[i]},{y[i]}\n")
+                csv.seek(0)
+                response = HttpResponse(csv, content_type="text/csv")
+
+            case _:
+                raise ValueError(f"Invalid file type: {file_type}")
+
+        response["Content-Disposition"] = f"attachment; filename=plot.{file_type}"
+
+        return response
+
     def render_figure(self):
         if not self.plotter or not self.kind:
             return None