diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt
index 0c33de290175c5ebca1f2b0a5b7e04f268679bea..7a665e5c3b9fe66f3e7fea1282ae18ce7e430456 100644
--- a/example/CMakeLists.txt
+++ b/example/CMakeLists.txt
@@ -10,3 +10,4 @@ add_subdirectory(ring)
 add_subdirectory(gap_junctions)
 add_subdirectory(single)
 add_subdirectory(probe-demo)
+add_subdirectory(lfp)
diff --git a/example/lfp/CMakeLists.txt b/example/lfp/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a3046c48121e81509f63aeb205eef364b559a3f9
--- /dev/null
+++ b/example/lfp/CMakeLists.txt
@@ -0,0 +1,4 @@
+add_executable(lfp EXCLUDE_FROM_ALL lfp.cpp)
+add_dependencies(examples lfp)
+target_link_libraries(lfp PRIVATE arbor ext-tinyopt)
+file(COPY plot-lfp.py DESTINATION "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
diff --git a/example/lfp/README.md b/example/lfp/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a185549c527f88c5a78d1c206266f27cafd4008b
--- /dev/null
+++ b/example/lfp/README.md
@@ -0,0 +1,30 @@
+# Local field potential demo
+
+How might one use Arbor to compute the local field potential near a cell?
+
+This example provide a simple demonstration, simulating one cell and computing
+the LFP from the total membrane current.
+
+The code attempts to provide an Arbor version of the supplied NEURON LFP example
+`neuron_lfp_example.py`. The plot from the NEURON code is included as `example_nrn_EP.png`.
+
+## How to run
+
+The example builds an executable `lfp` that performs a 100 ms simulation of
+a single ball-and-stick neuron, measuring the local field potential at two
+electrode sites.
+
+Running `lfp` generates a JSON file with the simulation output written to stdout.
+The included `plot-lfp.py` script will parse this output and generate a plot.
+
+Run `lfp` and display the output in a window:
+```
+lfp | plot-lfp.py
+```
+
+Run `lfp` and save the results, then generate a plot image:
+```
+lfp > out.json
+plot-lfp.py -o out.png out.json
+```
+
diff --git a/example/lfp/example_nrn_EP.png b/example/lfp/example_nrn_EP.png
new file mode 100644
index 0000000000000000000000000000000000000000..d3b3fa042ddaf518eae6271953484ce33e378c1d
Binary files /dev/null and b/example/lfp/example_nrn_EP.png differ
diff --git a/example/lfp/lfp.cpp b/example/lfp/lfp.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..268fd286f283d8238d67d3e873e2a8fa4b60688f
--- /dev/null
+++ b/example/lfp/lfp.cpp
@@ -0,0 +1,303 @@
+#include <cassert>
+#include <vector>
+#include <iostream>
+
+#include <arbor/load_balance.hpp>
+#include <arbor/cable_cell.hpp>
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/place_pwlin.hpp>
+#include <arbor/morph/region.hpp>
+#include <arbor/simple_sampler.hpp>
+#include <arbor/simulation.hpp>
+#include <arbor/sampling.hpp>
+#include <arbor/util/any.hpp>
+#include <arbor/util/any_ptr.hpp>
+
+using arb::util::any;
+using arb::util::any_cast;
+using arb::util::any_ptr;
+using arb::cell_gid_type;
+
+// Recipe represents one cable cell with one synapse, together with probes for total trans-membrane current, membrane voltage,
+// ionic current density, and synaptic conductance. A sequence of spikes are presented to the one synapse on the cell.
+
+struct lfp_demo_recipe: public arb::recipe {
+    explicit lfp_demo_recipe(arb::event_generator events): events_(std::move(events))
+    {
+        make_cell(); // initializes cell_ and synapse_location_.
+    }
+
+    arb::cell_size_type num_cells() const override { return 1; }
+    arb::cell_size_type num_targets(cell_gid_type) const override { return 1; }
+
+    std::vector<arb::probe_info> get_probes(cell_gid_type) const override {
+        // Four probes:
+        //   0. Total membrane current across cell.
+        //   1. Voltage at synapse location.
+        //   2. Total ionic current density at synapse location.
+        //   3. Expsyn synapse conductance value.
+        return {
+            arb::cable_probe_total_current_cell{},
+            arb::cable_probe_membrane_voltage{synapse_location_},
+            arb::cable_probe_total_ion_current_density{synapse_location_},
+            arb::cable_probe_point_state{0, "expsyn", "g"}};
+    }
+
+    arb::cell_kind get_cell_kind(cell_gid_type) const override {
+        return arb::cell_kind::cable;
+    }
+
+    arb::util::unique_any get_cell_description(cell_gid_type) const override {
+        return cell_;
+    }
+
+    virtual std::vector<arb::event_generator> event_generators(cell_gid_type) const {
+        return {events_};
+    }
+
+    any get_global_properties(arb::cell_kind) const {
+        arb::cable_cell_global_properties gprop;
+        gprop.default_parameters = arb::neuron_parameter_defaults;
+        return gprop;
+    }
+
+private:
+    arb::cable_cell cell_;
+    arb::locset synapse_location_;
+    arb::event_generator events_;
+
+    void make_cell() {
+        using namespace arb;
+
+        // Set up morphology as two branches:
+        // * soma, length 20 μm radius 10 μm, with SWC tag 1.
+        // * apical dendrite, length 490 μm, radius 1 μm, with SWC tag 4.
+        sample_tree tree;
+        tree.append({{0, 0, +10, 10}, 1}); // (root point)
+        tree.append({{0, 0, -10, 10}, 1});
+        tree.append(0, {{0, 0,   10, 1}, 4}); // attach to root point.
+        tree.append({{0, 0, 500, 1}, 4});
+
+        cell_ = cable_cell(tree);
+
+        // Use NEURON defaults for reversal potentials, ion concentrations etc., but override ra, cm.
+        cell_.default_parameters.axial_resistivity = 100;     // [Ω·cm]
+        cell_.default_parameters.membrane_capacitance = 0.01; // [F/m²]
+
+        // Twenty CVs per branch, except for the soma.
+        cell_.default_parameters.discretization = cv_policy_fixed_per_branch(20, cv_policy_flag::single_root_cv);
+
+        // Add pas and hh mechanisms:
+        cell_.paint(reg::tagged(1), "hh"); // (default parameters)
+        cell_.paint(reg::tagged(4), mechanism_desc("pas").set("e", -70));
+
+        // Add exponential synapse at centre of soma (0.5 along branch 0).
+        synapse_location_ = mlocation{0, 0.5};
+        cell_.place(synapse_location_, mechanism_desc("expsyn").set("e", 0).set("tau", 2));
+    }
+};
+
+struct position { double x, y, z; };
+
+struct lfp_sampler {
+    lfp_sampler(const arb::place_pwlin& p, std::vector<position> electrodes, double sigma):
+        placement(p), electrodes(std::move(electrodes)), sigma(sigma) {}
+
+    // Compute response coefficients for each electrode, given a set of cable-like sources.
+    void initialize(const arb::mcable_list& cables) {
+        const unsigned n_electrode = electrodes.size();
+        response.assign(n_electrode, std::vector<double>(cables.size()));
+
+        std::vector<arb::mpoint> midpoints;
+        std::transform(cables.begin(), cables.end(), std::back_inserter(midpoints),
+            [this](const auto& c) { return placement.at({c.branch, 0.5*(c.prox_pos+c.dist_pos)}); });
+
+        const double coef = 1/(4*M_PI*sigma); // [Ω·m]
+        for (unsigned i = 0; i<n_electrode; ++i) {
+            const position& e = electrodes[i];
+
+            std::transform(midpoints.begin(), midpoints.end(), response[i].begin(),
+                [coef, &e](auto p) {
+                    p.x -= e.x;
+                    p.y -= e.y;
+                    p.z -= e.z;
+                    double r = std::sqrt(p.x*p.x+p.y*p.y+p.z*p.z); // [μm]
+                    return coef/r; // [MΩ]
+                });
+        }
+    }
+
+    void reset() {
+        response.clear();
+        lfp_time.clear();
+        lfp_voltage.clear();
+    }
+
+    bool is_initialized() const {
+        return !response.empty();
+    }
+
+    // On receipt of a sequence of cell-wide current samples, apply response matrix and save results to lfp_voltage.
+    arb::sampler_function callback() {
+        return [this](arb::probe_metadata pm, std::size_t n, const arb::sample_record* samples) {
+            auto cables_ptr = any_cast<const arb::mcable_list*>(pm.meta);
+            assert(cables_ptr);
+
+            if (!is_initialized()) {
+                // The first time we get metadata, build the response matrix.
+                initialize(*cables_ptr);
+            }
+
+            std::vector<double> currents;
+            lfp_voltage.resize(response.size());
+
+            for (std::size_t i = 0; i<n; ++i) {
+                lfp_time.push_back(samples[i].time);
+
+                auto data_ptr = any_cast<const arb::cable_sample_range*>(samples[i].data);
+                assert(data_ptr);
+
+                for (unsigned j = 0; j<response.size(); ++j) {
+                    lfp_voltage[j].push_back(std::inner_product(data_ptr->first, data_ptr->second, response[j].begin(), 0.));
+                }
+            }
+        };
+    }
+
+    std::vector<double> lfp_time;
+    std::vector<std::vector<double>> lfp_voltage; // [mV] (one vector per electrode)
+
+private:
+    const arb::place_pwlin placement; // Represents cell morphology in space.
+    const std::vector<position> electrodes;    // [μm]
+    const double sigma;                        // [S/m]
+    std::vector<std::vector<double>> response; // [MΩ]
+};
+
+// JSON output helpers:
+
+template <typename T, typename F>
+struct as_json_array_wrap {
+    const T& data;
+    F fn;
+    as_json_array_wrap(const T& data, const F& fn): data(data), fn(fn) {}
+
+    friend std::ostream& operator<<(std::ostream& out, const as_json_array_wrap& a) {
+        out << '[';
+        bool first = true;
+        for (auto& x: a.data) out << (!first? ", ": (first=false, "")) << a.fn(x);
+        return out << ']';
+    }
+};
+
+struct {
+    template <typename F>
+    auto operator()(const F& fn) const {
+        return [&fn](const auto& data) { return as_json_array_wrap<decltype(data), F>(data, fn); };
+    }
+
+    auto operator()() const {
+        return this->operator()([](const auto& x) { return x; });
+    }
+} as_json_array;
+
+// Run simulation.
+
+int main(int argc, char** argv) {
+    auto context = arb::make_context();
+
+    // Weight 0.005 μS, onset at t = 0 ms, mean frequency 0.1 kHz.
+    auto events = arb::poisson_generator({0, 0}, .005, 0., 0.1, std::minstd_rand{});
+    lfp_demo_recipe R(events);
+
+    const double t_stop = 100;    // [ms]
+    const double sample_dt = 0.1; // [ms]
+    const double dt = 0.1;        // [ms]
+
+    arb::simulation sim(R, arb::partition_load_balance(R, context), context);
+
+    std::vector<position> electrodes = {
+        {30, 0, 0},
+        {30, 0, 100}
+    };
+
+    auto sample_schedule = arb::regular_schedule(sample_dt);
+
+    arb::morphology cell_morphology = any_cast<arb::cable_cell>(R.get_cell_description(0)).morphology();
+    arb::place_pwlin placed_cell(cell_morphology);
+    lfp_sampler lfp(placed_cell, electrodes, 3.0);
+    sim.add_sampler(arb::one_probe({0, 0}), sample_schedule, lfp.callback(), arb::sampling_policy::exact);
+
+    arb::trace_vector<double, arb::mlocation> membrane_voltage;
+    sim.add_sampler(arb::one_probe({0, 1}), sample_schedule, make_simple_sampler(membrane_voltage), arb::sampling_policy::exact);
+
+    arb::trace_vector<double> ionic_current_density;
+    sim.add_sampler(arb::one_probe({0, 2}), sample_schedule, make_simple_sampler(ionic_current_density), arb::sampling_policy::exact);
+
+    arb::trace_vector<double> synapse_g;
+    sim.add_sampler(arb::one_probe({0, 3}), sample_schedule, make_simple_sampler(synapse_g), arb::sampling_policy::exact);
+
+    sim.run(t_stop, dt);
+
+    // Output results in JSON format suitable for plotting by plot-lfp.py script.
+
+    auto get_t = [](const auto& x) { return x.t; };
+    auto get_v = [](const auto& x) { return x.v; };
+    auto scale = [](double s) { return [s](const auto& x) { return x*s; }; };
+    auto to_xz = [](const auto& p) { return std::array<double, 2>{p.x, p.z}; };
+
+    // Compute synaptic current from synapse conductance and membrane potential.
+    std::vector<double> syn_i;
+    assert(synapse_g.get(0).size()==membrane_voltage.get(0).size());
+    std::transform(synapse_g.get(0).begin(), synapse_g.get(0).end(), membrane_voltage.get(0).begin(), std::back_inserter(syn_i),
+        [](arb::trace_entry<double> g, arb::trace_entry<double> v) {
+            assert(g.t==v.t);
+            return g.v*v.v;
+        });
+
+    // Collect points from 2-d morphology in vectors of arrays (x, z, radius), one per branch.
+    // (This process will be simplified with improvements to the place_pwlin API.)
+    std::vector<std::vector<std::array<double, 3>>> samples;
+    for (unsigned branch = 0; branch<cell_morphology.num_branches(); ++branch) {
+        samples.push_back({});
+        auto branch_range = cell_morphology.branch_indexes(branch);
+        for (auto i_ptr = branch_range.first; i_ptr!=branch_range.second; ++i_ptr) {
+            arb::msample s = cell_morphology.samples()[*i_ptr];
+            samples.back().push_back(std::array<double, 3>{s.loc.x, s.loc.z, s.loc.radius});
+        }
+    }
+
+    auto probe_xz = to_xz(placed_cell.at(membrane_voltage.get(0).meta));
+    std::vector<std::array<double, 2>> electrodes_xz;
+    std::transform(electrodes.begin(), electrodes.end(), std::back_inserter(electrodes_xz), to_xz);
+
+    std::cout <<
+        "{\n"
+        "\"morphology\": {\n"
+        "\"unit\": \"μm\",\n"
+        "\"samples\": " << as_json_array(as_json_array(as_json_array()))(samples) << ",\n"
+        "\"probe\": " << as_json_array()(probe_xz) << ",\n"
+        "\"electrodes\": " << as_json_array(as_json_array())(electrodes_xz) << "\n"
+        "},\n"
+        "\"extracellular potential\": {\n"
+        "\"unit\": \"μV\",\n"
+        "\"time\": " << as_json_array()(lfp.lfp_time) << ",\n"
+        "\"values\": " << as_json_array(as_json_array(scale(1e3)))(lfp.lfp_voltage) << "\n"
+        "},\n"
+        "\"synaptic current\": {\n"
+        "\"unit\": \"nA\",\n"
+        "\"time\": "  << as_json_array(get_t)(synapse_g.get(0)) << ",\n"
+        "\"value\": " << as_json_array()(syn_i) << "\n"
+        "},\n"
+        "\"membrane potential\": {\n"
+        "\"unit\": \"mV\",\n"
+        "\"time\": "  << as_json_array(get_t)(membrane_voltage.get(0)) << ",\n"
+        "\"value\": " << as_json_array(get_v)(membrane_voltage.get(0)) << "\n"
+        "},\n"
+        "\"ionic current density\": {\n"
+        "\"unit\": \"A/m²\",\n"
+        "\"time\": "  << as_json_array(get_t)(ionic_current_density.get(0)) << ",\n"
+        "\"value\": " << as_json_array(get_v)(ionic_current_density.get(0)) << "\n"
+        "}\n"
+        "}\n";
+}
diff --git a/example/lfp/neuron_lfp_example.py b/example/lfp/neuron_lfp_example.py
new file mode 100755
index 0000000000000000000000000000000000000000..1d1023ac427c3afc1688311e783f0e2388a71de1
--- /dev/null
+++ b/example/lfp/neuron_lfp_example.py
@@ -0,0 +1,296 @@
+#!/usr/env/bin python
+# -*- coding: utf-8 -*-
+# Author: Torbjørn Ness <torbjorn.ness@nmbu.no>
+'''
+NEURON and Python - Creating a multi-compartment model with synaptic input
+with randomized activation times
+'''
+# Import modules for plotting and NEURON itself 
+import matplotlib.pyplot as plt
+import neuron
+import numpy as np
+
+
+class Cell:
+    """
+    Cell class that handles interactions with NEURON. It finds the center position
+    of each cellular compartment (cell.xmid, cell.ymid, cell.zmid), and the transmembrane currents
+    cell.imem
+    """
+
+    def __init__(self):
+        cvode = neuron.h.CVode()
+        cvode.use_fast_imem(1)
+        self.tstop = 100.        # simulation duration in ms
+        self.v_init = -65        # membrane voltage(s) at t = 0
+
+        neuron.h.dt = 0.1
+
+        self.make_cell()
+        neuron.h.define_shape()
+        self.seclist = []
+        counter = 0
+        for sec in neuron.h.allsec():
+            self.seclist.append(sec)
+            for seg in sec:
+                counter += 1
+        self.totnsegs = counter  # Total number of compartments in cell model
+        self.collect_geometry()
+
+
+        self.insert_synapse(self.seclist[0])
+
+        self.initiate_recorders()
+
+    def make_cell(self):
+        neuron.h("""
+        create soma[1]
+        create apic[1]
+        objref all
+        all = new SectionList()
+        soma[0] {pt3dclear()
+          pt3dadd(0, 0, -10, 20)
+          pt3dadd(0, 0, 10, 20)}
+
+        apic[0] {pt3dclear()
+          pt3dadd(0, 0, 10, 2)
+          pt3dadd(0, 0, 500, 2)}
+
+        connect apic[0](0), soma[0](1)
+
+        apic[0] {nseg = 20}
+        forall {
+            Ra = 100.
+            cm = 1.
+            all.append()
+        }
+        apic[0] { insert pas }
+        soma[0] { insert hh }
+
+        """)
+
+    def initiate_recorders(self):
+        self.imem = []  # Record membrane currents
+        self.vmem = []  # Record membrane potentials
+
+        for sec in neuron.h.allsec():
+            for seg in sec:
+                v_ = neuron.h.Vector()
+                v_.record(seg._ref_v, neuron.h.dt)
+                self.vmem.append(v_)
+
+                i_ = neuron.h.Vector()
+                i_.record(seg._ref_i_membrane_, neuron.h.dt)
+                self.imem.append(i_)
+
+        self.tvec = neuron.h.Vector()
+        self.tvec.record(neuron.h._ref_t)
+
+        if hasattr(self, "syn"):
+            self.syn_i = neuron.h.Vector()  # Record synaptic current
+            self.syn_i.record(self.syn._ref_i, neuron.h.dt)
+
+    def insert_synapse(self, syn_sec):
+        """
+        Function to insert a single synapse into cell model, as section syn_sec
+        """
+        print(syn_sec.diam)
+        syn = neuron.h.ExpSyn(0.5, sec=syn_sec)
+        syn.e = 0.         # reversal potential of synapse conductance in mV
+        syn.tau = 2.       # time constant of synapse conductance in ms
+
+        ns = neuron.h.NetStim(0.5)     # spike time generator object (~presynaptic)
+        ns.noise = 1.                  # Fractional randomness (intervals from exp dist)
+        ns.start = 0.                  # approximate time of first spike
+        ns.number = 1000               # number of spikes
+        ns.interval = 10.              # average interspike interval
+        nc = neuron.h.NetCon(ns, syn)    # Connect generator to synapse
+        nc.weight[0] = .005                  # Set synapse weight
+
+        # Everything must be stored or NEURON will forget they ever existed
+        self.ns = ns
+        self.nc = nc
+        self.syn = syn
+
+    def collect_geometry(self):
+        """
+        Function to get positions, diameters etc of each segment in NEURON
+        """
+
+        areavec = np.zeros(self.totnsegs)
+        diamvec = np.zeros(self.totnsegs)
+        lengthvec = np.zeros(self.totnsegs)
+
+        xstartvec = np.zeros(self.totnsegs)
+        xendvec = np.zeros(self.totnsegs)
+        ystartvec = np.zeros(self.totnsegs)
+        yendvec = np.zeros(self.totnsegs)
+        zstartvec = np.zeros(self.totnsegs)
+        zendvec = np.zeros(self.totnsegs)
+
+        counter = 0
+
+        #loop over all segments
+        for sec in neuron.h.allsec():
+            n3d = int(neuron.h.n3d())
+            nseg = sec.nseg
+            gsen2 = 1./2/nseg
+            if n3d > 0:
+                #create interpolation objects for the xyz pt3d info:
+                L = np.zeros(n3d)
+                x = np.zeros(n3d)
+                y = np.zeros(n3d)
+                z = np.zeros(n3d)
+                for i in range(n3d):
+                    L[i] = neuron.h.arc3d(i)
+                    x[i] = neuron.h.x3d(i)
+                    y[i] = neuron.h.y3d(i)
+                    z[i] = neuron.h.z3d(i)
+
+                #normalize as seg.x [0, 1]
+                L /= sec.L
+
+                #temporary store position of segment midpoints
+                segx = np.zeros(nseg)
+                for i, seg in enumerate(sec):
+                    segx[i] = seg.x
+
+                #can't be >0 which may happen due to NEURON->Python float transfer:
+                segx0 = (segx - gsen2).round(decimals=6)
+                segx1 = (segx + gsen2).round(decimals=6)
+
+                #fill vectors with interpolated coordinates of start and end points
+                xstartvec[counter:counter+nseg] = np.interp(segx0, L, x)
+                xendvec[counter:counter+nseg] = np.interp(segx1, L, x)
+
+                ystartvec[counter:counter+nseg] = np.interp(segx0, L, y)
+                yendvec[counter:counter+nseg] = np.interp(segx1, L, y)
+
+                zstartvec[counter:counter+nseg] = np.interp(segx0, L, z)
+                zendvec[counter:counter+nseg] = np.interp(segx1, L, z)
+
+                #fill in values area, diam, length
+                for i, seg in enumerate(sec):
+                    areavec[counter] = neuron.h.area(seg.x)
+                    diamvec[counter] = seg.diam
+                    lengthvec[counter] = sec.L/nseg
+                    counter += 1
+
+        # starting position of each compartment (segment)
+        self.xstart = xstartvec
+        self.ystart = ystartvec
+        self.zstart = zstartvec
+
+        # ending position of each compartment (segment)
+        self.xend = xendvec
+        self.yend = yendvec
+        self.zend = zendvec
+
+        # Calculates the center position of each compartment (segment)
+        self.xmid = 0.5 * (self.xstart + self.xend)
+        self.ymid = 0.5 * (self.ystart + self.yend)
+        self.zmid = 0.5 * (self.zstart + self.zend)
+        self.area = areavec
+        self.diam = diamvec
+
+    def simulate(self):
+
+        neuron.h.finitialize(self.v_init)
+        neuron.h.fcurrent()
+
+        while neuron.h.t < self.tstop:
+            neuron.h.fadvance()
+
+        self.vmem = np.array(self.vmem)
+        self.imem = np.array(self.imem)
+        self.syn_i = np.array(self.syn_i)
+        self.tvec = np.array(self.tvec)[:self.vmem.shape[1]]
+
+
+class ExtElectrode:
+    def __init__(self, elec_x, elec_y, elec_z):
+        """
+
+        :param elec_x, elec_y, elec_z : x,y,z-positions (um) of each electrode. Must
+        be numpy arrays of equal length
+
+        """
+        self.sigma = 0.3  # Extracellular conductivity (S/m)
+        self.elec_x = elec_x
+        self.elec_y = elec_y
+        self.elec_z = elec_z
+        self.num_elecs = len(self.elec_x)
+
+        # Give electrodes different colors for plotting purposes:
+        self.elec_clr = lambda idx: plt.cm.viridis(idx / self.num_elecs)
+
+    def calc_extracellular_potential(self, cell):
+        self.calc_mapping(cell)
+        self.extracellular_potential = np.dot(electrode.mapping, cell.imem)
+
+    def calc_mapping(self, cell):
+        """
+        Calculates 'mapping' of size (number of electrodes) * (number of cell compartments)
+        Extracellular potential can then be calculated as
+        :param cell: class containing x,y,z-positions (um) of each
+        compartment (segment) center as cell.xmid, cell.ymid, cell.zmid
+
+        """
+        self.mapping = np.zeros((self.num_elecs, cell.totnsegs))
+        for e_idx in range(self.num_elecs):
+            r2 = ((cell.xmid - self.elec_x[e_idx])**2 +
+                  (cell.ymid - self.elec_y[e_idx])**2 +
+                  (cell.zmid - self.elec_z[e_idx])**2)
+
+            self.mapping[e_idx] = 1 / (4 * np.pi * self.sigma * np.sqrt(r2))
+
+
+def plot_results(cell, electrode):
+    ################################################################################
+    # Plot simulated output
+    ################################################################################
+    fig = plt.figure(figsize=(9, 5))
+    fig.subplots_adjust(wspace=0.5, hspace=0.9)
+    ax_morph = fig.add_subplot(131, aspect=1, xlim=[-150, 150], ylim=[-100, 600],
+                               title="morphology", xlabel="x ($\mu$m)", ylabel="y ($\mu$m)")
+    ax_syn = fig.add_subplot(332, ylabel='nA', title="synaptic current", xlabel='time (ms)')
+    ax_vmem = fig.add_subplot(335, ylabel='mV', xlabel='time (ms)', title="membrane potential")
+    ax_imem = fig.add_subplot(338, ylabel='nA', xlabel='time (ms)', title="membrane current")
+    ax_ep = fig.add_subplot(133, ylabel='$\mu$V', xlabel="time (ms)", title="Extracellular potential")
+
+    plot_comp_idx = 0
+    plot_comp_clr = 'r'
+
+    for idx in range(cell.totnsegs):
+        ax_morph.plot([cell.xstart[idx], cell.xend[idx]],
+                      [cell.zstart[idx], cell.zend[idx]], lw=cell.diam[idx] / 2, c='k')
+    ax_morph.plot(cell.xmid[plot_comp_idx], cell.zmid[plot_comp_idx], marker='*', c=plot_comp_clr)
+
+    ax_syn.plot(cell.tvec, cell.syn_i, c='k', lw=2)
+    ax_vmem.plot(cell.tvec, cell.vmem[0, :], c=plot_comp_clr, lw=2)
+    ax_imem.plot(cell.tvec, cell.imem[0, :], c=plot_comp_clr, lw=2)
+
+    for e_idx in range(electrode.num_elecs):
+        e_clr = electrode.elec_clr(e_idx)
+        sig = 1000 * electrode.extracellular_potential[e_idx]  # convert to uV
+        ax_ep.plot(cell.tvec, sig, c=e_clr)
+        ax_morph.plot(electrode.elec_x[e_idx], electrode.elec_z[e_idx], marker='o', c=e_clr)
+
+    fig.savefig('example_nrn_EP.png')
+
+    plt.close(fig)
+
+if __name__ == '__main__':
+    cell = Cell()
+    cell.simulate()
+
+    num_elecs = 2
+    elec_x = 30 * np.ones(num_elecs)
+    elec_y = np.zeros(num_elecs)
+    elec_z = np.linspace(0, 100, num_elecs)
+
+    electrode = ExtElectrode(elec_x, elec_y, elec_z)
+    electrode.calc_extracellular_potential(cell)
+
+    plot_results(cell, electrode)
+
diff --git a/example/lfp/plot-lfp.py b/example/lfp/plot-lfp.py
new file mode 100755
index 0000000000000000000000000000000000000000..14ddde3eacd33da10de67a8879498c2d21eb6001
--- /dev/null
+++ b/example/lfp/plot-lfp.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+
+import argparse
+import matplotlib.pyplot as plt
+import json
+import math
+import sys
+
+# Read JSON output of lfp example from stdin and plot.
+#
+# The JSON data for timeseries is structured as:
+#    <string>: { "unit": <string>, "time": [ <number>... ], "value": [ <number>... ] }
+# or:
+#    <string>: { "unit": <string>, "time": [ <number>... ], "values": [[ <number>... ] ...] }
+#
+# 2-d morphology data is represented as samples (x, z, r) where r is the radius, one array
+# per branch. Extra point data (probe location, electrode sites) as pairs (x, z):
+#   "morphology": { "unit": <string>, "samples":  [[[<number> <number> <number>] ...] ...]
+#                   "probe": [<number> <number>], "electrodes": [[<number> <number>] ...] }
+
+def subplot_timeseries(fig, index, jdict, key):
+    data = jdict[key]
+    sub = fig.add_subplot(index, ylabel=data['unit'], title=key, xlabel='time (ms)')
+    ts = data['time']
+    vss = data['values'] if 'values' in data else [data['value']]
+
+    for vs in vss: sub.plot(ts, vs)
+
+def subplot_morphology(fig, index, jdict, key, xlim, ylim):
+    data = jdict[key]
+    unit = data['unit']
+    sub = fig.add_subplot(index, xlabel='x ('+unit+')', ylabel='y ('+unit+')', title=key, xlim=xlim, ylim=ylim)
+
+    for samples in data['samples']:
+        polys = [([x0-s0*dy, x0+s0*dy, x1+s1*dy, x1-s1*dy], [y0+s0*dx, y0-s0*dx, y1-s1*dx, y1+s1*dx])
+                for ((x0, y0, r0), (x1, y1, r1)) in zip(samples, samples[1:])
+                for dx, dy in [(x1-x0, y1-y0)]
+                for d in [math.sqrt(dx*dx+dy*dy)]
+                if d>0
+                for s0, s1 in [(r0/d, r1/d)]]
+
+        for xs, ys in polys: sub.fill(xs, ys, 'k')
+    sub.plot(*[u for x, y in data['electrodes'] for u in [[x], [y], 'o']])
+    sub.plot(*[u for x, y in [data['probe']] for u in [[x], [y], 'r*']])
+
+P = argparse.ArgumentParser(description='Plot results of LFP demo.')
+P.add_argument(
+    'input', metavar='FILE', nargs='?', type=argparse.FileType('r'), default=sys.stdin,
+    help='LFP example output in JSON')
+P.add_argument(
+    '-o', '--output', metavar='FILE', dest='outfile',
+    help='save plot to file FILE')
+
+args = P.parse_args()
+j = json.load(args.input)
+
+fig = plt.figure(figsize=(9, 5))
+fig.subplots_adjust(wspace=0.6, hspace=0.9)
+
+subplot_morphology(fig, 131, j, 'morphology', xlim=[-100, 100], ylim=[-100, 600])
+subplot_timeseries(fig, 332, j, 'synaptic current')
+subplot_timeseries(fig, 335, j, 'membrane potential')
+subplot_timeseries(fig, 338, j, 'ionic current density')
+subplot_timeseries(fig, 133, j, 'extracellular potential')
+
+if args.outfile:
+    fig.savefig(args.outfile)
+else:
+    plt.show()
+
+plt.close(fig)