Coverage for gramex\pptgen\color.py: 53%

# Coverage for gramex\pptgen\color.py : 53%

## 159 statements 95 run 64 missing 0 excluded 8 partial

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

1"""

2 A color and gradient management system.

3

4*Do not pick colors yourself*. A lot of research has gone into color

5 palettes. Specifically, for:

6

7 Numeric data

8 use `ColorBrewer <http://colorbrewer2.org/>`_ themes

9

10 Non-numeric data

11 use `ColorBrewer <http://colorbrewer2.org/>`_

12 or `Microsoft Office themes`_

13

14 Page design

15 use `kuler.adobe.com <https://kuler.adobe.com/#create/fromanimage>`_

16 *from an image* (not directly).

17

18

19 Microsoft office themes

20-----------------------

21

22 The following color palettes based on Microsoft Office are available:

23

24 Office, Adjacency, Apex, Apothecary, Aspect, Austin, BlackTie, Civic, Clarity,

25 Composite, Concourse, Couture, Elemental, Equity, Essential, Executive, Flow,

26 Foundry, Grid, Hardcover, Horizon, Median, Metro, Module, Newsprint, Opulent,

27 Oriel, Origin, Paper, Perspective, Pushpin, SlipStream, Solstice, Technic,

28 Thatch, Trek, Urban, Verve, Waveform

29

30 A palette's colours can be accessed as an array or as an attribute. For e.g.:

31

32>>> Office[0] # The first element

33 u'#4f81bd'

34>>> Office.accent_1 # ... is called .accent_1

35 u'#4f81bd'

36>>> Office[1] # The next element

37 u'#c0504d'

38>>> Office.accent_2 # ... is called .accent_2

39 u'#c0504d'

40

41 The following 10 attributes are available (in order):

42

43 0. accent_1

44 1. accent_2

45 2. accent_3

46 3. accent_4

47 4. accent_5

48 5. accent_6

49 6. light_2

50 7. dark_2

51 8. light_1

52 9. dark_1

53

54"""

55

56 import os

57 import re

58 import six

59 import json

60 import operator

61 import colorsys

62 import warnings

63 import numpy as np

64 from io import open

65

66

67 BASE_0 = 0

68 BASE_1 = 1

69 BASE_2 = 2

70 BASE_3 = 3

71 BASE_4 = 4

72 BASE_5 = 5

73 BASE_7 = 7

74 BASE_9 = 9

75 BASE_15 = 15

76 BASE_16 = 16

77 BASE_255 = 255

78 BASE_256 = 256

79

80

81 class _MSO(object):

82 """

83 Microsoft office themes. Refer to colors in any of these ways:

84

85 color['accent_1']

86 color[0]

87 color.accent_1

88 color[:1]

89 """

90

91 _lookup = {

92 'accent_1': 0,

93 'accent_2': 1,

94 'accent_3': 2,

95 'accent_4': 3,

96 'accent_5': 4,

97 'accent_6': 5,

98 'light_2': 6,

99 'dark_2': 7,

100 'light_1': 8,

101 'dark_1': 9,

102 }

103

104 def __init__(self, *values):

105 self.values = values

106

107 def __getitem__(self, key):

108 if isinstance(key, slice) or type(key) is int:

109 return self.values.__getitem__(key)

110 elif key in self._lookup:

111 return self.values[self._lookup[key]]

112

113 def __getattr__(self, key):

114 if key in self._lookup:

115 return self.values[self._lookup[key]]

116

117 def __len__(self):

118 return len(self.values)

119

120 def __str__(self):

121 return ' '.join(self.values)

122

123 def __repr__(self):

124 return '_MSO' + repr(self.values)

125

126

127 def _rgb(color):

128 """

129 .. deprecated:: 0.1

130 Use :func:`rgba` instead

131 """

132 warnings.warn('Use color.rgba instead of color._rgb',

133 FutureWarning, stacklevel=2)

134 return (int(color[-6:-4], BASE_16), int(color[-4:-2], BASE_16),

135 int(color[-2:], BASE_16))

136

137

