{-# LANGUAGE FlexibleInstances, OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Render (dumpAnalyses, renderOne, render) where import Control.Monad (forM_, when) import Numeric (showFFloat) -- Chart import Data.Char (isDigit) import Data.Colour import Data.Colour.Names import Graphics.Rendering.Chart.Easy hiding (render) import Graphics.Rendering.Chart.Backend.Cairo -- CSV import Data.Csv hiding ((.=)) import qualified Data.ByteString.Lazy as BL -- MIDI import MIDI (writeToMidi) import Types import Parser import Analysis ------------------- -- CSV files -- | Whether to print absolute values or percentages in CSV fields. data OutputMode = Absolute | Percentage -- | Conversion from AnalysisResult to CSV. instance ToNamedRecord (OutputMode, Analysis, AnalysisResult) where toNamedRecord (mode, curAnalysis, an) = namedRecord $ [ ("name", toField (name an)) , ("total", toField tot) ] ++ map (\(s,n) -> (toField s, showPercentage n)) (orderedResults curAnalysis an) ++ [ ("unclassified", showPercentage $ length (unclassified an)) ] where tot = total an showPercentage n = case mode of Absolute -> toField n Percentage -> toField $ showFFloat (Just 2) (fromIntegral n / fromIntegral tot * 100) "" -- | Dump all files, relevant to a (group of) analyses. dumpAnalyses :: String -> Bool -> Analysis -> [AnalysisResult] -> IO () dumpAnalyses fname expo curAnalysis as = do let resFields = (toField . fst) <$> orderedResults curAnalysis (head as) let headerFields = header $ ["name", "total"] ++ resFields ++ ["unclassified"] -- File #1: absolute values BL.writeFile (fname ++ ".csv") $ encodeByName headerFields (map (\a -> (Absolute, curAnalysis, a)) as) -- File #2: percentage values BL.writeFile (fname ++ "-percentages.csv") $ encodeByName headerFields (map (\a -> (Percentage, curAnalysis, a)) as) -- File #3: unclassified indices let uncls = concatMap unclassified as writeFile "unclassified.txt" $ unlines (fst <$> uncls) when expo $ do emptyDirectory "unclassified" cd "unclassified" $ forM_ uncls $ \(f, p) -> writeToMidi (f ++ ".mid") p -- | Show instance for AnalysisResult. instance {-# OVERLAPPING #-} Show (Analysis, AnalysisResult) where show (curAnalysis, an) = name an ++ " {" ++ "\n\ttotal: " ++ show tot ++ concat [ "\n\t" ++ s ++ ": " ++ showPercentage n | (s, n) <- orderedResults curAnalysis an ] ++ "\n\tother: " ++ showPercentage (length $ unclassified an) ++ "\n}" where tot = total an showPercentage n = showFFloat (Just 2) (fromIntegral n / fromIntegral tot * 100) "" ++ "% (" ++ show n ++ ")" ------------------- -- Charts -- | Visualize the results of analyzing a single pattern group in a pie chart. renderOne :: Analysis -> PatternGroup -> AnalysisResult -> IO () renderOne curAnalysis (PatternGroup piece_n expert_n pattern_n _ _) an = cd (piece_n ++ "/" ++ expert_n) $ render curAnalysis pattern_n an -- | Visualize an arbitrary AnalysisResult. render :: Analysis -> String -> AnalysisResult -> IO () render curAnalysis fname an | total an == 0 = return () | otherwise = do toFile def (fname ++ ".png") $ do pie_plot . pie_data .= values pie_plot . pie_colors .= map opaque colours pie_plot . pie_label_line_style . line_width .= 0.5 pie_plot . pie_label_style . font_size .= 14 where colours :: [Colour Double] colours = -- transformations (100%, 80%, 60%, 40%) take 50 (cycle [pink, darkblue, darkred, green, darkorange, darkcyan, darkmagenta, brown, darkviolet, darkorange]) -- other ++ [black] values :: [PieItem] values = [ pitem_value .~ v $ pitem_label .~ (if v > 2 then formatLabel s else "") $ pitem_offset .~ 30 $ def | (s, v) <- map (fmap (\n -> (fromIntegral n / fromIntegral (total an) * 100))) (orderedResults curAnalysis an ++ [("other", length $ unclassified an)]) ] formatLabel :: String -> String formatLabel s = trS ++ case n of ('0':_) -> " ~ " ++ show ((read n :: Int) * 10) ++ "%" _ -> "" where (trS, n) = break isDigit s