Spaces:
Runtime error
Runtime error
""" | |
********** | |
Plutchik | |
********** | |
This package contains a data visualization tool for corpora annotated with emotions. | |
Given a JSON representation of the Plutchik's emotions (or dyads) in a text or in a group of texts, | |
it draws the corresponding Plutchik's flower. | |
See Plutchik, Robert. "A general psychoevolutionary theory of emotion." Theories of emotion. Academic press, 1980. 3-33. | |
-------- | |
repository available at https://www.github.com/alfonsosemeraro/pyplutchik | |
@author: Alfonso Semeraro <alfonso.semeraro@gmail.com> | |
""" | |
import shapely.geometry as sg | |
import matplotlib.pyplot as plt | |
import descartes | |
from math import sqrt, cos, sin, radians | |
import numpy as np | |
from matplotlib import colors | |
__author__ = """Alfonso Semeraro (alfonso.semeraro@gmail.com)""" | |
__all__ = ['emo_params', | |
'dyad_params', | |
'_rotate_point', | |
'_polar_coordinates', | |
'_neutral_central_circle', | |
'_petal_shape_emotion', | |
'_petal_shape_dyad', | |
'_petal_spine_emotion', | |
'_petal_spine_dyad', | |
'_petal_circle', | |
'_draw_emotion_petal', | |
'_draw_dyad_petal', | |
'_check_scores_kind', | |
'plutchik'] | |
def emo_params(emotion): | |
""" | |
Gets color and angle for drawing a petal. | |
Color and angle depend on the emotion name. | |
Required arguments: | |
---------- | |
*emotion*: | |
Emotion's name. Possible values: | |
['joy', 'trust', 'fear', 'surprise', 'sadness', 'disgust', 'anger', 'anticipation'] | |
Returns: | |
---------- | |
*color*: | |
Matplotlib color for the petal. See: https://matplotlib.org/3.1.0/gallery/color/named_colors.html | |
*angle*: | |
Each subsequent petal is rotated 45° around the origin. | |
Notes: | |
----- | |
This function allows also 8 principal emotions, one for each Plutchik's flower petal. | |
No high or low intensity emotions are allowed (no 'ecstasy' or 'serenity', for instance). | |
""" | |
if emotion == 'joy': | |
color = 'gold' | |
angle = 0 | |
elif emotion == 'trust': | |
color = 'olivedrab' | |
angle = -45 | |
elif emotion == 'fear': | |
color = 'forestgreen' | |
angle = -90 | |
elif emotion == 'surprise': | |
color = 'skyblue' | |
angle = -135 | |
elif emotion == 'sadness': | |
color = 'dodgerblue' | |
angle = -180 | |
elif emotion == 'disgust': | |
color = 'slateblue' | |
angle = -225 | |
elif emotion == 'anger': | |
color = 'orangered' | |
angle = -270 | |
elif emotion == 'anticipation': | |
color = 'darkorange' | |
angle = -315 | |
else: | |
raise Exception("""Bad input: {} is not an accepted emotion. | |
Must be one of 'joy', 'trust', 'fear', 'surprise', 'sadness', 'disgust', 'anger', 'anticipation'""".format(emotion)) | |
return color, angle, 0 | |
def dyad_params(dyad): | |
""" | |
Gets colormap and angle for drawing a dyad. | |
Colormap and angle depend on the dyad name. | |
Required arguments: | |
---------- | |
*dyad*: | |
Dyad's name. Possible values: | |
{"primary": ['love', 'submission', 'alarm', 'disappointment', 'remorse', 'contempt', 'aggression', 'optimism'], | |
"secondary": ['guilt', 'curiosity', 'despair', '', 'envy', 'cynism', 'pride', 'fatalism'], | |
"tertiary": ['delight', 'sentimentality', 'shame', 'outrage', 'pessimism', 'morbidness', 'dominance', 'anxiety']} | |
Returns: | |
---------- | |
*colormap*: | |
Matplotlib colormap for the dyad. See: https://matplotlib.org/3.1.0/gallery/color/named_colors.html | |
*angle*: | |
Each subsequent dyad is rotated 45° around the origin. | |
""" | |
# PRIMARY DYADS | |
if dyad == 'love': | |
cmap = ['gold', 'olivedrab'] | |
angle = -45 / 2 | |
emos = ['joy', 'trust'] | |
level = 1 | |
elif dyad == 'submission': | |
cmap = ['olivedrab', 'forestgreen'] | |
angle = (-45 / 2) + (-45) | |
emos = ['trust', 'fear'] | |
level = 1 | |
elif dyad == 'alarm': | |
cmap = ['forestgreen', 'skyblue'] | |
angle = (-45 / 2) + (-90) | |
emos = ['fear', 'surprise'] | |
level = 1 | |
elif dyad == 'disappointment': | |
cmap = ['skyblue', 'dodgerblue'] | |
angle = (-45 / 2) + (-135) | |
emos = ['surprise', 'sadness'] | |
level = 1 | |
elif dyad == 'remorse': | |
cmap = ['dodgerblue', 'slateblue'] | |
angle = (-45 / 2) + (-180) | |
emos = ['sadness', 'disgust'] | |
level = 1 | |
elif dyad == 'contempt': | |
cmap = ['slateblue', 'orangered'] | |
angle = (-45 / 2) + (-225) | |
emos = ['disgust', 'anger'] | |
level = 1 | |
elif dyad == 'aggressiveness': | |
cmap = ['orangered', 'darkorange'] | |
angle = (-45 / 2) + (-270) | |
emos = ['anger', 'anticipation'] | |
level = 1 | |
elif dyad == 'optimism': | |
cmap = ['darkorange', 'gold'] | |
angle = (-45 / 2) + (-315) | |
emos = ['anticipation', 'joy'] | |
level = 1 | |
# SECONDARY DYADS | |
elif dyad == 'guilt': | |
cmap = ['gold', 'forestgreen'] | |
angle = -45 | |
emos = ['joy', 'fear'] | |
level = 2 | |
elif dyad == 'curiosity': | |
cmap = ['olivedrab', 'skyblue'] | |
angle = -90 | |
emos = ['trust', 'surprise'] | |
level = 2 | |
elif dyad == 'despair': | |
cmap = ['forestgreen', 'dodgerblue'] | |
angle = -135 | |
emos = ['fear', 'sadness'] | |
level = 2 | |
elif dyad == 'unbelief': | |
cmap = ['skyblue', 'slateblue'] | |
angle = -180 | |
emos = ['surprise', 'disgust'] | |
level = 2 | |
elif dyad == 'envy': | |
cmap = ['dodgerblue', 'orangered'] | |
angle = -225 | |
emos = ['sadness', 'anger'] | |
level = 2 | |
elif dyad == 'cynism': | |
cmap = ['slateblue', 'darkorange'] | |
angle = -270 | |
emos = ['disgust', 'anticipation'] | |
level = 2 | |
elif dyad == 'pride': | |
cmap = ['orangered', 'gold'] | |
angle = -315 | |
emos = ['anger', 'joy'] | |
level = 2 | |
elif dyad == 'hope': | |
cmap = ['darkorange', 'olivedrab'] | |
angle = 0 | |
emos = ['anticipation', 'trust'] | |
level = 2 | |
# TERTIARY DYADS | |
elif dyad == 'delight': | |
cmap = ['gold', 'skyblue'] | |
angle = (-45 / 2) + (-45) | |
emos = ['joy', 'surprise'] | |
level = 3 | |
elif dyad == 'sentimentality': | |
cmap = ['olivedrab', 'dodgerblue'] | |
angle = (-45 / 2) + (-90) | |
emos = ['trust', 'sadness'] | |
level = 3 | |
elif dyad == 'shame': | |
cmap = ['forestgreen', 'slateblue'] | |
angle = (-45 / 2) + (-135) | |
emos = ['fear', 'disgust'] | |
level = 3 | |
elif dyad == 'outrage': | |
cmap = ['skyblue', 'orangered'] | |
angle = (-45 / 2) + (-180) | |
emos = ['surprise', 'anger'] | |
level = 3 | |
elif dyad == 'pessimism': | |
cmap = ['dodgerblue', 'darkorange'] | |
angle = (-45 / 2) + (-225) | |
emos = ['sadness', 'anticipation'] | |
level = 3 | |
elif dyad == 'morbidness': | |
cmap = ['slateblue', 'gold'] | |
angle = (-45 / 2) + (-270) | |
emos = ['disgust', 'joy'] | |
level = 3 | |
elif dyad == 'dominance': | |
cmap = ['orangered', 'olivedrab'] | |
angle = (-45 / 2) + (-315) | |
emos = ['anger', 'trust'] | |
level = 3 | |
elif dyad == 'anxiety': | |
cmap = ['darkorange', 'forestgreen'] | |
angle = (-45 / 2) | |
emos = ['anticipation', 'fear'] | |
level = 3 | |
# OPPOSITES | |
elif dyad == 'bittersweetness': | |
cmap = ['gold', 'dodgerblue'] | |
angle = 0 | |
emos = ['joy', 'sadness'] | |
level = 4 | |
elif dyad == 'ambivalence': | |
cmap = ['olivedrab', 'slateblue'] | |
angle = -45 | |
emos = ['trust', 'disgust'] | |
level = 4 | |
elif dyad == 'frozenness': | |
cmap = ['forestgreen', 'orangered'] | |
angle = -90 | |
emos = ['fear', 'anger'] | |
level = 4 | |
elif dyad == 'confusion': | |
cmap = ['skyblue', 'darkorange'] | |
angle = -135 | |
emos = ['surprise', 'anticipation'] | |
level = 4 | |
else: | |
raise Exception("""Bad input: '{}' is not an accepted name for a dyad. | |
Must be one of: | |
'love', 'submission', 'alarm', 'disappointment', 'remorse', 'contempt', 'aggressiveness', 'optimism', | |
'guilt', 'curiosity', 'despair', 'unbelief', 'envy', 'cynism', 'pride', 'hope', | |
'delight', 'sentimentality', 'shame', 'outrage', 'pessimism', 'morbidness', 'dominance', 'anxiety', | |
'bittersweetness', 'ambivalence', 'frozenness', 'confusion' | |
""".format(dyad)) | |
return emos, cmap, angle, level | |
def _rotate_point(point, angle): | |
""" | |
Rotate a point counterclockwise by a given angle around a given origin. | |
Required arguments: | |
---------- | |
*point*: | |
A two-values tuple, (x, y), of the point to rotate | |
*angle*: | |
The angle the point is rotated. The angle should be given in radians. | |
Returns: | |
---------- | |
*(qx, qy)*: | |
A two-values tuple, the new coordinates of the rotated point. | |
""" | |
ox, oy = 0, 0 | |
px, py = point | |
angle = radians(angle) | |
qx = ox + cos(angle) * (px - ox) - sin(angle) * (py - oy) | |
qy = oy + sin(angle) * (px - ox) + cos(angle) * (py - oy) | |
return (qx, qy) | |
def _polar_coordinates(ax, font, fontweight, fontsize, show_ticklabels, ticklabels_angle, ticklabels_size, offset = .15): | |
""" | |
Draws polar coordinates as a background. | |
Required arguments: | |
---------- | |
*ax*: | |
Axes to draw the coordinates. | |
*font*: | |
Font of text. Default is Montserrat. | |
*fontweight*: | |
Font weight of text. Default is light. | |
*fontsize*: | |
Font size of text. Default is 15. | |
*show_ticklabels*: | |
Boolean, wether to show tick labels under Joy petal. Default is False. | |
*ticklabels_angle*: | |
How much to rotate tick labels from y=0. Value should be given in radians. Default is 0. | |
*ticklabels_size*: | |
Size of tick labels. Default is 11. | |
*offset*: | |
Central neutral circle has radius = .15, and coordinates must start from there. | |
Returns: | |
---------- | |
*ax*: | |
The input Axes modified. | |
""" | |
# Lines | |
for i in range(0, 110, 20): | |
c = plt.Circle((0, 0), offset + i/100, color = 'grey', alpha = .3, fill = False, zorder = -20) | |
ax.add_artist(c) | |
# Tick labels | |
if show_ticklabels: | |
for x in np.arange(0.2, 1.2, .2): | |
a = round(x, 1) | |
x, y = _rotate_point((0, a + offset), ticklabels_angle) #-.12 | |
ax.annotate(s = str(a), xy = (x, y), fontfamily = font, size = ticklabels_size, fontweight = fontweight, zorder = 8, rotation = ticklabels_angle) | |
return ax | |
def _neutral_central_circle(ax, r = .15): | |
""" | |
Draws central neutral circle (in grey). | |
Required arguments: | |
---------- | |
*ax*: | |
Axes to draw the coordinates. | |
*r*: | |
Radius of the circle. Default is .15. | |
Returns: | |
---------- | |
*ax*: | |
The input Axes modified. | |
""" | |
c = sg.Point(0, 0).buffer(r) | |
ax.add_patch(descartes.PolygonPatch(c, fc='white', ec=(.5, .5, .5, .3), alpha=1, zorder = 15)) | |
return ax | |
def _outer_border(ax, emotion_score, color, angle, highlight, offset = .15, height_width_ratio = 1, normalize = False): | |
""" | |
Draw a the outer border of a petal. | |
Required arguments: | |
---------- | |
*ax*: | |
Axes to draw the coordinates. | |
*emotion_score*: | |
Score of the emotion. Values range from 0 to 1. | |
*color*: | |
Color of the petal. See emo_params(). | |
*angle*: | |
Rotation angle of the petal. See emo_params(). | |
*highlight*: | |
String. 'opaque' if the petal must be shadowed, 'regular' is default. | |
*offset*: | |
Central neutral circle has radius = .15, and petals must start from there. | |
*height_width_ratio*: | |
Ratio between height and width of the petal. Lower the ratio, thicker the petal. Default is 1. | |
*normalize*: | |
Either False or the highest value among emotions. If not False, must normalize all petal lengths. | |
""" | |
if normalize: | |
emotion_score /= normalize | |
# Computing proportions. | |
h = 1*emotion_score + offset | |
x = height_width_ratio*emotion_score | |
y = h/2 | |
r = sqrt(x**2 + y**2) | |
# Computing rotated centers | |
x_right, y_right = _rotate_point((x, y), angle) | |
x_left, y_left = _rotate_point((-x, y), angle) | |
# Circles and intersection | |
right = sg.Point(x_right, y_right).buffer(r) | |
left = sg.Point(x_left, y_left).buffer(r) | |
petal = right.intersection(left) | |
# alpha and color | |
alpha = 1 if highlight == 'regular' else .8 | |
ecol = (colors.to_rgba(color)[0], colors.to_rgba(color)[1], colors.to_rgba(color)[2], alpha) | |
ax.add_patch(descartes.PolygonPatch(petal, fc=(0, 0, 0, 0), ec = ecol, lw= 1)) | |
def _petal_shape_emotion(ax, emotion_score, color, angle, font, fontweight, fontsize, highlight, will_circle, offset = .15, height_width_ratio = 1, normalize = False): | |
""" | |
Draw a petal. | |
A petal is the intersection area between two circles. | |
The height of the petal depends on the radius and the center of the circles. | |
Full details at http://www.github.com/alfonsosemeraro/plutchik/tutorial.ipynb | |
Required arguments: | |
---------- | |
*ax*: | |
Axes to draw the coordinates. | |
*emotion_score*: | |
Score of the emotion. Values range from 0 to 1. | |
*color*: | |
Color of the petal. See emo_params(). | |
*angle*: | |
Rotation angle of the petal. See emo_params(). | |
*font*: | |
Font of text. Default is Montserrat. | |
*fontweight*: | |
Font weight of text. Default is light. | |
*fontsize*: | |
Font size of text. Default is 15. | |
*highlight*: | |
String. 'opaque' if the petal must be shadowed, 'regular' is default. | |
*will_circle*: | |
Boolean. If three intensities will be plotted, then the lower petal must be pale. | |
*offset*: | |
Central neutral circle has radius = .15, and petals must start from there. | |
*height_width_ratio*: | |
Ratio between height and width of the petal. Lower the ratio, thicker the petal. Default is 1. | |
*normalize*: | |
Either False or the highest value among emotions. If not False, must normalize all petal lengths. | |
Returns: | |
---------- | |
*petal*: | |
The petal, a shapely shape. | |
""" | |
if normalize: | |
emotion_score /= normalize | |
# Computing proportions. | |
h = 1*emotion_score + offset | |
x = height_width_ratio*emotion_score | |
y = h/2 | |
r = sqrt(x**2 + y**2) | |
# Computing rotated centers | |
x_right, y_right = _rotate_point((x, y), angle) | |
x_left, y_left = _rotate_point((-x, y), angle) | |
# Circles and intersection | |
right = sg.Point(x_right, y_right).buffer(r) | |
left = sg.Point(x_left, y_left).buffer(r) | |
petal = right.intersection(left) | |
# Alpha for highlighting | |
if highlight == 'regular': | |
if will_circle: | |
alpha = .3 | |
else: | |
alpha = .5 | |
elif will_circle: | |
alpha = .0 | |
else: | |
alpha = .0 | |
ax.add_patch(descartes.PolygonPatch(petal, fc='white', lw = 0, alpha=1, zorder = 0)) | |
ax.add_patch(descartes.PolygonPatch(petal, fc=color, lw= 0, alpha=alpha, zorder = 10)) | |
return petal | |
def _petal_shape_dyad(ax, emotion_score, colorA, colorB, angle, font, fontweight, fontsize, highlight, will_circle, offset = .15, height_width_ratio = 1, normalize = False): | |
""" | |
Draw a petal. | |
A petal is the intersection area between two circles. | |
The height of the petal depends on the radius and the center of the circles. | |
Full details at http://www.github.com/alfonsosemeraro/plutchik/tutorial.ipynb | |
Required arguments: | |
---------- | |
*ax*: | |
Axes to draw the coordinates. | |
*emotion_score*: | |
Score of the emotion. Values range from 0 to 1. | |
*colorA*: | |
First color of the petal. See dyad_params(). | |
*colorB*: | |
Second color of the petal. See dyad_params(). | |
*angle*: | |
Rotation angle of the petal. See dyad_params(). | |
*font*: | |
Font of text. Default is Montserrat. | |
*fontweight*: | |
Font weight of text. Default is light. | |
*fontsize*: | |
Font size of text. Default is 15. | |
*highlight*: | |
String. 'opaque' if the petal must be shadowed, 'regular' is default. | |
*will_circle*: | |
Boolean. If three intensities will be plotted, then the lower petal must be pale. | |
*offset*: | |
Central neutral circle has radius = .15, and petals must start from there. | |
*height_width_ratio*: | |
Ratio between height and width of the petal. Lower the ratio, thicker the petal. Default is 1. | |
*normalize*: | |
Either False or the highest value among emotions. If not False, must normalize all petal lengths. | |
Returns: | |
---------- | |
*petal*: | |
The petal, a shapely shape. | |
""" | |
if emotion_score == 0: | |
return ax | |
if normalize: | |
emotion_score /= normalize | |
# Computing proportions. | |
h = 1*emotion_score + offset | |
x = height_width_ratio*emotion_score | |
y = h/2 | |
r = sqrt(x**2 + y**2) | |
# Computing rotated centers | |
x_right, y_right = _rotate_point((x, y), angle) | |
x_left, y_left = _rotate_point((-x, y), angle) | |
# Circles and intersection | |
right = sg.Point(x_right, y_right).buffer(r) | |
left = sg.Point(x_left, y_left).buffer(r) | |
petal = right.intersection(left) | |
# Computing squares: left | |
A = _rotate_point((-2*x, 0), angle) | |
B = _rotate_point((-2*x, h), angle) | |
C = _rotate_point((0, h), angle) | |
D = _rotate_point((0, 0), angle) | |
square_left = sg.Polygon([A, B, C, D, A]) | |
# Computing squares: right | |
A = _rotate_point((0, 0), angle) | |
B = _rotate_point((0, h), angle) | |
C = _rotate_point((2*x, h), angle) | |
D = _rotate_point((2*x, 0), angle) | |
square_right = sg.Polygon([A, B, C, D, A]) | |
# Computing semipetals | |
petalA = petal.intersection(square_left) | |
petalB = petal.intersection(square_right) | |
# white petal underneath | |
ax.add_patch(descartes.PolygonPatch(petal, fc='white', lw = 0, alpha=1, zorder = 0)) | |
# Draw each half-petal in alpha 0.7 | |
alpha = .7 | |
xs, ys = petalA.exterior.xy | |
ax.fill(xs, ys, alpha=alpha, fc= colorA, ec='none') | |
xs, ys = petalB.exterior.xy | |
ax.fill(xs, ys, alpha=alpha, fc=colorB, ec='none') | |
return ax | |
def _petal_spine_emotion(ax, emotion, emotion_score, color, angle, font, fontweight, fontsize, highlight = 'all', offset = .15): | |
""" | |
Draw the spine beneath a petal, and the annotation of emotion and emotion's value. | |
The spine is a straight line from the center, of length 1.03. | |
Full details at http://www.github.com/alfonsosemeraro/plutchik/tutorial.ipynb | |
Required arguments: | |
---------- | |
*ax*: | |
Axes to draw the coordinates. | |
*emotion*: | |
Emotion's name. | |
*emotion_score*: | |
Score of the emotion. Values range from 0 to 1. if list, it must contain 3 values that sum up to 1. | |
*color*: | |
Color of the petal. See emo_params(). | |
*angle*: | |
Rotation angle of the petal. See emo_params(). | |
*font*: | |
Font of text. Default is Montserrat. | |
*fontweight*: | |
Font weight of text. Default is light. | |
*fontsize*: | |
Font size of text. Default is 15. | |
*highlight*: | |
String. 'opaque' if the petal must be shadowed, 'regular' is default. | |
*offset*: | |
Central neutral circle has radius = .15, and petals must start from there. | |
""" | |
# Diagonal lines and ticks | |
step = .03 | |
p1 = (0, 0) | |
p2 = _rotate_point((0, 1 + step + offset), angle) # draw line until 0, 1 + step + offset | |
p3 = _rotate_point((-step, 1 + step + offset), angle) # draw tick | |
ax.plot([p1[0], p2[0]], [p1[1], p2[1]], zorder = 5, color = 'black', alpha = .3, linewidth = .75) | |
ax.plot([p2[0], p3[0]], [p2[1], p3[1]], zorder = 5, color = 'black', alpha = .3, linewidth = .75) | |
# Managing highlighting and transparency | |
if highlight == 'opaque': | |
alpha = .8 | |
color = 'lightgrey' | |
else: | |
alpha = 1 | |
# Checking if iterable | |
try: | |
_ = emotion_score[0] | |
iterable = True | |
except: | |
iterable = False | |
if iterable: | |
# Label | |
angle2 = angle + 180 if -110 > angle > -260 else angle | |
p4 = _rotate_point((0, 1.40 + step + offset), angle) | |
ax.annotate(s = emotion, xy = p4, rotation = angle2, ha='center', va = 'center', | |
fontfamily = font, size = fontsize, fontweight = fontweight) | |
# Score 1 | |
p5 = _rotate_point((0, 1.07 + step + offset), angle) | |
ax.annotate(s = "{0:.2f}".format(round(emotion_score[0],2)), xy = p5, rotation = angle2, ha='center', va = 'center', | |
color = color, fontfamily = font, size = fontsize, fontweight = 'regular', alpha = alpha) | |
# Score 2 | |
p6 = _rotate_point((0, 1.17 + step + offset), angle) | |
ax.annotate(s = "{0:.2f}".format(round(emotion_score[1],2)), xy = p6, rotation = angle2, ha='center', va = 'center', | |
color = color, fontfamily = font, size = fontsize, fontweight = 'demibold', alpha = alpha) | |
# Score 3 | |
p7 = _rotate_point((0, 1.27 + step + offset), angle) | |
ax.annotate(s = "{0:.2f}".format(round(emotion_score[2],2)), xy = p7, rotation = angle2, ha='center', va = 'center', | |
color = color, fontfamily = font, size = fontsize, fontweight = 'regular', alpha = alpha) | |
else: | |
# Label | |
angle2 = angle + 180 if -110 > angle > -260 else angle | |
p4 = _rotate_point((0, 1.23 + step + offset), angle) | |
ax.annotate(s = emotion, xy = p4, rotation = angle2, ha='center', va = 'center', | |
fontfamily = font, size = fontsize, fontweight = fontweight) | |
# Score | |
p5 = _rotate_point((0, 1.1 + step + offset), angle) | |
ax.annotate(s = "{0:.2f}".format(round(emotion_score,2)), xy = p5, rotation = angle2, ha='center', va = 'center', | |
color = color, fontfamily = font, size = fontsize, fontweight = 'demibold', alpha = alpha) | |
def _petal_spine_dyad(ax, dyad, dyad_score, color, emotion_names, angle, font, fontweight, fontsize, highlight = 'all', offset = .15): | |
""" | |
Draw the spine beneath a petal, and the annotation of dyad and dyad's value. | |
The spine is a straight line from the center, of length 1.03. | |
Full details at http://www.github.com/alfonsosemeraro/plutchik/tutorial.ipynb | |
Required arguments: | |
---------- | |
*ax*: | |
Axes to draw the coordinates. | |
*dyad*: | |
Dyad's name. | |
*dyad_score*: | |
Score of the dyad. Values range from 0 to 1. if list, it must contain 3 values that sum up to 1. | |
*color*: | |
Color of the two emotions of the dyad. See dyad_params(). | |
*emotion_names*: | |
Name of the emotions the dyad is made of. See dyad_params(). | |
*angle*: | |
Rotation angle of the petal. See dyad_params(). | |
*font*: | |
Font of text. Default is Montserrat. | |
*fontweight*: | |
Font weight of text. Default is light. | |
*fontsize*: | |
Font size of text. Default is 15. | |
*highlight*: | |
String. 'opaque' if the petal must be shadowed, 'regular' is default. | |
*offset*: | |
Central neutral circle has radius = .15, and petals must start from there. | |
""" | |
# Diagonal lines and ticks | |
step = .03 | |
p1 = (0, 0) # 0, 0 + offset | |
p2 = _rotate_point((0, 1 + step + offset), angle) # draw line until 0, 1 + step + offset | |
p3 = _rotate_point((-step, 1 + step + offset), angle) # draw tick | |
ax.plot([p1[0], p2[0]], [p1[1], p2[1]], zorder = 5, color = 'black', alpha = .3, linewidth = .75) | |
ax.plot([p2[0], p3[0]], [p2[1], p3[1]], zorder = 5, color = 'black', alpha = .3, linewidth = .75) | |
# Managing highlighting and opacity | |
if highlight == 'opaque': | |
alpha = .8 | |
color = 'lightgrey' | |
else: | |
alpha = 1 | |
## Drawing the two-colored circular arc over dyads | |
from matplotlib.patches import Arc | |
H = 3.2 | |
pac1 = Arc((0, 0), width = H, height = H, angle = 90, theta2 = angle, theta1 = angle - 18, ec = color[1], linewidth = 3) | |
pac2 = Arc((0, 0), width = H, height = H, angle = 90, theta2 = angle + 18, theta1 = angle, ec = color[0], linewidth = 3) | |
ax.add_patch(pac1) | |
ax.add_patch(pac2) | |
# Labels over the arcs | |
angle2 = angle + 180 if -110 > angle > -260 else angle | |
p9 = _rotate_point((0, 1.7), angle - 9) | |
ax.annotate(s = emotion_names[1], xy = p9, rotation = angle2 - 8, ha='center', va = 'center', zorder = 30, | |
fontfamily = font, size = fontsize * .7, fontweight = 'demibold', color = color[1]) | |
p10 = _rotate_point((0, 1.7), angle + 9) | |
ax.annotate(s = emotion_names[0], xy = p10, rotation = angle2 + 8, ha='center', va = 'center', zorder = 30, | |
fontfamily = font, size = fontsize * .7, fontweight = 'demibold', color = color[0]) | |
# Dyad label must be grey | |
color = '#363636' | |
# Label | |
angle2 = angle + 180 if -110 > angle > -260 else angle | |
p4 = _rotate_point((0, 1.23 + step + offset), angle) | |
ax.annotate(s = dyad, xy = p4, rotation = angle2, ha='center', va = 'center', | |
fontfamily = font, size = fontsize, fontweight = fontweight) | |
# Score | |
p5 = _rotate_point((0, 1.1 + step + offset), angle) | |
ax.annotate(s = "{0:.2f}".format(round(dyad_score,2)), xy = p5, rotation = angle2, ha='center', va = 'center', | |
color = color, fontfamily = font, size = fontsize, fontweight = 'demibold', alpha = alpha) | |
def _petal_circle(ax, petal, radius, color, inner = False, highlight = 'none', offset = .15, normalize = False): | |
""" | |
Each petal may have 3 degrees of intensity. | |
Each of the three sections of a petal is the interception between | |
the petal and up to two concentric circles from the origin. | |
This function draws one section. | |
Full details at http://www.github.com/alfonsosemeraro/plutchik/tutorial.ipynb | |
Required arguments: | |
---------- | |
*ax*: | |
Axes to draw the coordinates. | |
*petal*: | |
The petal shape. See petal(). | |
*radius*: | |
Radius of the section. | |
*color*: | |
Color of the section. See emo_params(). | |
*inner*: | |
Boolean. If True, a second patch is drawn with alpha = 0.3, making the inner circle darker. | |
*highlight*: | |
String. 'opaque' if the petal must be shadowed, 'regular' is default. | |
*offset*: | |
Central neutral circle has radius = .15, and petals must start from there. | |
*normalize*: | |
Either False or the highest value among emotions. If not False, must normalize all petal lengths. | |
""" | |
if radius: | |
if normalize: | |
radius /= normalize | |
# Define the intersection between circle c and petal | |
c = sg.Point(0, 0).buffer(radius + offset) | |
area = petal.intersection(c) | |
# Managing alpha and color | |
alpha0 = 1 if highlight == 'regular' else .2 | |
ecol = (colors.to_rgba(color)[0], colors.to_rgba(color)[1], colors.to_rgba(color)[2], alpha0) | |
alpha1 = .5 if highlight == 'regular' else .0 | |
# Drawing separately the shape and a thicker border | |
ax.add_patch(descartes.PolygonPatch(area, fc=color, ec = 'black', lw = 0, alpha=alpha1)) | |
ax.add_patch(descartes.PolygonPatch(area, fc=(0, 0, 0, 0), ec = ecol, lw = 1.3)) | |
# The innermost circle gets to be brighter because of the repeated overlap | |
# Its alpha is diminished to avoid too much bright colors | |
if inner: | |
alpha2 = .3 if highlight == 'regular' else .0 | |
ax.add_patch(descartes.PolygonPatch(area, fc=color, ec = 'w', lw = 0, alpha=alpha2)) | |
ax.add_patch(descartes.PolygonPatch(area, fc=(0, 0, 0, 0), ec = ecol, lw = 1.5)) | |
def _draw_emotion_petal(ax, emotion, emotion_score, highlight_emotions, show_intensity_labels, font, fontweight, fontsize, show_coordinates, height_width_ratio, normalize = False): | |
""" | |
Draw the petal and its possible sections. | |
Full details at http://www.github.com/alfonsosemeraro/plutchik/tutorial.ipynb | |
Required arguments: | |
---------- | |
*ax*: | |
Axes to draw the coordinates. | |
*emotion*: | |
Emotion's name. | |
*emotion_score*: | |
Score of the emotion. Values range from 0 to 1. | |
*highlight_emotions*: | |
A list of main emotions to highlight. Other emotions will be shadowed. | |
*show_intensity_labels*: | |
A string or a list of main emotions. It shows all three intensity scores for each emotion in the list, and for the others cumulative scores. Default is 'none'. | |
*font*: | |
Font of text. Default is Montserrat. | |
*fontweight*: | |
Font weight of text. Default is light. | |
*fontsize*: | |
Font size of text. Default is 15. | |
*offset*: | |
Central neutral circle has radius = .15, and petals must start from there. | |
*show_coordinates*: | |
A boolean, wether to show polar coordinates or not. | |
*normalize*: | |
Either False or the highest value among emotions. If not False, normalize petal length. | |
""" | |
color, angle, _ = emo_params(emotion) | |
# Check if iterable | |
try: | |
_ = emotion_score[0] | |
iterable = True | |
except: | |
iterable = False | |
# Manage highlight and opacity | |
if highlight_emotions != 'all': | |
if emotion in highlight_emotions: | |
highlight = 'regular' | |
else: | |
highlight = 'opaque' | |
else: | |
highlight = 'regular' | |
if not iterable: | |
if show_coordinates: | |
# Draw the line and tick behind a petal | |
_petal_spine_emotion(ax = ax, emotion = emotion, emotion_score = emotion_score, | |
color = color, angle = angle, | |
font = font, fontweight = fontweight, fontsize = fontsize, | |
highlight = highlight, | |
offset = .15) | |
# Draw petal | |
_petal_shape_emotion(ax, emotion_score, color, angle, font, fontweight, fontsize, height_width_ratio = height_width_ratio, highlight = highlight, will_circle = False, normalize = normalize) | |
# Draw border | |
_outer_border(ax, emotion_score, color, angle, height_width_ratio = height_width_ratio, highlight = highlight, normalize = normalize) | |
else: | |
# Total length is the sum of the emotion score | |
a, b, c = emotion_score | |
length = a + b + c | |
# Show three scores or just the cumulative one? | |
label = emotion_score if ((show_intensity_labels == 'all') or (emotion in show_intensity_labels)) else length | |
if show_coordinates: | |
# Draw the line and tick behind a petal | |
_petal_spine_emotion(ax = ax, emotion = emotion, emotion_score = label, | |
color = color, angle = angle, | |
font = font, fontweight = fontweight, fontsize = fontsize, | |
highlight = highlight, | |
offset = .15) | |
# Draw petal | |
petal_shape = _petal_shape_emotion(ax, length, color, angle, font, fontweight, fontsize, height_width_ratio = height_width_ratio, highlight = highlight, will_circle = True, normalize = normalize) | |
# Draw inner petal section | |
_petal_circle(ax, petal_shape, a + b, color, False, highlight, normalize = normalize) | |
# Draw middle petal section | |
_petal_circle(ax, petal_shape, a, color, True, highlight, normalize = normalize) | |
# Draw border | |
_outer_border(ax, length, color, angle, height_width_ratio = height_width_ratio, highlight = highlight, normalize = normalize) | |
def _draw_dyad_petal(ax, dyad, dyad_score, font, fontweight, fontsize, show_coordinates, height_width_ratio, offset = .15, normalize = False): | |
""" | |
Draw the petal and its possible sections. | |
Full details at http://www.github.com/alfonsosemeraro/plutchik/tutorial.ipynb | |
Required arguments: | |
---------- | |
*ax*: | |
Axes to draw the coordinates. | |
*dyad*: | |
Dyad's name. | |
*dyad_score*: | |
Score of the dyad. Values range from 0 to 1. | |
*font*: | |
Font of text. Default is Montserrat. | |
*fontweight*: | |
Font weight of text. Default is light. | |
*fontsize*: | |
Font size of text. Default is 15. | |
*offset*: | |
Central neutral circle has radius = .15, and petals must start from there. | |
*show_coordinates*: | |
A boolean, wether to show polar coordinates or not. | |
*normalize*: | |
Either False or the highest value among the dyads. If not False, normalize petal length. | |
""" | |
emos, color, angle, _ = dyad_params(dyad) | |
colorA, colorB = color | |
if show_coordinates: | |
# Draw the line and tick behind a petal | |
_petal_spine_dyad(ax = ax, dyad = dyad, dyad_score = dyad_score, | |
emotion_names = emos, | |
color = color, angle = angle, | |
font = font, fontweight = fontweight, fontsize = fontsize, | |
highlight = 'all', | |
offset = .15) | |
# Draw petal (and get the modified ax) | |
ax = _petal_shape_dyad(ax, dyad_score, colorA, colorB, angle, font, fontweight, fontsize, height_width_ratio = height_width_ratio, highlight = 'all', will_circle = False, normalize = normalize) | |
# Draw border | |
_outer_border(ax, dyad_score, colorA, angle, height_width_ratio = height_width_ratio, highlight = 'all', normalize = normalize) | |
def _check_scores_kind(tags): | |
""" | |
Checks if the inputed scores are all of the same kind | |
(emotions or primary dyads or secondary dyads or tertiary dyads or opposites). | |
No mixed kinds are allowed. | |
Required arguments: | |
---------- | |
*tags*: | |
List of the tags provided as 'scores'. | |
Returns: | |
---------- | |
A boolean, True if `scores` contains emotions, False if it contains dyads. | |
""" | |
kinds = [] | |
for t in tags: | |
try: | |
kinds += [emo_params(t)[2]] | |
except: | |
kinds += [dyad_params(t)[3]] | |
unique_kinds = list(set(sorted(kinds))) | |
if len(unique_kinds) > 1: | |
unique_kinds_str = ', '.join([str(a) for a in unique_kinds]) | |
unique_kinds_str = unique_kinds_str.replace('0', 'emotions') | |
unique_kinds_str = unique_kinds_str.replace('1', 'primary dyads') | |
unique_kinds_str = unique_kinds_str.replace('2', 'secondary dyads') | |
unique_kinds_str = unique_kinds_str.replace('3', 'tertiary dyads') | |
unique_kinds_str = unique_kinds_str.replace('4', 'opposite emotions') | |
unique_kinds_str = ' and'.join(unique_kinds_str.rsplit(',', 1)) | |
error_str = "Bad input: can't draw {} altogether. Please input only one of them as 'scores'.".format(unique_kinds_str) | |
raise Exception(error_str) | |
else: | |
kind = kinds[0] | |
if kind == 0: | |
return True | |
else: | |
return False | |
def random_flower(): | |
""" Draws a Plutchik's flower with random scores """ | |
import random | |
emo = {'joy': random.uniform(0, 1), | |
'trust': random.uniform(0,1), | |
'fear': random.uniform(0,1), | |
'surprise': random.uniform(0,1), | |
'sadness': random.uniform(0,1), | |
'disgust': random.uniform(0,1), | |
'anger': random.uniform(0,1), | |
'anticipation': random.uniform(0,1)} | |
plutchik(emo) | |
def plutchik(scores, | |
ax = None, | |
font = None, | |
fontweight = 'light', | |
fontsize = 15, | |
show_coordinates = True, | |
show_ticklabels = False, | |
highlight_emotions = 'all', | |
show_intensity_labels = 'none', | |
ticklabels_angle = 0, | |
ticklabels_size = 11, | |
height_width_ratio = 1, | |
title = None, | |
title_size = None, | |
normalize = False): | |
""" | |
Draw the petal and its possible sections. | |
Full details at http://www.github.com/alfonsosemeraro/plutchik/tutorial.ipynb | |
Required arguments: | |
---------- | |
*scores*: | |
A dictionary with emotions or dyads. | |
For each entry, values accepted are a 3-values iterable (for emotions only) or a scalar value between 0 and 1. | |
The sum of the 3-values iterable values must not exceed 1, and no value should be negative. | |
See emo_params() and dyad_params() for accepted keys. | |
Emotions and dyads are mutually exclusive. Different kinds of dyads are mutually exclusive. | |
*ax*: | |
Axes to draw the coordinates. | |
*font*: | |
Font of text. Default is sans-serif. | |
*fontweight*: | |
Font weight of text. Default is light. | |
*fontsize*: | |
Font size of text. Default is 15. | |
*offset*: | |
Central neutral circle has radius = .15, and petals must start from there. | |
*show_coordinates*: | |
A boolean, wether to show polar coordinates or not. | |
*show_ticklabels*: | |
Boolean, wether to show tick labels under Joy petal. Default is False. | |
*highlight_emotions*: | |
A string or a list of main emotions to highlight. If a list of emotions is given, other emotions will be shadowed. Default is 'all'. | |
*show_intensity_labels*: | |
A string or a list of main emotions. It shows all three intensity scores for each emotion in the list, and for the others cumulative scores. Default is 'none'. | |
*ticklabels_angle*: | |
How much to rotate tick labels from y=0. Value should be given in radians. Default is 0. | |
*ticklabels_size*: | |
Size of tick labels. Default is 11. | |
*height_width_ratio*: | |
Ratio between height and width of the petal. Lower the ratio, thicker the petal. Default is 1. | |
*title*: | |
Title for the plot. | |
*title_size*: | |
Size of the title. Default is font_size. | |
Returns: | |
---------- | |
*ax*: | |
The input Axes modified. | |
""" | |
scores = {key.lower(): val for key, val in scores.items()} | |
# Check if dyads or emotions, and what kind of dyads | |
score_is_emotions = _check_scores_kind(scores) | |
if score_is_emotions: | |
emotions, dyads = scores, None | |
else: | |
emotions, dyads = None, scores | |
# Create subplot if is not provided as parameter | |
if not ax: | |
fig, ax = plt.subplots(figsize = (8, 8)) | |
# Managing fonts | |
if not font: | |
font = 'sans-serif' | |
# Draw coordinates (if needed) before any petal | |
if show_coordinates: | |
_polar_coordinates(ax, font, fontweight, fontsize, show_ticklabels, ticklabels_angle, ticklabels_size) | |
# Draw inner white circle | |
_neutral_central_circle(ax) | |
# Emotions and dyads are mutually exclusive | |
if emotions: | |
emotions = {key.lower(): val for key, val in emotions.items()} | |
for emo in emotions: | |
# Check correctedness of values | |
if hasattr(emotions[emo], '__iter__'): | |
if sum(emotions[emo]) > 1 or any([e < 0 for e in emotions[emo]]): | |
raise Exception("Bad input for `{}`. Emotion scores array should be between 0 and 1.".format(emo)) | |
else: | |
if emotions[emo] > 1 or emotions[emo] < 0: | |
raise Exception("Bad input for `{}`. Emotion scores array should sum to between 0 and 1.".format(emo)) | |
# Draw emotion petal | |
_draw_emotion_petal(ax, emotion_score = emotions[emo], emotion = emo, | |
font = font, fontweight = fontweight, fontsize = fontsize, | |
highlight_emotions = highlight_emotions, show_intensity_labels = show_intensity_labels, | |
show_coordinates = show_coordinates, height_width_ratio = height_width_ratio, normalize = normalize) | |
elif dyads: | |
for dyad in dyads: | |
# Check correctedness of values | |
if dyads[dyad] > 1 or dyads[dyad] < 0: | |
print("Alert: {} = {}".format(dyad, dyads[dyad])) | |
raise Exception("Bad input for `{}`. Dyads scores array should sum to between 0 and 1.".format(dyad)) | |
# Draw dyad bicolor petal | |
_draw_dyad_petal(ax, dyad_score = dyads[dyad], dyad = dyad, | |
font = font, fontweight = fontweight, fontsize = fontsize, | |
show_coordinates = show_coordinates, height_width_ratio = height_width_ratio, | |
normalize = normalize) | |
# Annotation inside the circle | |
_, _, _, level = dyad_params(list(dyads.keys())[0]) # get the first dyad level (they all are the same) | |
ll = level if level != 4 else 'opp.' # what to annotate | |
xy = (-0.03, -0.03) if level != 4 else (-0.13, -0.03) # exact center of '1' or 'opp' is slightly different | |
ax.annotate(s = ll, xy = xy, fontsize = fontsize, fontfamily = font, fontweight = 'bold', zorder = 30) | |
# Ghost dotted track that connects colored arcs | |
c = plt.Circle((0, 0), 1.60, color = 'grey', alpha = .3, fill = False, zorder = -20, linestyle = 'dotted' ) | |
ax.add_artist(c) | |
# Adjusting printable area size | |
lim = 1.6 if show_coordinates else 1.2 | |
lim = lim + 0.1 if dyads else lim | |
ax.set_xlim((-lim, lim)) | |
ax.set_ylim((-lim, lim)) | |
# Default is no axis | |
ax.axis('off') | |
# Title and title size | |
if not title_size: | |
title_size = fontsize | |
if title: | |
ax.set_title(title, fontfamily = font, fontsize = title_size, fontweight = 'bold', pad = 20) | |
return ax |