138 def gradient(value, grad, opacity=1, sort=True):

139 """

140 Converts a number or list ``x`` into a color using a gradient.

141

142

143 :arg number value: int, float, list, numpy array or any iterable.

144 If an iterable is passed, a list of colors is returned.

145 :arg list grad: tuple/list of ``(value, color)`` tuples

146 For example, ``((0, 'red'), (.5, 'yellow'), (1, 'green'))``.

147 Values will be sorted automatically

148 :arg float opacity: optional alpha to be added to the returned color

149 :arg bool sort: optional. If ``grad`` is already sorted, set ``sort=False``

150

151 These gradients are available:

152

153 **divergent gradients** : on a ``[-1, 1]`` scale

154 multi-color

155 ``RdGy``, ``RdYlGn``, ``Spectral``

156 ... also print-friendly and color-blind safe

157 ``BrBG``, ``PiYG``, ``PRGn``, ``RdBu``, ``RdYlBu``

158 ... and also photocopyable

159 ``PuOr``

160 **sequential gradients** : on a ``[0, 1]`` scale

161 one-color

162 ``Reds``, ``Blues``, ``Greys``, ``Greens``, ``Oranges``,

163 ``Purples``, ``Browns``, ``Yellows``

164 two-color

165 ``BuGn``, ``BuPu``, ``GnBu``, ``OrRd``, ``PuBu``, ``PuRd``,

166 ``RdPu``, ``YlGn``

167 three-color

168 ``YlGnBu``, ``YlOrBr``, ``YlOrRd``, ``PuBuGn``

169

170 The following are also available, but do not use them.

171

172 - ``RYG`` maps ``[0, 1]`` to Red-Yellow-Green

173 - ``RWG`` maps ``[0, 1]`` to Red-White-Green

174 - ``RYG_1`` maps ``[-1, -1]`` to Red-Yellow-Green

175 - ``RWG_1`` maps ``[-1, -1]`` to Red-White-Green

176

177 Examples::

178

179 # Get a value that is 40% blue and 60% white, use:

180 >>> gradient(0.4, ((0, 'blue'), (1, 'white')))

181 '#66f'

182

183 # A list (or any iterable) input returns a list of colors

184 >>> gradient([0.2, 0.6, 0.8], ((0, 'blue'), (1, 'white')))

185 ['#33f', '#99f', '#ccf']

186

187 # Values out of range are truncated.

188 >>> gradient([-10, +10], ((0, 'blue'), (1, 'white')))

189 ['blue', 'white']

190

191 # Use a pre-defined gradient to classify a value between -1 to 1 on a

192 # Red-Yellow-Green scale. (Returns a value between Yellow and Green).

193 >>> gradient(0.5, RdYlGn)

194 '#8ccb87'

195 """

196 if sort:

197 grad = sorted(grad, key=operator.itemgetter(BASE_0))

198

199 # If value is iterable, apply gradient to each value (recursively)

200 if np.ndim(value) > BASE_0:

201 return [gradient(val, grad, opacity=opacity, sort=False)

202 for val in value]

203

204 value = float(value) if not np.isnan(value) else BASE_0

205 if value <= grad[BASE_0][BASE_0]:

206 return grad[BASE_0][BASE_1]

207

208 grd_idx = -1

209 if value >= grad[grd_idx][BASE_0]: 209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true

210 return grad[grd_idx][BASE_1]

211 i = BASE_0

212 for i, (start, color1) in enumerate(grad): 212 ↛ 215line 212 didn't jump to line 215, because the loop on line 212 didn't complete

213 if value <= start:

214 break

215 dist1 = (value - grad[i - BASE_1][BASE_0]) / (

216 grad[i][BASE_0] - grad[i - BASE_1][BASE_0])

217

218 sec_dist = 1.0

219 dist2 = sec_dist - dist1

220 color1 = rgba(grad[i - BASE_1][BASE_1])

221 color2 = rgba(grad[i][BASE_1])

