Radial Bar Chart with Pending Pipeline

Radial bar charts are a great way to visualize progress charts - but sometimes it's helpful to be able to visualize that progress with the additional context of more pipeline as well.

 

Input: you will need to provide the chart maker function with at least two values (progress and goal - you will just need to edit the very bottom section of this code block). You can also specify a pipeline value if you have one.

current = float(101) #float(df.iloc[0,0]
goal = float(100) #float(df.iloc[0,1]

# this is an optional parameter if you do have pipeline
pipeline = float(30) #float(df.iloc[0,1]

Output: a single ring that denotes progress towards goal (optionally with pipeline as well). Anything past 100% will result in a color change indicating the goal has been reached. 

Full Code Block:

# Periscope automatically imports the output from the SQL editor above into the dataframe when you add Python analysis.
# Filters applied in the SQL workflow above will update Python based analysis when applied.

#######################################################################
###                                                        LIBRARIES                                                            ###
#######################################################################
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

#######################################################################
###                                                    HELPER FUNCTIONS                                                ###
#######################################################################

#USE: Create an array structure for rings.
#INPUT: a df of row length 1 with the first column as the current metric value and the second colum is the target metric value
#OUTPUT: an aray of arrays representing each ring
def calculate_rings(first, second, pending):
  if first < second and first + pending <= second:
    rings=[[first, pending, second-first - pending]]
  elif first < second and first + pending > second:
    rings=[[first, second - first , 0]]
  else:
    rings = [[1, 0, 0]]
  return rings

#USE: Determine if the label for the rotating number label should be left/center/right
#INPUT: a df of row length 1 with the first column as the current metric value and the second colum is the target metric value
#OUTPUT: the proper text alignment
def horizontal_aligner(first, second):
  metric = 1.0 * first % second / second
  if metric in (0, 0.5):
    align = 'center'
  elif metric < 0.5:
    align = 'left'
  else:
    align = 'right'
  return align

def vertical_aligner(first, second):
  metric = 1.0 * first % second / second
  if metric < 0.25:
    align = 'bottom'
  elif metric < 0.75:
    align = 'top'
  elif metric > 0.75:
    align = 'bottom'
  else:
    align = 'center'
  return align

#USE: Create a center label in the middle of the radial chart.
#INPUT: a df of row length 1 with the first column as the current metric value and the second column is the target metric value
#OUTPUT: the proper text label
def add_center_label(first, second):
    percent = str(round(1.0*first/second*100,1)) + '%'
    return plt.text(0,
           0.2,
           percent,
           horizontalalignment='center',
           verticalalignment='center',
           fontsize = 40,
           family = 'sans-serif')

#USE: Formats a number with the apropiate currency tags.
#INPUT: a currency number
#OUTPUT: the properly formmated currency string
def get_currency_label(num):
  currency = ''
  if num < 10**3:
    currency = '$' + str(num)
  elif num < 10**6:
      currency = '$' + str(round(1.0*num/10**3,1)) + 'K'
  elif num < 10**9:
    currency = '$' + str(round(num/10**6,1)) + 'M'
  else:
    currency = '$' + str(round(num/10**9,1)) + 'B'

  return currency

#USE: Create a dynamic outer label that servers a pointer on the ring.
#INPUT: a df of row length 1 with the first column as the current metric value and the second column is the target metric value
#OUTPUT: the proper text label at the apropiate position
def add_current_label(first, second):
  currency = get_currency_label(first)

  return plt.text(1.5 * np.cos(0.5 *np.pi - 2 * np.pi * (first /second)) if first < second else 0,
           1.5 * np.sin(0.5 *np.pi - 2 * np.pi * first / second) if first < second else 1.5,
           currency,
           horizontalalignment=horizontal_aligner(first, second) if first < second else 'center',
           verticalalignment=vertical_aligner(first, second) if first < second else 'bottom',
           fontsize = 20,
           family = 'sans-serif')

def add_sub_center_label(second):
    amount = 'Goal: ' + get_currency_label(second)
    return plt.text(0,
            -.1,
            amount,
            horizontalalignment='center',
            verticalalignment='top',
            fontsize = 22,family = 'sans-serif')

#######################################################################
###                                                    MAIN FUNCTION                                                        ###
#######################################################################

def chart(current, goal, pipeline=0, colors = ["black", 'gray', 'lightgray'], overachiever_color = '#0c561d', width = 5, height = 5, dpi = 100):


  first = current
  second = goal

  # base styling logic
  ring_width = 0.3
  outer_radius = 1.5
  inner_radius = outer_radius - ring_width

  # set up plot
  ring_arrays = calculate_rings(first, second, pipeline)
  fig, ax = plt.subplots(figsize = (width,height), dpi = dpi)

  if first  >= second:
    outer_edge_color = None
    inner_edge_color = 'white'
    colors = [overachiever_color, 'white', 'white']
  else:
    outer_edge_color, inner_edge_color = ['white', None]


  # plot logic

  outer_ring, _ = ax.pie(ring_arrays[0],radius=1.5,
                    colors=colors,
                    startangle = 90,
                    counterclock = False)

  # add labels
  add_center_label(first, second)
  add_current_label(first, second)
  add_sub_center_label(second)
  plt.setp( outer_ring, width=ring_width, edgecolor=outer_edge_color)
  ax.axis('equal')
  plt.margins(0,0)
  plt.autoscale('enable')


  return plt

# TODO: EDIT these to reflect where the variables exist in your data frame

current = float(101) #float(df.iloc[0,0]
goal = float(100)  #float(df.iloc[0,1]

# this is an optional parameter if you do have pipeline
pipeline = float(30)  #float(df.iloc[0,1]

# call the function and output in periscope
periscope.output(chart(current, goal, pipeline,
colors = ["black", 'gray', 'lightgray'], overachiever_color = '#0c561d'))
#additional options (specify a color array of three colors) called color, matching current, pipeline, goal
#specify an overachiever color

 

Shout out to Kyle Dempsey for surfacing this use case. 

Reply Oldest first
  • Oldest first
  • Newest first
  • Active threads
  • Popular
Like2 Follow
  • 2 Likes
  • 2 mths agoLast active
  • 141Views
  • 1 Following