# Density line chart method for Makie

**URL:** <https://discourse.julialang.org/t/density-line-chart-method-for-makie/134051>\
**Category:** Visualization\
**Tags:** plotting, time-series, makie\
**Created:** [November 22, 2025, 11:46pm UTC](https://discourse.julialang.org/t/density-line-chart-method-for-makie/134051 "2025-11-22T23:46:45Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![ConnectedSystems](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/connectedsystems/32/16815_2.png) [@ConnectedSystems](https://discourse.julialang.org/u/ConnectedSystems)\
**Post date:** [November 22, 2025, 11:46pm UTC](https://discourse.julialang.org/t/density-line-chart-method-for-makie/134051/1 "2025-11-22T23:46:45Z")

</div>

Not sure if this is against any rules but I want to cross-post this FR I just submitted in the Makie repo for awareness.

I have a working example of a density line chart method (by [Moritz & Fisher, 2018](https://idl.cs.washington.edu/files/2018-DenseLines-arXiv.pdf)) to visualize many time series together (where “many” can be 10s of thousands). My current roadblock is my inexperience with the Makie architecture and code organisation.

Here’s an example of 20,000 time series (with 76 time steps):

 ![image](https://global.discourse-cdn.com/julialang/original/3X/0/3/039d39f820ecda51d963d78658dba7b5b0c45ab5.png)

> <https://github.com/MakieOrg/Makie.jl/issues/5427>
>
> I'm interested in contributing a density line chart method to Makie. Having a lo…ok around, I don't think this plot method exists in Makie yet, I didn't see any open issues to add it, and it would be very useful in my work to visualize many time series together (where "many" can be tens of thousands or more).
> 
> The method is described in \[Moritz & Fisher (2018)\](https://idl.cs.washington.edu/files/2018-DenseLines-arXiv.pdf) (a high-level graphical explanation can be found \[here\](https://idl.uw.edu/papers/dense-lines)).
> 
> In effect, the trajectory of each series is rasterized and the number of times a trajectory hit a pixel is normalized to create a density map. It is similar to, if not identical, to the implementation in \[Python Datashader\](https://datashader.org/user\_guide/Timeseries.html#plotting-large-numbers-of-time-series-together). 
> 
> I have a working demonstrator, and while I'm comfortable submitting PRs, my main issue is how complex the Makie architecture is and the organisation of the monorepo itself - I'm not sure where I'm to actually make my changes.
> 
> Full disclosure: I found the documentation and examples around plotting recipes very hard to digest and understand so in the end I got things working with some trial and error and a lot of help from AI, so things in that end may not be idiomatic or optimal.
> 
> Here's an example output for 76 timesteps and 20,000 series discretized into 200 and 100 bins for x and y respectively.
> 
> \<img width="582" height="430" alt="Image" src="https://github.com/user-attachments/assets/ad1efc3c-4d3c-40ca-8128-55f08d29c533" /\>
> 
> Current code below. Open to any and all suggestions of course:
> 
> 
> \`\`\`julia
> using GLMakie
> using Statistics
> using Dates
> 
> """
> bresenham\_line(x1, y1, x2, y2)
> 
> Bresenham's line algorithm to get all pixels between two points.
> 
> \# References
> \- Bresenham, J.E., 1965. \\
> Algorithm for computer control of a digital plotter. \\
> IBM Systems Journal 4, 25-30. \\
> https://doi.org/10.1147/sj.41.0025
> """
> function bresenham\_line(x1::Int, y1::Int, x2::Int, y2::Int)::Vector{Tuple{Int,Int}}
> pixels = Tuple{Int,Int}\[\]
> 
> dx = abs(x2 - x1)
> dy = abs(y2 - y1)
> sx = x1 \< x2 ? 1 : -1
> sy = y1 \< y2 ? 1 : -1
> err = dx - dy
> 
> x, y = x1, y1
> 
> while true
> push!(pixels, (x, y))
> 
> if x == x2 && y == y2
> break
> end
> 
> e2 = 2 \* err
> if e2 \> -dy
> err -= dy
> x += sx
> end
> if e2 \< dx
> err += dx
> y += sy
> end
> end
> 
> return pixels
> end
> 
> """
> compute\_normalized\_density(
> series::AbstractVector, bins\_x::Int, bins\_y::Int
> )::Matrix{Float64}
> 
> Compute arc-length normalized density for a single time series using Bresenham-style line
> rendering.
> 
> \# References
> \- Moritz, D., & Fisher, D. (2018). \\
> Visualizing a Million Time Series with the Density Line Chart. \\
> arXiv:1808.06019 \[cs.HC\] \\
> https://doi.org/10.48550/arXiv.1808.06019
> 
> \# Arguments
> \- \`series::AbstractVector\`: A single time series (vector of values over time)
> \- \`bins\_x::Int\`: Number of bins in the time dimension (horizontal resolution)
> \- \`bins\_y::Int\`: Number of bins in the value dimension (vertical resolution)
> 
> \# Returns
> \`density::Matrix{Float64}\`: A \`bins\_x ⋅ bins\_y\` matrix where each entry represents the
> normalized density contribution of this time series at that location.
> Each column sums to at most 1.0 (or 0.0 if no data passes through that time bin).
> """
> function compute\_normalized\_density(
> series::AbstractVector, bins\_x::Int, bins\_y::Int, value\_min::AbstractFloat, value\_max::AbstractFloat
> )::Matrix{Float64}
> n\_times = length(series)
> density = zeros(bins\_x, bins\_y)
> 
> # Map time indices to bins
> time\_to\_bin = range(1, bins\_x, n\_times)
> 
> # Map values to bins (higher values -\> higher row indices)
> value\_range = value\_max - value\_min
> value\_to\_bin(v) = clamp(round(Int, (v - value\_min) / value\_range \* (bins\_y - 1)) + 1, 1, bins\_y)
> 
> # Render each line segment
> for i in 1:(n\_times-1)
> x1 = round(Int, time\_to\_bin\[i\])
> y1 = value\_to\_bin(series\[i\])
> x2 = round(Int, time\_to\_bin\[i+1\])
> y2 = value\_to\_bin(series\[i+1\])
> 
> # Get pixels along the line using Bresenham's algorithm
> pixels = bresenham\_line(x1, y1, x2, y2)
> 
> # Mark all pixels as 1 (before normalization)
> for (px, py) in pixels
> if 1 \<= px \<= bins\_x && 1 \<= py \<= bins\_y
> density\[px, py\] = 1.0
> end
> end
> end
> 
> # Normalize by column (arc length normalization)
> for row in 1:bins\_x
> col\_sum = sum(view(density, row, :))
> if col\_sum \> 0
> density\[row, :\] ./= col\_sum
> end
> end
> 
> return density
> end
> 
> function denseline\_data(ts\_matrix::T, bins\_x::Int64, bins\_y::Int64)::T where {T\<:AbstractMatrix}
> n\_times, n\_series = size(ts\_matrix)
> 
> # Define value range for binning
> value\_min = minimum(ts\_matrix)
> value\_max = maximum(ts\_matrix)
> 
> # Initialize density matrix
> density\_map = zeros(bins\_x, bins\_y)
> 
> # Process each time series
> for series\_idx in 1:n\_series
> series = view(ts\_matrix, :, series\_idx)
> series\_density = compute\_normalized\_density(series, bins\_x, bins\_y, value\_min, value\_max)
> density\_map .+= series\_density
> end
> 
> return density\_map
> end
> 
> function example\_data()
> n\_times = 76
> n\_series = 10000
> t = range(0, 2π, n\_times)
> 
> time\_series = zeros(n\_times, n\_series)
> 
> # First group: constant frequency sine wave
> for i in 1:div(n\_series, 2)
> time\_series\[:, i\] = 2.0 .+ sin.(t) .+ 0.1 .\* randn(n\_times)
> end
> 
> # Second group: increasing frequency and amplitude
> for i in (div(n\_series, 2)+1):n\_series
> freq = range(1, 10, n\_times)
> amp = range(0.5, 2, n\_times)
> time\_series\[:, i\] = 1.0 .+ amp .\* sin.(freq .\* t) .+ 0.1 .\* randn(n\_times)
> end
> 
> return time\_series
> end
> 
> 
> """
> DenseLines
> 
> A Makie recipe for creating density line visualizations of multiple time series.
> 
> \# References
> \- Moritz, D., & Fisher, D. (2018).
> Visualizing a Million Time Series with the Density Line Chart.
> arXiv:1808.06019 \[cs.HC\]
> https://doi.org/10.48550/arXiv.1808.06019
> """
> @recipe DenseLines (ts\_matrix,) begin
> "Number of bins in time dimension"
> bins\_x = 400
> "Number of bins in value dimension"
> bins\_y = 300
> "Color scheme for the density heatmap"
> colormap = :viridis
> "Show colorbar"
> colorbar = true
> "Label for colorbar"
> colorbar\_label = "Density"
> 
> # Inherit standard plot attributes
> Makie.mixin\_generic\_plot\_attributes()...
> end
> 
> function Makie.plot!(dl::DenseLines)
> # Extract the time series matrix from converted arguments
> ts\_matrix = dl.ts\_matrix
> 
> # Compute density data with dynamic updating
> map!(dl.attributes, \[:ts\_matrix, :bins\_x, :bins\_y\], \[:density\_map, :value\_min, :value\_max\]) do ts\_mat, bx, by
> n\_times, n\_series = size(ts\_mat)
> 
> density\_map = denseline\_data(ts\_mat, bx, by)
> 
> # Define value range for binning
> val\_min = minimum(ts\_mat)
> val\_max = maximum(ts\_mat)
> 
> return (density\_map, val\_min, val\_max)
> end
> 
> # Compute coordinate ranges
> map!(dl.attributes, \[:ts\_matrix, :bins\_x, :bins\_y, :value\_min, :value\_max\], \[:x\_range, :y\_range\]) do ts\_mat, bx, by, val\_min, val\_max
> n\_times = size(ts\_mat, 1)
> x\_rng = range(1, n\_times, bx)
> y\_rng = range(val\_min, val\_max, by)
> return (x\_rng, y\_rng)
> end
> 
> # Create the heatmap visualization
> hm = heatmap!(
> dl,
> dl.x\_range,
> dl.y\_range,
> dl.density\_map,
> colormap=dl.colormap
> )
> 
> return dl
> end
> 
> Makie.argument\_names(::Type{\<:DenseLines}) = (:ts\_matrix,)
> 
> f, ax, sp = denselines(example\_data(); bins\_x=200, bins\_y=100, colormap=:plasma)
> ax.xlabel = "Time"
> ax.ylabel = "Value"
> \`\`\`