222 return name(color1[BASE_0] * dist2 + color2[BASE_0] * dist1,

223 color1[BASE_1] * dist2 + color2[BASE_1] * dist1,

224 color1[BASE_2] * dist2 + color2[BASE_2] * dist1,

225 opacity)

226

227

228 _DISTINCTS = [

229 "#1f77b4", "#aec7e8",

230 "#ff7f0e", "#ffbb78",

231 "#2ca02c", "#98df8a",

232 "#d62728", "#ff9896",

233 "#9467bd", "#c5b0d5",

234 "#8c564b", "#c49c94",

235 "#e377c2", "#f7b6d2",

236 "#7f7f7f", "#c7c7c7",

237 "#bcbd22", "#dbdb8d",

238 "#17becf", "#9edae5"

239]

240

241

242 def distinct(count):

243 """

244 Generates a list of ``count`` distinct colors, for up to 20 colors.

245

246 :arg int count: number of colors to return

247

248 Examples::

249

250 >>> distinct(4)

251 ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728']

252

253 Notes:

254

255 - Colour conversion between RGB and HCL are available on

256 `color.py <http://2sn.org/python/color.py>`_ and

257 `python-colormath <http://python-colormath.readthedocs.org/>`_

258 - People seem to struggle with about

259 `26 colours

260 <http://eleanormaclure.files.wordpress.com/2011/03/colour-coding.pdf>`_

261 so this may be an upper bound

262 - A good starting point for resources is on

263 `stackoverflow <http://stackoverflow.com/q/470690/100904>`_ and the

264 `graphic design <http://graphicdesign.stackexchange.com/q/3682>`_

265 stackexchange

266 - More palettes are available from

267 `Kenneth Kelly (22) <http://www.iscc.org/pdf/PC54_1724_001.pdf>`_ and

268 `ggplot2 <http://learnr.wordpress.com/2009/04/15/ggplot2/>`_

269 - See also: http://epub.wu.ac.at/1692/1/document.pdf

270 """

271 tens_count = 10

272 twenty_count = 20

273 _distinct = 2

274 if count <= tens_count:

275 return [_DISTINCTS[_distinct * i] for i in range(count)]

276 elif count <= twenty_count:

277 return _DISTINCTS[:count]

278 else:

279 return _DISTINCTS[:]

280

281

282 def contrast(color, white='#ffffff', black='#000000'):

283 """

284 Returns the colour (white or black) that contrasts best with a given

285 color.

286

287 :arg color color: color string recognied by :func:`rgba`.

288 If this color is dark, returns white. Else, black

289 :arg color white: color string, optional.

290 Replace this with any light colour you want to use instead of white

291 :arg color black: color string, optional.

292 Replace this with any dark colour you want to use instead of black

293

294 Examples::

295

296 >>> contrast('blue')

297 '#fff'

298 >>> contrast('lime')

299 '#000'

300

301 Note: `Decolorize <http://www.eyemaginary.com/Rendering/TurnColorsGray.pdf>`_

302 algorithm used. Appears to be the most preferred

303 (`ref <http://dcgi.felk.cvut.cz/home/cadikm/color_to_gray_evaluation/>`_).

304 """

305 idx = 3

306 red, green, blue = rgba(color)[:idx]

307 red_opac = 0.299

308 blue_opac = 0.114

309 green_opac = 0.582

310 cut_off = .5

311

312 luminosity = red_opac * red + green_opac * green + blue_opac * blue

313 return black if luminosity > cut_off else white

314

315

316 def brighten(color, percent):

317 """

318 Brighten or darken a color by percentage. If ``percent`` is positive,

319 brighten the color. Else darken the color.

320

321 :arg str color: color string recognied by :func:`rgba`

322 :arg float percent: -1 indicates black, +1 indicates white. Rest are interpolated.

323

324 Examples::

325

326 >>> brighten('lime', -1) # 100% darker, i.e. black

327 '#000'

328 >>> brighten('lime', -0.5) # 50% darker

329 '#007f00'

330 >>> brighten('lime', 0) # Returns the color as-is

331 '#0f0'

332 >>> brighten('lime', 0.5) # 50% brighter

333 '#7fff7f'

334 >>> brighten('lime', 1) # 100% brighter, i.e. white

335 '#fff'

336 >>> brighten('lime', 2) # Values beyond +1 or -1 are truncated

337 '#fff'

338 """

