""" ********** 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 """ 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