packages feed

nano-ui-diagrams-0.1.0.0: lib/NanoUI/Plot/Decimate.hs

-- | Point reduction for long series: largest-triangle-three-buckets and
-- per-bucket minimum and maximum.
module NanoUI.Plot.Decimate
  ( lttb
  , minMaxDecimate
  ) where

import Control.Monad.ST (runST)
import Data.Vector.Generic (Vector)
import qualified Data.Vector.Generic as V
import qualified Data.Vector.Generic.Mutable as MV
import qualified Data.Vector.Unboxed as U

-- | Largest-Triangle-Three-Buckets sampling. Returns at most the requested
-- number of points in input order, preserving both endpoints for budgets >= 2.
{-# INLINABLE lttb #-}
{-# SPECIALIZE lttb :: Int -> U.Vector (Double, Double) -> U.Vector (Double, Double) #-}
lttb :: Vector v (Double, Double) => Int -> v (Double, Double) -> v (Double, Double)
lttb k0 pts
  | k0 <= 0 = V.empty
  | n <= k0 = pts
  | k0 == 1 = V.take 1 pts
  | k0 == 2 = V.fromList [V.head pts, V.last pts]
  | otherwise = V.create $ do
      out <- MV.new k0
      let !k = k0
          !bucketSize = fromIntegral (n - 2) / (fromIntegral (k - 2) :: Double)
          !firstPt = pts V.! 0
          !lastPt = pts V.! (n - 1)
          go !i !prevIdx
            | i >= k - 2 = MV.write out (k - 1) lastPt
            | otherwise =
                let !rangeStart = floor (fromIntegral i * bucketSize) + 1
                    !rangeEnd = min (n - 1) (floor (fromIntegral (i + 1) * bucketSize) + 1)
                    !avgStart = rangeEnd
                    !avgEnd = min n (floor (fromIntegral (i + 2) * bucketSize) + 1)
                    (!avgX, !avgY) = bucketAvg pts avgStart avgEnd
                    !prevPt = pts V.! prevIdx
                    findBest !j !bestIdx !bestArea
                      | j >= rangeEnd = bestIdx
                      | otherwise =
                          let !area = triArea prevPt (pts V.! j) (avgX, avgY)
                           in if area > bestArea
                                then findBest (j + 1) j area
                                else findBest (j + 1) bestIdx bestArea
                    -- An empty range keeps rangeStart; areas are never negative.
                    !best = findBest rangeStart rangeStart (-1)
                 in do
                   MV.write out (i + 1) (pts V.! best)
                   go (i + 1) best
      MV.write out 0 firstPt
      go 0 0
      pure out
  where
    !n = V.length pts

{-# INLINE bucketAvg #-}
bucketAvg :: Vector v (Double, Double) => v (Double, Double) -> Int -> Int -> (Double, Double)
bucketAvg pts !start !end
  | start >= end = (0, 0)
  | otherwise =
      let !len = end - start
          !denom = fromIntegral len :: Double
          go !i !sx !sy
            | i >= end = (sx / denom, sy / denom)
            | otherwise =
                let (!x, !y) = pts V.! i
                 in go (i + 1) (sx + x) (sy + y)
       in go start 0 0

triArea :: (Double, Double) -> (Double, Double) -> (Double, Double) -> Double
triArea (!x0, !y0) (!x1, !y1) (!x2, !y2) =
  abs ((x0 - x2) * (y1 - y0) - (x0 - x1) * (y2 - y0)) * 0.5

-- | Split into at most @k@ buckets and retain each bucket's Y extrema in
-- input order. The output has at most @2*k@ points; non-positive budgets
-- return an empty vector. A point selected as both extrema is emitted once.
{-# INLINABLE minMaxDecimate #-}
{-# SPECIALIZE minMaxDecimate :: Int -> U.Vector (Double, Double) -> U.Vector (Double, Double) #-}
minMaxDecimate :: Vector v (Double, Double) => Int -> v (Double, Double) -> v (Double, Double)
minMaxDecimate k pts
  | k <= 0 = V.empty
  | len <= k = pts
  | otherwise = runST $ do
      out <- MV.new (min len (2 * numChunks))
      let chunks !start !written
            | start >= len = V.freeze (MV.slice 0 written out)
            | otherwise = do
                let !end = start + min bucket (len - start)
                    extrema !i !lowIdx !highIdx
                      | i >= end = (lowIdx, highIdx)
                      | otherwise =
                          let !y = snd (pts V.! i)
                              !lo' = if y < snd (pts V.! lowIdx) then i else lowIdx
                              !hi' = if y > snd (pts V.! highIdx) then i else highIdx
                           in extrema (i + 1) lo' hi'
                    (!lo, !hi) = extrema (start + 1) start start
                MV.write out written (pts V.! min lo hi)
                if lo == hi
                  then chunks end (written + 1)
                  else do
                    MV.write out (written + 1) (pts V.! max lo hi)
                    chunks end (written + 2)
      chunks 0 0
  where
    !len = V.length pts
    bucket = (len - 1) `div` k + 1
    numChunks = (len - 1) `div` bucket + 1