339 _min = -1

340 _mid = 0

341 _max = +1

342 black = '#0000000'

343 white = '#ffffff'

344 return gradient(percent, ((_min, black), (_mid, color), (_max, white)))

345

346

347 def msrgb(value, grad=None):

348 """

349 Returns the Microsoft

350 `RGB color <http://msdn.microsoft.com/en-in/library/dd355244.aspx>`_

351 corresponding to a given a color. This is used for Microsoft office

352 output (e.g. Excel, PowerPoint, etc.)

353

354 :arg color value: color or int/float. If ``grad`` is not specified, this

355 should be a color that will get converted into a Microsoft RGB value. If

356 ``grad`` is specified, this should be a number. It will be converted into

357 a color using the gradient, and then into a Microsoft RGB value.

358 :arg tuple grad: tuple/list of ``(value, color)`` tuples For example, ``((0,

359 'red'), (.5, 'yellow'), (1, 'green'))``. This is passed as an input to

360 :func:`gradient`

361

362 Examples::

363

364 >>> msrgb('#fff')

365 16777215

366 >>> msrgb('red')

367 255

368 >>> msrgb(0.5, RYG) # == msrgb(gradient(0.5, RYG))

369 65535

370 """

371 idx = 3

372 red, green, blue = rgba(gradient(value, grad) if grad else value)[:idx]

373 return int((blue * BASE_255 * BASE_256 + green * BASE_255) * BASE_256 + red * BASE_255)

374

375

376 def msrgbt(value, grad=None):

377 """

378 Returns the Microsoft RGB value (same as :func:`msrgbt`) and transparency

379 as a tuple.

380

381 For example::

382

383 color, transparency = msrgbt('rgba(255, 0, 0, .5)')

384 shp.Fill.ForeColor.RGB, shp.Fill.Transparency = color, transparency

385

386 See :func:`msrgbt`. The parameters are identical.

387

388 Examples::

389

390 >>> msrgbt('rgba(255,0,0,.5)')

391 (255, 0.5)

392 >>> msrgbt('red')

393 (255, 0.0)

394 """

395 _alpha = 1

396 red, green, blue, alpha = rgba(gradient(value, grad) if grad else value)

397 alpha = _alpha - alpha

398 col = (blue * BASE_255 * BASE_256 + green * BASE_255) * BASE_256 + red * BASE_255

399 return int(col), alpha

400

401

402 def rgba(color):

403 """

404 Returns red, green, blue and alpha values (as a 0-1 float) of a color.

405

406 :arg color color: a string representing the color.

407 Most color formats defined in

408 `CSS3 <http://dev.w3.org/csswg/css3-color/>`_ are allowed.

409

410 Examples::

411

412 >>> rgba('#f00')

413 (1.0, 0.0, 0.0, 1.0)

414 >>> rgba('#f003')

415 (1.0, 0.0, 0.0, 0.2)

416 >>> rgba('#ff0000')

417 (1.0, 0.0, 0.0, 1.0)

418 >>> rgba('#ff000033')

419 (1.0, 0.0, 0.0, 0.2)

420 >>> rgba('rgb(255,0,0)')

421 (1.0, 0.0, 0.0, 1.0)

422 >>> rgba('rgba(255,0,0,.2)')

423 (1.0, 0.0, 0.0, 0.2)

424 >>> rgba('red')

425 (1.0, 0.0, 0.0, 1.0)

426 >>> rgba('white')

427 (1.0, 1.0, 1.0, 1.0)

428 >>> rgba('hsl(0,1,1)')

429 (1.0, 0.0, 0.0, 1.0)

430 >>> rgba('hsla(0,1,1,.2)')

431 (1.0, 0.0, 0.0, 0.2)

432 >>> rgba('hsl(0, 100%, 50%)')

433 (0.5, 0.0, 0.0, 1.0)

434 >>> rgba('hsla(0, 100%, 100%, .9)')

435 (1.0, 0.0, 0.0, 0.9)

436 >>> rgba('hsla(360, 100%, 100%, 1.9)')

437 (1.0, 0.0, 0.0, 1.0)

438 >>> rgba('hsla(360, 0%, 50%, .5)')

439 (0.5, 0.5, 0.5, 0.5)

440 >>> rgba('hsla(0, 0%, 50%, .5)')

441 (0.5, 0.5, 0.5, 0.5)

442 """

