File: Colors.hs

package info (click to toggle)
haskell-graphviz 2999.17.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 1,488 kB
  • sloc: haskell: 12,152; makefile: 2
file content (381 lines) | stat: -rw-r--r-- 13,550 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
{-# LANGUAGE OverloadedStrings #-}

{- |
   Module      : Data.GraphViz.Attributes.Colors
   Description : Specification of Color-related types and functions.
   Copyright   : (c) Ivan Lazar Miljenovic
   License     : 3-Clause BSD-style
   Maintainer  : Ivan.Miljenovic@gmail.com

   This module defines the various colors, etc. for Graphviz.  For
   information on colors in general, see:
     <http://graphviz.org/doc/info/attrs.html#k:color>
   For named colors, see:
     <http://graphviz.org/doc/info/colors.html>

   Note that the ColorBrewer Color Schemes (shortened to just
   \"Brewer\" for the rest of this module) are covered by the
   following license (also available in the LICENSE file of this
   library):
     <http://graphviz.org/doc/info/colors.html#brewer_license>
-}
module Data.GraphViz.Attributes.Colors
       ( -- * Color schemes.
         ColorScheme(..)
         -- * Colors
       , Color(..)
       , ColorList
       , WeightedColor(..)
       , toWC
       , toColorList
       , NamedColor(toColor)
       , toWColor
         -- * Conversion to\/from @Colour@.
       , toColour
       , fromColour
       , fromAColour
       ) where

import Data.GraphViz.Attributes.Colors.Brewer (BrewerColor (..))
import Data.GraphViz.Attributes.Colors.SVG    (SVGColor, svgColour)
import Data.GraphViz.Attributes.Colors.X11    (X11Color (Transparent),
                                               x11Colour)
import Data.GraphViz.Attributes.ColorScheme   (ColorScheme (..))
import Data.GraphViz.Exception
import Data.GraphViz.Internal.State
import Data.GraphViz.Internal.Util            (bool)
import Data.GraphViz.Parsing
import Data.GraphViz.Printing

import Data.Colour              (AlphaColour, alphaChannel, black, darken,
                                 opaque, over, withOpacity)
import Data.Colour.RGBSpace     (uncurryRGB)
import Data.Colour.RGBSpace.HSV (hsv)
import Data.Colour.SRGB         (Colour, sRGB, sRGB24, toSRGB24)

import           Data.Char      (isHexDigit)
import           Data.Maybe     (isJust)
import qualified Data.Text.Lazy as T
import           Data.Word      (Word8)
import           Numeric        (readHex, showHex)

-- -----------------------------------------------------------------------------

-- | Defining a color for use with Graphviz.  Note that named colors
--   have been split up into 'X11Color's and those based upon the
--   Brewer color schemes.
data Color = RGB { red   :: Word8
                 , green :: Word8
                 , blue  :: Word8
                 }
           | RGBA { red   :: Word8
                  , green :: Word8
                  , blue  :: Word8
                  , alpha :: Word8
                  }
             -- | The 'hue', 'saturation' and 'value' values must all
             --   be @0 <= x <=1@.
           | HSV { hue        :: Double
                 , saturation :: Double
                 , value      :: Double
                 }
           | X11Color X11Color
           | SVGColor SVGColor
           | BrewerColor BrewerColor
           deriving (Eq, Ord, Show, Read)

instance PrintDot Color where
  unqtDot (RGB  r g b)     = hexColor [r,g,b]
  unqtDot (RGBA r g b a)   = hexColor [r,g,b,a]
  unqtDot (HSV  h s v)     = hcat . punctuate comma $ mapM unqtDot [h,s,v]
  unqtDot (SVGColor name)  = printNC False name
  unqtDot (X11Color name)  = printNC False name
  unqtDot (BrewerColor bc) = printNC False bc

  -- Some cases might not need quotes.
  toDot (X11Color name)  = printNC True name
  toDot (SVGColor name)  = printNC True name
  toDot (BrewerColor bc) = printNC True bc
  toDot c                = dquotes $ unqtDot c

  unqtListToDot = hcat . punctuate colon . mapM unqtDot

  -- These three might not need to be quoted if they're on their own.
  listToDot [X11Color name]  = printNC True name
  listToDot [SVGColor name]  = printNC True name
  listToDot [BrewerColor bc] = printNC True bc
  listToDot cs               = dquotes $ unqtListToDot cs

hexColor :: [Word8] -> DotCode
hexColor = (<>) (char '#') . hcat . mapM word8Doc

word8Doc   :: Word8 -> DotCode
word8Doc w = text $ padding `T.append` simple
  where
    simple = T.pack $ showHex w ""
    padding = T.replicate count (T.singleton '0')
    count = 2 - findCols 1 w
    findCols c n
      | n < 16 = c
      | otherwise = findCols (c+1) (n `div` 16)

instance ParseDot Color where
  parseUnqt = oneOf [ parseHexBased
                    , parseHSV
                      -- Have to parse BrewerColor first, as some of them may appear to be X11 colors
                    , parseNC (undefined :: BrewerColor) False
                    , parseNC (undefined :: SVGColor) False
                    , parseX11Color False
                    ]
              `onFail`
              fail "Could not parse Color"
    where
      parseHexBased
          = character '#' *>
            do cs <- many1 parse2Hex
               return $ case cs of
                          [r,g,b] -> RGB r g b
                          [r,g,b,a] -> RGBA r g b a
                          _ -> throw . NotDotCode
                               $ "Not a valid hex Color specification: "
                                  ++ show cs
      parseHSV = HSV <$> parseUnqt
                     <*  parseSep
                     <*> parseUnqt
                     <*  parseSep
                     <*> parseUnqt
      parseSep = character ',' *> whitespace <|> whitespace1

      parse2Hex = do c1 <- satisfy isHexDigit
                     c2 <- satisfy isHexDigit
                     let [(n, [])] = readHex [c1, c2]
                     return n

  parse = quotedParse parseUnqt
          `onFail` -- These three might not need to be quoted
          oneOf [ parseNC (undefined :: BrewerColor) True
                , parseNC (undefined :: SVGColor) True
                , parseX11Color True
                ]
          `onFail`
          fail "Could not parse Color"

  parseUnqtList = sepBy1 parseUnqt (character ':')
                  `onFail`
                  do cs <- getColorScheme
                     failBad $ "Error parsing list of Colors with color scheme of "
                               ++ show cs

  parseList = fmap (:[])
              -- Potentially unquoted single color
              (oneOf [ parseNC (undefined :: BrewerColor) True
                     , parseNC (undefined :: SVGColor) True
                     , parseX11Color True
                     ]
              )
              `onFail`
              quotedParse parseUnqtList
              `onFail`
              do cs <- getColorScheme
                 failBad $ "Error parsing list of Colors with color scheme of "
                           ++ show cs

-- | The sum of the optional weightings /must/ sum to at most @1@.
type ColorList = [WeightedColor]

-- | A 'Color' tagged with an optional weighting.
data WeightedColor = WC { wColor    :: Color
                          -- | Must be in range @0 <= W <= 1@.
                        , weighting :: Maybe Double
                        }
                   deriving (Eq, Ord, Show, Read)

-- | For colors without weightings.
toWC :: Color -> WeightedColor
toWC = (`WC` Nothing)

-- | For a list of colors without weightings.
toColorList :: [Color] -> ColorList
toColorList = map toWC

instance PrintDot WeightedColor where
  unqtDot (WC c mw) = unqtDot c
                      <> maybe empty ((semi<>) . unqtDot) mw

  toDot (WC c Nothing) = toDot c
  toDot wc             = dquotes $ unqtDot wc

  unqtListToDot = hcat . punctuate colon . mapM unqtDot

  -- Might not need quoting
  listToDot [wc] = toDot wc
  listToDot wcs  = dquotes $ unqtListToDot wcs

instance ParseDot WeightedColor where
  parseUnqt = WC <$> parseUnqt <*> optional (character ';' *> parseUnqt)

  parse = quotedParse parseUnqt
          `onFail`
          -- Using parse rather than parseUnqt as there shouldn't be
          -- any quotes, but to avoid copy-pasting the oneOf block.
          (toWC <$> parse)

  parseUnqtList = sepBy1 parseUnqt (character ':')
                  `onFail`
                  do cs <- getColorScheme
                     failBad $ "Error parsing a ColorList with color scheme of "
                               ++ show cs

  parseList = quotedParse parseUnqtList
              `onFail`
              ((:[]) . toWC <$> parse)
              -- Potentially unquoted un-weighted single color
              `onFail`
              do cs <- getColorScheme
                 failBad $ "Error parsing ColorList with color scheme of "
                           ++ show cs

-- -----------------------------------------------------------------------------

-- | More easily convert named colors to an overall 'Color' value.
class NamedColor nc where
    colorScheme :: nc -> ColorScheme

    toColor :: nc -> Color

    printNC :: Bool -> nc -> DotCode

    -- | Bool is for whether quoting is needed.
    parseNC' :: Bool -> Parse nc

toWColor :: (NamedColor nc) => nc -> WeightedColor
toWColor = toWC . toColor

-- First value just used for type
parseNC :: (NamedColor nc) => nc -> Bool -> Parse Color
parseNC nc q = fmap (toColor . (`asTypeOf` nc))
               $ parseNC' q

instance NamedColor BrewerColor where
    colorScheme (BC bs _) = Brewer bs

    toColor = BrewerColor

    printNC = printNamedColor (\ (BC _ l) -> l)

    parseNC' = parseNamedColor mBCS parseUnqt (const True) BC
        where
          mBCS (Brewer bs) = Just bs
          mBCS _           = Nothing

instance NamedColor X11Color where
    colorScheme = const X11

    toColor = X11Color

    printNC = printNamedColor id

    parseNC' = parseNamedColor mX11 (parseColorScheme False) (isJust . mX11) (const id)
        where
          mX11 X11 = Just X11
          mX11 _   = Nothing

instance NamedColor SVGColor where
    colorScheme = const SVG

    toColor = SVGColor

    printNC = printNamedColor id

    parseNC' = parseNamedColor mSVG (parseColorScheme False) (isJust . mSVG) (const id)
        where
          mSVG SVG = Just SVG
          mSVG _   = Nothing

printNamedColor :: (NamedColor nc, PrintDot lv) => (nc -> lv)
                   -> Bool -> nc -> DotCode
printNamedColor fl q c = do currentCS <- getColorScheme
                            if cs == currentCS
                               then (bool unqtDot toDot q) lv
                               else bool id dquotes q
                                    $ fslash <> printColorScheme False cs
                                      <> fslash <> unqtDot lv
    where
      cs = colorScheme c
      lv = fl c

parseNamedColor :: (NamedColor nc, ParseDot lv)
                   => (ColorScheme -> Maybe cs) -> Parse cs -> (cs -> Bool)
                   -> (cs -> lv -> nc) -> Bool -> Parse nc
parseNamedColor gcs parseCS vcs mkC q
    = do Just cs <- gcs <$> getColorScheme
         lv <- bool parseUnqt parse q
               `onFail`
               mQts (string "//" *> parseUnqt)
         return $ mkC cs lv
      `onFail`
      mQts ( do character '/'
                cs <- parseCS
                character '/'
                if vcs cs
                   then mkC cs <$>  parseUnqt
                   else fail "Explicit colorscheme not as expected."
           )
    where
      mQts = bool id quotedParse q

-- -----------------------------------------------------------------------------

-- X11 has a special case when parsing: '/yyyy'

parseX11Color   :: Bool -> Parse Color
parseX11Color q = X11Color
                  <$> parseNC' q
                      `onFail`
                      bool id quotedParse q (character '/' *> parseUnqt)
                      `onFail`
                      -- Can use X11 colors within brewer colorscheme.
                      do cs <- getColorScheme
                         case cs of
                           Brewer{} -> bool parseUnqt parse q
                           _        -> fail "Unable to parse an X11 color within Brewer"

-- -----------------------------------------------------------------------------

-- | Attempt to convert a 'Color' into a 'Colour' value with an alpha
--   channel.  The use of 'Maybe' is because the RGB values of the
--   'BrewerColor's haven't been stored here (primarily for licensing
--   reasons).
toColour                :: Color -> Maybe (AlphaColour Double)
toColour (RGB r g b)    = Just . opaque $ sRGB24 r g b
toColour (RGBA r g b a) = Just . withOpacity (sRGB24 r g b) $ toOpacity a
-- Colour expects the hue to be an angle, so multiply by 360
toColour (HSV h s v)    = Just . opaque . uncurryRGB sRGB $ hsv (h*360) s v
toColour (X11Color c)   = Just $ x11Colour c
toColour (SVGColor c)   = Just . opaque $ svgColour c
toColour BrewerColor{}  = Nothing

toOpacity   :: Word8 -> Double
toOpacity a = fromIntegral a / maxWord

-- | Convert a 'Colour' value to an 'RGB' 'Color'.
fromColour :: Colour Double -> Color
fromColour = uncurryRGB RGB . toSRGB24

-- | Convert an 'AlphaColour' to an 'RGBA' 'Color'.  The exception to
--   this is for any 'AlphaColour' which has @alphaChannel ac == 0@;
--   these are converted to @X11Color 'Transparent'@ (note that the
--   'Show' instance for such an 'AlphaColour' is @\"transparent\"@).
fromAColour :: AlphaColour Double -> Color
fromAColour ac
  | a == 0    = X11Color Transparent
  | otherwise = rgb $ round a'
  where
    a = alphaChannel ac
    a' = a * maxWord
    rgb = uncurryRGB RGBA $ toSRGB24 colour
    colour = darken (recip a) (ac `over` black)

-- | The 'maxBound' of a 'Word8' value.
maxWord :: Double
maxWord = fromIntegral (maxBound :: Word8)