diff --git a/nummi/api/views.py b/nummi/api/views.py
index 876d5fd..2d46911 100644
--- a/nummi/api/views.py
+++ b/nummi/api/views.py
@@ -1,5 +1,5 @@
-from django.db.models import Sum
-from django.db.models.functions import ExtractQuarter, ExtractYear
+from django.db.models import Sum, FloatField, Q
+from django.db.models.functions import TruncMonth
 from django.http import JsonResponse
 from django.views import View
 from django.views.generic.list import MultipleObjectMixin
@@ -44,8 +44,17 @@ class HistoryView(UserMixin, MultipleObjectMixin, View):
             {
                 "data": list(
                     self.get_queryset()
-                    .values("category__name", quarter=ExtractQuarter("date"), year=ExtractYear("date"))
-                    .annotate(Sum("value"))
+                    .values(month=TruncMonth("date"))
+                    .annotate(
+                        sum_p=Sum(
+                            "value", output_field=FloatField(), filter=Q(value__gt=0)
+                        ),
+                        sum_m=Sum(
+                            "value", output_field=FloatField(), filter=Q(value__lt=0)
+                        ),
+                        sum=Sum("value", output_field=FloatField()),
+                    )
+                    .order_by("month")
                 )
             }
         )
diff --git a/nummi/main/static/main/js/history_plot.js b/nummi/main/static/main/js/history_plot.js
new file mode 100644
index 0000000..ddec5bd
--- /dev/null
+++ b/nummi/main/static/main/js/history_plot.js
@@ -0,0 +1,31 @@
+import {json} from "https://cdn.skypack.dev/d3-fetch@3";
+import {timeParse} from "https://cdn.skypack.dev/d3-time-format@4";
+import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm";
+
+const history = await json("/api/history");
+const parseDate = timeParse("%Y-%m-%d");
+console.log(history);
+history["data"].forEach((d) => {
+	d.month = parseDate(d.month);
+});
+const data = history["data"];
+
+let history_plot = Plot.plot({
+	x: {
+		round: true,
+		label: "Month",
+		grid: true,
+	},
+	y: {
+		grid: true,
+	},
+	marks: [
+		Plot.areaY(data, {x: "month", y: "sum_p", interval: "month", curve: "bump-x", fill: "#aaccff"}),
+		Plot.areaY(data, {x: "month", y: "sum_m", interval: "month", curve: "bump-x", fill: "#ffccaa"}),
+		Plot.ruleY([0]),
+		Plot.lineY(data, {x: "month", y: "sum", interval: "month", curve: "bump-x", marker: "circle", strokeWidth: 2}),
+	],
+	insetTop: 20,
+	insetBottom: 20,
+});
+document.querySelector("#history_plot").append(history_plot);
diff --git a/nummi/main/templates/main/category_form.html b/nummi/main/templates/main/category_form.html
index b2c04cf..a3b327a 100644
--- a/nummi/main/templates/main/category_form.html
+++ b/nummi/main/templates/main/category_form.html
@@ -20,8 +20,6 @@
         {{ form }}
     </form>
     {% if form.instance.transactions %}
-        <img src="{% url "plot-category" form.instance.id %}"
-             alt="Graph representing value over time"/>
         <h2>{% translate "Transactions" %}</h2>
         {% include "main/table/transaction.html" %}
     {% endif %}
diff --git a/nummi/main/templates/main/index.html b/nummi/main/templates/main/index.html
index 503d0fe..2dc4d85 100644
--- a/nummi/main/templates/main/index.html
+++ b/nummi/main/templates/main/index.html
@@ -10,6 +10,7 @@
     <link rel="stylesheet"
           href="{% static 'main/css/table.css' %}"
           type="text/css"/>
+    <script src="{% static 'main/js/history_plot.js' %}" type="module"></script>
 {% endblock %}
 {% block body %}
     {% if accounts %}
@@ -35,6 +36,7 @@
                 {% endfor %}
             </div>
         {% endspaceless %}
+        <div id="history_plot"></div>
     {% endif %}
     {% if snapshots %}
         <h2>{% translate "Snapshots" %}</h2>
