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)
|