443 result = []

444 if color.startswith('#'): 444 ↛ 466line 444 didn't jump to line 466, because the condition on line 444 was never false

445 if len(color) == BASE_9: 445 ↛ 446line 445 didn't jump to line 446, because the condition on line 445 was never true

446 result = [int(color[BASE_1: BASE_3], BASE_16) / float(BASE_255),

447 int(color[BASE_3: BASE_5], BASE_16) / float(BASE_255),

448 int(color[BASE_5: BASE_7], BASE_16) / float(BASE_255),

449 int(color[BASE_7: BASE_9], BASE_16) / float(BASE_255)]

450 elif len(color) == BASE_7: 450 ↛ 454line 450 didn't jump to line 454, because the condition on line 450 was never false

451 result = [int(color[BASE_1: BASE_3], BASE_16) / float(BASE_255),

452 int(color[BASE_3: BASE_5], BASE_16) / float(BASE_255),

453 int(color[BASE_5: BASE_7], BASE_16) / float(BASE_255)]

454 elif len(color) == BASE_5:

455 result = [int(color[BASE_1: BASE_2], BASE_16) / float(BASE_15),

456 int(color[BASE_2: BASE_3], BASE_16) / float(BASE_15),

457 int(color[BASE_3: BASE_4], BASE_16) / float(BASE_15),

458 int(color[BASE_4: BASE_5], BASE_16) / float(BASE_15)]

459 elif len(color) == BASE_4:

460 result = [int(color[BASE_1: BASE_2], BASE_16) / float(BASE_15),

461 int(color[BASE_2: BASE_3], BASE_16) / float(BASE_15),

462 int(color[BASE_3: BASE_4], BASE_16) / float(BASE_15)]

463 else:

464 result = []

465

466 elif color.startswith('rgb(') or color.startswith('rgba('):

467 for i, val in enumerate(re.findall(r'[0-9\.%]+', color.split('(')[1])):

468 if val.endswith('%'):

469 result.append(float(val[:-1]) / 100)

470 elif i < 3:

471 result.append(float(val) / float(BASE_255))

472 else:

473 result.append(float(val))

474

475 elif color.startswith('hsl(') or color.startswith('hsla('):

476 for i, val in enumerate(re.findall(r'[0-9\.%]+', color.split('(')[BASE_1])):

477 if val.endswith('%'):

478 val_idx = -1

479 div = 100

480 result.append(float(val[:val_idx]) / div)

481 elif i == BASE_0:

482 div_base = 360

483 result.append(float(val) / div_base % BASE_1)

484 else:

485 result.append(float(val))

486 result[BASE_0], result[BASE_1], result[BASE_2] = colorsys.hsv_to_rgb(

487 result[BASE_0], result[BASE_1], result[BASE_2])

488

489 elif color in _COLORNAMES:

490 result = [val / float(BASE_255) for val in _COLORNAMES[color]]

491

492 if len(result) == BASE_3: 492 ↛ 495line 492 didn't jump to line 495, because the condition on line 492 was never false

493 result.append(float(BASE_1))

494

495 if len(result) != BASE_4: 495 ↛ 496line 495 didn't jump to line 496, because the condition on line 495 was never true

496 raise ValueError('%s: invalid color' % color)

497

498 return tuple(

499 float(BASE_0) if val < BASE_0 else float(

500 BASE_1) if val > BASE_1 else val for val in result)

501

502

503 def hsla(color):