diff --git a/nummi/main/views.py b/nummi/main/views.py
index 9b078d7..b981fa8 100644
--- a/nummi/main/views.py
+++ b/nummi/main/views.py
@@ -233,9 +233,7 @@ class SnapshotUpdateView(NummiUpdateView):
             )
         if _transactions:
             data["categories"] = (
-                _transactions.values(
-                    "category", "category__name", "category__icon"
-                )
+                _transactions.values("category", "category__name", "category__icon")
                 .annotate(
                     sum=models.Sum("value"),
                     sum_m=models.Sum("value", filter=models.Q(value__lt=0)),
diff --git a/nummi/nummi/urls.py b/nummi/nummi/urls.py
index a888c80..372b4cb 100644
--- a/nummi/nummi/urls.py
+++ b/nummi/nummi/urls.py
@@ -19,7 +19,6 @@ from django.urls import include, path
 
 urlpatterns = i18n_patterns(
     path("", include("main.urls")),
-    path("plot/", include("plot.urls")),
     path("api/", include("api.urls")),
     path("admin/", admin.site.urls),
     prefix_default_language=False,
diff --git a/nummi/plot/__init__.py b/nummi/plot/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/nummi/plot/admin.py b/nummi/plot/admin.py
deleted file mode 100644
index 846f6b4..0000000
--- a/nummi/plot/admin.py
+++ /dev/null
@@ -1 +0,0 @@
-# Register your models here.
diff --git a/nummi/plot/apps.py b/nummi/plot/apps.py
deleted file mode 100644
index 1f7a601..0000000
--- a/nummi/plot/apps.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.apps import AppConfig
-
-
-class PlotConfig(AppConfig):
-    default_auto_field = "django.db.models.BigAutoField"
-    name = "plot"
diff --git a/nummi/plot/migrations/__init__.py b/nummi/plot/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/nummi/plot/models.py b/nummi/plot/models.py
deleted file mode 100644
index 6b20219..0000000
--- a/nummi/plot/models.py
+++ /dev/null
@@ -1 +0,0 @@
-# Create your models here.
diff --git a/nummi/plot/nummi.mplstyle b/nummi/plot/nummi.mplstyle
deleted file mode 100644
index b49262b..0000000
--- a/nummi/plot/nummi.mplstyle
+++ /dev/null
@@ -1,13 +0,0 @@
-font.family: Inter
-
-lines.linewidth: 2
-
-figure.autolayout: True
-figure.figsize: 8, 4
-figure.dpi: 300
-
-axes.prop_cycle: cycler('color', ["66cc66", "338033", "99ff99", "802653", "cc6699"])
-axes.axisbelow: True
-axes.grid: True
-
-svg.fonttype: none
diff --git a/nummi/plot/tests.py b/nummi/plot/tests.py
deleted file mode 100644
index a39b155..0000000
--- a/nummi/plot/tests.py
+++ /dev/null
@@ -1 +0,0 @@
-# Create your tests here.
diff --git a/nummi/plot/urls.py b/nummi/plot/urls.py
deleted file mode 100644
index 1412ba8..0000000
--- a/nummi/plot/urls.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from django.urls import path
-
-from . import views
-
-urlpatterns = [
-    path("timeline", views.timeline, name="plot-timeline"),
-    path("categories", views.categories, name="plot-categories"),
-    path("category/<uuid>", views.category, name="plot-category"),
-]
diff --git a/nummi/plot/views.py b/nummi/plot/views.py
deleted file mode 100644
index 46ebfa5..0000000
--- a/nummi/plot/views.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import io
-
-import matplotlib
-import matplotlib.pyplot as plt
-from django.contrib.auth.decorators import login_required
-from django.db import models
-from django.http import HttpResponse
-from django.shortcuts import get_object_or_404
-from django.utils.translation import gettext as _
-from matplotlib import dates as mdates
-
-from main.models import Category, Snapshot, Transaction
-
-matplotlib.use("Agg")
-plt.style.use("./plot/nummi.mplstyle")
-
-
-@login_required
-def timeline(request):
-    _snapshots = Snapshot.objects.all()
-
-    fig, ax = plt.subplots()
-    ax.step(
-        [s.date for s in _snapshots],
-        [s.value for s in _snapshots],
-        where="post",
-    )
-    ax.set(ylabel=_("Snapshots"), ylim=0)
-    ax.xaxis.set_major_formatter(
-        mdates.ConciseDateFormatter(ax.xaxis.get_major_locator())
-    )
-    ax.autoscale(True, "x", True)
-
-    _io = io.StringIO()
-
-    fig.savefig(_io, format="svg")
-
-    return HttpResponse(_io.getvalue(), headers={"Content-Type": "image/svg+xml"})
-
-
-@login_required
-def categories(request):
-    _categories = Category.objects.filter(budget=True)
-
-    fig, ax = plt.subplots(figsize=(8, _categories.count() / 4))
-    ax.barh(
-        [str(c) for c in _categories][::-1],
-        [
-            Transaction.objects.filter(category=c).aggregate(sum=models.Sum("value"))[
-                "sum"
-            ]
-            for c in _categories
-        ][::-1],
-    )
-
-    _io = io.StringIO()
-
-    fig.savefig(_io, format="svg")
-
-    return HttpResponse(_io.getvalue(), headers={"Content-Type": "image/svg+xml"})
-
-
-@login_required
-def category(request, uuid):
-    _category = get_object_or_404(Category, id=uuid)
-    _values_p = (
-        Transaction.objects.filter(category=_category)
-        .filter(value__gt=0)
-        .annotate(m=models.functions.TruncMonth("date"))
-        .values("m")
-        .annotate(sum=models.Sum("value"))
-        .order_by("m")
-    )
-    _values_m = (
-        Transaction.objects.filter(category=_category)
-        .filter(value__lt=0)
-        .annotate(m=models.functions.TruncMonth("date"))
-        .values("m")
-        .annotate(sum=models.Sum("value"))
-        .order_by("m")
-    )
-
-    fig, ax = plt.subplots()
-    ax.bar(
-        [v["m"] for v in _values_p],
-        [v["sum"] for v in _values_p],
-        width=12,
-        color="#66cc66",
-    )
-    ax.bar(
-        [v["m"] for v in _values_m],
-        [v["sum"] for v in _values_m],
-        width=12,
-        color="#cc6699",
-    )
-    ax.xaxis.set_major_formatter(
-        mdates.ConciseDateFormatter(ax.xaxis.get_major_locator())
-    )
-    ax.autoscale(True, "x", True)
-    _ym, _yp = ax.get_ylim()
-    ax.set(ylim=(min(_ym, 0), max(_yp, 0)))
-    ax.set(ylabel=f"{_category.name} (€)")
-
-    _io = io.StringIO()
-
-    fig.savefig(_io, format="svg")
-
-    return HttpResponse(_io.getvalue(), headers={"Content-Type": "image/svg+xml"})
