First we combine all the useful functions from part one to get a list of smoothed out pace values:
transformAndSmoothRunData :: [Double] -> [Double] transformAndSmoothRunData = flipInRange . (convolve (gaussianKernel 5)) . (correlate (movingAverageKernel 6)) . (map (\x -> (1.0 / (6.0 * x)))) . (filter (/=0)) . diff
This gives us a smoothed moving average of our pace (one minute moving average window, because the measurements are in intervals of 10 secs), flipped so that the faster pace (smaller values) will appear on the upper y-regions of the chart. We also define a version of the function that skips the gaussian smoothing which we will use for range calculation because the gaussian smoothing shifts the values away:
transformAndAverageRunData :: [Double] -> [Double] transformAndAverageRunData = flipInRange . (correlate (movingAverageKernel 6)) . (map (\x -> (1.0 / (6.0 * x)))) . (filter (/=0)) . diff
A google charts url encodes the actual chart data in the url. There are several ways to encode it, we will use extended encoding. This means scaling the values into the range [0.. 4095]. We define a function to do this:
scaler :: [Double] -> Double -> (Double -> Double)
scaler xs y = (\x -> (x - minV) * y / d)
where
minV = minInList xs
maxV = maxInList xs
d = maxV - minV
It takes a list of doubles and a target range (well, the right value of the target range, the left value is assumed to be zero) and returns a function that maps any double value into that target range.
We're now ready to encode the data. Fortunately there already is a Haskell Google Charts hackage which will take over most of the work from us. First we import it and call its encodeDataExtended to encode our data:
encodeRunData :: [Double] -> ChartData
encodeRunData xs = encodeDataExtended [xs']
where
sc = scaler xs (fromIntegral 4095)
xs' = map (round . sc) xs :: [Int]
This will take care of the data part of the chart. We still want to define axis labels though. Let's say we want 5 axis labels spread evenly for both x axis and y axis. We define a function that given a range, samples n values from the range, evenly spread out:
sampler :: Int -> Double -> Double -> [Double]
sampler n minV maxV =
[(minV + d * (fromIntegral x) / (fromIntegral n)) | x <- [0..n]]
where
d = maxV - minV
With this function we define x and y labels functions:
yLabels :: Int -> [Double] -> [String]
yLabels n xs =
map (\x -> printf "%.2f" x)
(reverse (sampler n (minInList xs) (maxInList xs)))
xLabels :: Int -> [Double] -> [String]
xLabels n xs =
map (\x -> printf "%.1f" x) (sampler n 0.0 duration)
where
duration = (fromIntegral $ length xs) / 6.0
We use a little helper function that generates an "append a suffix to a string" function to append a magic incantation at the end of the url we're building to generate a grid on the chart:
suffix :: String -> (String -> String) suffix s = (\x -> x ++ s)
With this we define the chartRun function which will return a google charts url given the width and height of the chart and the actual data:
chartRun :: Int -> Int -> [Double] -> String
chartRun w h xs =
suffix "&chg=25.0,25.0,3,2" $
chartURL $
setAxisLabelPositions [[0, 25, 50, 75, 100], [50], [0, 25, 50, 75, 100], [50]] $
setAxisLabels
[(yLabels 4 ylxs), ["pace (min/km)"], (xLabels 4 xs), ["time (min)"]] $
setAxisTypes [AxisLeft, AxisLeft, AxisBottom, AxisBottom] $
setSize w h $
setData (encodeRunData txs) $
newLineChart
where
ylxs = transformAndAverageRunData xs
txs = transformAndSmoothRunData xs
So let's try it out (pretend we have the run data already bound to oneRun, we just copy it in from xml for now for testing):
putStrLn $ chartRun 600 200 oneRun
We get:
Small confession: I changed a little the parameters in this haskell version from the charts you've seen in the blog run entries which for now are still done by the scala script. I like the less aggressive averaging and smoothing better so once the haskell version is ready the charts will look more like the one in this entry.
In the next part we will see how we can fetch the xml from the Nike+ servers and how we can process it to extract what we need. In the meantime here is today's module with all the functions from this entry:
module Codemanic.NikeRuns
(
chartRun
)
where
import Codemanic.NumericLists
import Graphics.Google.Chart
import Text.Printf
transformAndSmoothRunData :: [Double] -> [Double]
transformAndSmoothRunData xs =
(flipInRange .
(convolve (gaussianKernel 5)) .
(correlate (movingAverageKernel 6)) .
(map (\x -> (1.0 / (6.0 * x)))) .
(filter (/=0)) .
diff) xs
transformAndAverageRunData :: [Double] -> [Double]
transformAndAverageRunData xs =
(flipInRange .
(correlate (movingAverageKernel 6)) .
(map (\x -> (1.0 / (6.0 * x)))) .
(filter (/=0)) .
diff) xs
scaler :: [Double] -> Double -> (Double -> Double)
scaler xs y = (\x -> (x - minV) * y / d)
where
minV = minInList xs
maxV = maxInList xs
d = maxV - minV
encodeRunData :: [Double] -> ChartData
encodeRunData xs = encodeDataExtended [xs']
where
sc = scaler xs (fromIntegral 4095)
xs' = map (round . sc) xs :: [Int]
sampler :: Int -> Double -> Double -> [Double]
sampler n minV maxV =
[(minV + d * (fromIntegral x) / (fromIntegral n)) | x <- [0..n]]
where
d = maxV - minV
yLabels :: Int -> [Double] -> [String]
yLabels n xs =
map (\x -> printf "%.2f" x)
(reverse (sampler n (minInList xs) (maxInList xs)))
xLabels :: Int -> [Double] -> [String]
xLabels n xs =
map (\x -> printf "%.1f" x) (sampler n 0.0 duration)
where
duration = (fromIntegral $ length xs) / 6.0
suffix :: String -> (String -> String)
suffix s = (\x -> x ++ s)
chartRun :: Int -> Int -> [Double] -> String
chartRun w h xs =
suffix "&chg=25.0,25.0,3,2" $
chartURL $
setAxisLabelPositions [[0, 25, 50, 75, 100], [50], [0, 25, 50, 75, 100], [50]] $
setAxisLabels
[(yLabels 4 ylxs), ["pace (min/km)"], (xLabels 4 xs), ["time (min)"]] $
setAxisTypes [AxisLeft, AxisLeft, AxisBottom, AxisBottom] $
setSize w h $
setData (encodeRunData txs) $
newLineChart
where
ylxs = transformAndAverageRunData xs
txs = transformAndSmoothRunData xs
Leave a comment