-- GoogleChart -- a Haskell module for using Google's Chart API -- Copyright (c) 2008 Evan Martin {-| This module is for generating web-based charts using Google's Chart API: . Its output is URLs that will resolve to a PNG image of the resulting chart. Most of the functions in this module, with names like @setFoo@, take a 'Chart' as an argument and produce a new 'Chart' with the specified attribute added. These calls are designed to be chained together. See the example below. 'Chart's are represented as a hierarchy of type classes so that parameters that only affect a specific chart type are only available to that chart type. @ putStrLn \"URL for your chart:\" putStrLn $ 'chartURL' $ 'setSize' 400 257 $ 'setTitle' \"My Chart\" $ 'setData' ('encodeDataSimple' [[1..20]]) $ 'setLegend' [\"1 to 20\"] $ 'newLineChart' @ This produces: Remaining features to implement: - lxy line charts - chbh bar charts - scatter plots - background\/fill colors - all style attributes -} module Graphics.Google.Chart ( -- * Chart basics -- | These functions and types are shared by all chart types. Chart, chartURL, setSize, setTitle, setTitleOpts, setData, -- ** Chart data -- | There are multiple options for encoding chart data. See -- for more details on -- the tradeoffs of the different encoding options. ChartData, encodeDataSimple, encodeDataText, encodeDataExtended, setDataColors, -- * Chart features -- ** Legends LegendChart, setLegend, -- ** Axis labels -- | The order of elements in the lists passed to these functions matter: -- If the first 'AxisType' passed to 'setAxisTypes' is 'AxisBottom', then -- the first set of labels passed to 'setAxisLabels' refers to that bottom -- axis. AxisLabelChart, setAxisTypes, AxisType(..), setAxisLabels, setAxisLabelPositions, setAxisRanges, AxisAlignment(..), setAxisStyles, -- * Specific chart types -- ** Line charts LineChart, newLineChart, -- ** Pie charts PieChart, newPieChart, PieStyle(..), setLabels, -- ** Bar charts BarChart, newBarChart, Orientation(..), BarStyle(..), -- ** Venn diagrams VennDiagram, newVennDiagram ) where import Data.Char (chr, ord) import Data.List (intercalate) import Numeric (showHex) -- | URL-encode a string. urlEnc str = concatMap enc str where enc c | c >= 'A' && c <= 'Z' = [c] | c >= 'a' && c <= 'z' = [c] | c >= '0' && c <= '9' = [c] | c `elem` safe = [c] | c == ' ' = "+" | otherwise = '%':(showHex (ord c) "") -- Argh, different resources differ on which characters need escaping. -- This is likely wrong. safe = "$-_.!*'(),|:" -- All charts are internally represented as a list of (key,value) pairs. -- We could switch this to Data.Map if it matters. type Params = [(String,String)] -- |The type class underneath all Charts. class Chart c where params :: c -> Params fromParams :: Params -> c setParam :: (Chart c) => String -> String -> c -> c setParam key val c = fromParams $ (key,val) : filter ((/= key) . fst) (params c) -- |Set the width and height, in pixels, of the resulting image. setSize :: (Chart c) => Int -> Int -> c -> c setSize width height = setParam "chs" (show width ++ "x" ++ show height) -- |Set the title of the chart. setTitle :: (Chart c) => String -> c -> c setTitle title = setParam "chtt" title -- |Set options for the display of the title of the chart. setTitleOpts :: (Chart c) => String -- ^Color of the text. -> Int -- ^Size of the text. -> c -> c setTitleOpts color size = setParam "chts" (color ++ "," ++ show size) -- |Set the data displayed by the chart. setData :: (Chart c) => ChartData -> c -> c setData (ChartData str) = setParam "chd" str -- |Construct the URL used to show the chart. chartURL :: (Chart c) => c -> String chartURL chart = baseURL ++ intercalate "&" urlparams where baseURL = "http://chart.apis.google.com/chart?" urlparams = [urlEnc a ++ "=" ++ urlEnc b | (a,b) <- params chart] -- |All the encoding methods produce 'ChartData', which is usable by 'setData'. newtype ChartData = ChartData String deriving Show -- |Encode data using the \"simple\" encoding, which maps each input value -- to a single letter in the URL. This produces minimal URLs but doesn't have -- as lot of resolution. Input values must be in the range @0 <= x <= 61@. -- Values outside the valid input range will be considered missing data. encodeDataSimple :: [[Int]] -> ChartData encodeDataSimple datas = ChartData $ "s:" ++ intercalate "," (map (map enc) datas) where enc i | i >= 0 && i <= 25 = chr (ord 'A' + i) | i >= 26 && i <= 51 = chr (ord 'a' + (i - 26)) | i >= 52 && i <= 61 = chr (ord '0' + (i - 52)) | otherwise = '_' -- |Encode data using the \"text\" encoding, which maps each input value to -- its string representation (e.g. \"3.4\") in the URL. This has more -- resolution than simple encoding but produces larger URLs. Input values must -- be in the range @0 <= x <= 100@. Values outside the valid input range will -- be considered missing data. Values with more than one decimal place of -- resolution will be rounded. encodeDataText :: RealFrac a => [[a]] -> ChartData -- I chose RealFrac because it's necessary for floor; what is appropriate here? -- The input must effectively be fixed-point. encodeDataText datas = ChartData $ "t:" ++ intercalate "|" (map encData datas) where encData = intercalate "," . map encDatum encDatum i | i >= 0 && i <= 100 = showDecimal i | otherwise = "-1" showDecimal :: RealFrac a => a -> String showDecimal i = show (fromIntegral (round (i * 10.0)) / 10.0) -- |Encode data using the \"extended\" encoding, which maps each input value -- to a two-character pair in base 64. This has more resolution than text -- encoding and is more compact. Input values must be in the range @0 <= x <= -- 4095@. Values outside the valid input range will be considered missing -- data. encodeDataExtended :: [[Int]] -> ChartData encodeDataExtended datas = ChartData $ "e:" ++ intercalate "," (map (concatMap encDatum) datas) where encDatum i | i >= 0 && i < 4096 = let (a, b) = i `quotRem` 64 in [encChar a, encChar b] | otherwise = "__" encChar i | i >= 0 && i <= 25 = chr (ord 'A' + i) | i >= 26 && i <= 51 = chr (ord 'a' + (i - 26)) | i >= 52 && i <= 61 = chr (ord '0' + (i - 52)) | i == 62 = '-' | i == 63 = '.' -- |Set data set colors. The nth color specified here colors the nth data -- set in the 'ChartData' passed to 'setData'. See -- for more -- information. setDataColors :: Chart c => [String] -> c -> c setDataColors colors = setParam "chco" (intercalate "," colors) -- |LegendChart represents charts that can display legends with 'setLegend'. class Chart c => LegendChart c -- |Set the legend for the corresponding data sets. The colors are taken -- from the data set colors. setLegend :: LegendChart c => [String] -> c -> c setLegend strs = setParam "chdl" (intercalate "|" strs) -- |AxisLabelChart represents charts that can display axis labels. class Chart c => AxisLabelChart c -- |Where to display an axis. data AxisType = AxisBottom | AxisTop | AxisLeft | AxisRight -- |Set which axes to display. Repeating an 'AxisType' produces multiple -- sets of labels on that axis. setAxisTypes :: AxisLabelChart c => [AxisType] -> c -> c setAxisTypes axes = setParam "chxt" (intercalate "," (map axisChar axes)) where axisChar AxisBottom = "x" axisChar AxisTop = "t" axisChar AxisLeft = "y" axisChar AxisRight = "r" -- |Set axis labels. The nth list of strings in the argument sets the labels -- for the nth axis specified with 'setAxisTypes'. An empty list of strings -- skips labelling the corresponding axis. setAxisLabels :: AxisLabelChart c => [[String]] -> c -> c setAxisLabels axislabels = setParam "chxl" (intercalate "|" (zipWith axisLabel [0..] axislabels)) where axisLabel :: Int -> [String] -> String axisLabel _ [] = "" axisLabel index labels = show index ++ ":|" ++ intercalate "|" labels -- |Set axis label positions. The nth list of Ints in the argument sets the -- positions for the nth axis specified with 'setAxisTypes'. An empty list -- skips setting position for the corresponding axis. setAxisLabelPositions :: AxisLabelChart c => [[Int]] -> c -> c setAxisLabelPositions positions = setParam "chxp" (intercalate "|" (zipWith axisPosn [0..] positions)) where axisPosn _ [] = "" axisPosn index xs = intercalate "," (map show (index:xs)) -- |Set axis ranges. The nth pair of Ints in the argument sets the range -- for the nth axis specified with 'setAxisTypes'. setAxisRanges :: AxisLabelChart c => [(Int,Int)] -> c -> c setAxisRanges ranges = setParam "chxr" (intercalate "|" (zipWith axisRange [0..] ranges)) where axisRange index (min,max) = intercalate "," (map show [index,min,max]) -- |Text alignment for labels on an axis. data AxisAlignment = AlignLeft | AlignCenter | AlignRight -- |Set axis styles. The nth element in the argument sets the style for the -- nth axis specified with 'setAxisTypes'. Each style is a tuple of -- (color, font size, text alignment). setAxisStyles :: AxisLabelChart c => [(String,Int,AxisAlignment)] -> c -> c setAxisStyles styles = setParam "chxs" (intercalate "|" (zipWith axisStyle [0..] styles)) where axisStyle index (color, size, align) = intercalate "," [show index, color, show size, alignString align] alignString AlignLeft = "-1" alignString AlignCenter = "0" alignString AlignRight = "1" newtype LineChart = LineChart Params instance Chart LineChart where params (LineChart p) = p fromParams = LineChart instance LegendChart LineChart instance AxisLabelChart LineChart newLineChart :: LineChart newLineChart = fromParams [("cht","lc")] newtype PieChart = PieChart Params instance Chart PieChart where params (PieChart p) = p fromParams = PieChart data PieStyle = Pie2D | Pie3D newPieChart :: PieStyle -> PieChart newPieChart Pie2D = fromParams [("cht","p")] newPieChart Pie3D = fromParams [("cht","p3")] -- |Set labels for the different data points on the chart. -- Specify missing values by passing an empty string. setLabels :: [String] -> PieChart -> PieChart setLabels labels = setParam "chl" $ intercalate "|" labels newtype BarChart = BarChart Params instance Chart BarChart where params (BarChart p) = p fromParams = BarChart instance LegendChart BarChart instance AxisLabelChart BarChart data Orientation = Horizontal | Vertical data BarStyle = Stacked | Grouped newBarChart :: Orientation -> BarStyle -> BarChart newBarChart orient style = fromParams [("cht",'b':oLetter orient:sLetter style:[])] where oLetter Horizontal = 'h' oLetter Vertical = 'v' sLetter Stacked = 's' sLetter Grouped = 'g' -- |Venn diagram data is specified in a particular format. There should be -- exactly seven data values, which represent, in order: -- circle A size, circle B size, circle C size, -- A\/B overlap, A\/C overlap, B\/C overlap, -- A\/B\/C overlap. newtype VennDiagram = VennDiagram Params instance Chart VennDiagram where params (VennDiagram p) = p fromParams = VennDiagram instance LegendChart VennDiagram newVennDiagram :: VennDiagram newVennDiagram = fromParams [("cht", "v")]