504 """

505 Returns hue, saturation, luminosity and alpha values (as a 0-1 float) for

506 a color.

507

508 :arg color color: a string representing the color.

509 Most color formats defined in

510 `CSS3 <http://dev.w3.org/csswg/css3-color/>`_ are allowed.

511

512 Examples::

513

514 >>> hsla('#f00')

515 (0.0, 1.0, 1.0, 1.0)

516 >>> hsla('#f003')

517 (0.0, 1.0, 1.0, 0.2)

518 >>> hsla('#ff0000')

519 (0.0, 1.0, 1.0, 1.0)

520 >>> hsla('#ff000033')

521 (0.0, 1.0, 1.0, 0.2)

522 >>> hsla('rgb(255,0,0)')

523 (0.0, 1.0, 1.0, 1.0)

524 >>> hsla('rgba(255,0,0,.2)')

525 (0.0, 1.0, 1.0, 0.2)

526 >>> hsla('red')

527 (0.0, 1.0, 1.0, 1.0)

528 >>> hsla('hsl(0,1,1)')

529 (0.0, 1.0, 1.0, 1.0)

530 >>> hsla('hsla(0,1,1,.2)')

531 (0.0, 1.0, 1.0, 0.2)

532 """

533 result = rgba(color)

534 return colorsys.rgb_to_hsv(

535 result[BASE_0], result[BASE_1], result[BASE_2]) + (result[BASE_3], )

536

537

538 def name(red, green, blue, alpha=1):

539 """

540 Returns a short color string

541

542 :arg float red: red color value (0-1)

543 :arg float green: green color value (0-1)

544 :arg float blue: blue color value (0-1)

545 :arg float alpha: float, optional transparency value between 0-1.

546 ``alpha=1`` produces color strings like ``#abc``

547 Lower values produce ``rgba(...)```.

548

549 Examples::

550

551 >>> name(1, 0, 0) # Short color versions preferred

552 '#f00'

553 >>> name(1, 0, 0, .2) # Alpha creates rgba() with 2 decimals

554 'rgba(255,0,0,0.20)'

555 >>> name(.5, .25, .75) # Multiply by 255 and round to nearest

556 '#8040bf'

557 >>> name(-1, 2, 0) # Values are truncated to 0-1

558 '#0f0'

559 """

560 red = int(round(

561 BASE_255 * (BASE_0 if red < BASE_0 else BASE_1 if red > BASE_1 else red),

562 BASE_0))

563

564 green = int(round(

565 BASE_255 * (BASE_0 if green < BASE_0 else BASE_1 if green > BASE_1 else green),

566 BASE_0))

567

568 blue = int(round(

569 BASE_255 * (BASE_0 if blue < BASE_0 else BASE_1 if blue > BASE_1 else blue),

570 BASE_0))

571

572 alpha = BASE_0 if alpha < BASE_0 else BASE_1 if alpha > BASE_1 else alpha

573 if alpha == BASE_1: 573 ↛ 576line 573 didn't jump to line 576, because the condition on line 573 was never false

574 return '#%02x%02x%02x' % (red, green, blue)

575 else:

576 return 'rgba(%d,%d,%d,%0.2f)' % (red, green, blue, alpha)

577

578

579 def _add_gradients(target, data):

580 """Add gradients to the module"""

581 mid_val = 0.5

582 min_val = -1.0

583 for grad, colors in six.iteritems(data['sequential']):

584 target[grad] = [[float(BASE_0), colors[BASE_0]],

585 [mid_val, colors[BASE_1]],

586 [float(BASE_1), colors[BASE_2]]]

587 for grad, colors in six.iteritems(data['divergent']):

588 target[grad] = [[min_val, colors[BASE_0]],

589 [float(BASE_0), colors[BASE_1]],

590 [float(BASE_1), colors[BASE_2]]]

591 for grad, colors in six.iteritems(data['office']):

592 target[grad] = _MSO(*colors)

593

594

595 _DATA = json.load(open(os.path.join(os.path.dirname(

596 os.path.realpath(__file__)), 'colors.json'), encoding='utf-8'))

597 _COLORNAMES = _DATA['names']

598 _add_gradients(globals(), _DATA)