Skip to content

Instantly share code, notes, and snippets.

@sampottinger
Last active March 3, 2025 21:55
Show Gist options
  • Save sampottinger/f69bd39def0cd05268d5a639d49f354e to your computer and use it in GitHub Desktop.
Save sampottinger/f69bd39def0cd05268d5a639d49f354e to your computer and use it in GitHub Desktop.
Example from Lecture 11 using Sketchingpy which draws dev satisfaction from Stack Overflow Developer Survey by age.
"""Data model for a Stack Overflow Dev Satisfaction visualization.
Author: A Samuel Pottinger
License: BSD-3-Clause
"""
import functools
class Dataset:
"""Representation of input survey data.
Object describing the stack overflow developer survey results to visualize,
acting as the model in model-view-presenter.
"""
def __init__(self):
"""Create an empty unfinalized dataset."""
self._age_satisfaction = {}
self._age_years_coding = {}
self._age_count = {}
self._score_percent = {}
self._finalized = False
self._age_groups = None
self._satisfaction_scores = None
def set_age_satisfaction(self, age_group, uses_ai, satisfaction):
"""Specify the average satisfaction for an age group.
Args:
age_group: Integer coding for the age group for which satisfaction
is being provided.
uses_ai: Boolean flag for if the group reports using AI.
satisfaction: The satisfaction score as a number between 0 and 1.
"""
self._require_unfinalized()
self._check_satisfaction(satisfaction)
key = self._key_age(age_group, uses_ai)
self._age_satisfaction[key] = satisfaction
def get_age_satisfaction(self, age_group, uses_ai):
"""Get the average satisfaction for an age group.
Args:
age_group: Integer coding for the age group for which satisfaction
is being requested.
uses_ai: Boolean flag for if the group reports using AI.
Returns:
Satisfaction score as a float between 0 and 1.
"""
self._require_finalized()
key = self._key_age(age_group, uses_ai)
return self._get_required_value(
key,
self._age_satisfaction,
'age',
'satisfaction'
)
def set_age_years_coding(self, age_group, uses_ai, years_coding):
"""Specify the average years coding for an age group.
Args:
age_group: Integer coding for the age group for which years coding
is being provided.
uses_ai: Boolean flag for if the group reports using AI.
years_coding: Average years coding as a float.
"""
self._require_unfinalized()
key = self._key_age(age_group, uses_ai)
self._age_years_coding[key] = years_coding
def get_age_years_coding(self, age_group, uses_ai):
"""Get the average years coding for an age group.
Args:
age_group: Integer coding for the age group for which years coding
is being requested.
uses_ai: Boolean flag to get those using (True) or not using AI
(False).
Returns:
Average years coding as a float.
"""
self._require_finalized()
key = self._key_age(age_group, uses_ai)
return self._get_required_value(
key,
self._age_years_coding,
'age',
'years coding'
)
def set_age_count(self, age_group, uses_ai, count):
"""Specify the number of respondents for an age group.
Args:
age_group: Integer coding for the age group for which respondent
count is being provided.
uses_ai: Boolean flag for if the group reports using AI.
count: Integer count of respondents.
"""
self._require_unfinalized()
key = self._key_age(age_group, uses_ai)
self._age_count[key] = count
def get_age_count(self, age_group, uses_ai):
"""Get the number of respondents for an age group.
Args:
age_group: Integer coding
uses_ai: Boolean flag to get those using (True) or not using AI
(False).
"""
self._require_finalized()
key = self._key_age(age_group, uses_ai)
return self._get_required_value(
key,
self._age_count,
'age',
'respondent count'
)
def set_score_percent(self, score, uses_ai, percent):
"""Specify the percent of respondents had a score.
Args:
score: The staisfaction score group which is a float in increments
of 0.1 from 0 to 1. Any scores will be converted to increments
of 0.1 through rounding.
uses_ai: Boolean flag for if the group reports using AI.
percent: Percent of respondents. May be a float between 0 and 1 or
a string ending with a percent sign (%).
"""
self._require_unfinalized()
percent_parsed = self._parse_percent(percent)
key = self._key_satisfaction(score, uses_ai)
self._score_percent[key] = percent_parsed
def get_score_percent(self, score, uses_ai):
"""Get the percent of respondents with a score.
Args:
score: Flaot satisfaction score in increments of 0.1. Will be
rounded to nearest tenth.
uses_ai: Boolean flag to get those using (True) or not using AI
(False).
Returns:
Percent of respondents with the given satisfaction score after
rounding to the nearest tenth.
"""
self._require_finalized()
key = self._key_satisfaction(score, uses_ai)
return self._get_required_value(
key,
self._score_percent,
'satisfaction score group',
'percent of respondents'
)
def get_age_groups(self):
"""Get the age groups for which data were reported.
Returns:
List of integers representing the age groups for which satisfaction
scores, years coding, and repondent count are available sorted in
ascending value.
"""
self._require_finalized()
return self._age_groups
def get_satisfaction_scores(self):
"""Get the satisfaction score groups reported.
Get the satisfaction score groups for which there was at least one
respondent.
Returns:
List of satisfaction score groups as floating point values sorted in
ascending value.
"""
self._require_finalized()
return self._satisfaction_scores
def get_max_years_coding(self):
"""Get the maximum years coding observed in the dataset.
Returns:
Maximum yearrs coding as a floating point value.
"""
self._require_finalized()
return max(self._age_years_coding.values())
def get_max_score_percent(self):
"""Get the maximum statisfaction score histogram bucket percentage.
Returns:
The percentage of respondents choosing the most common satisfaction
score as a number between 0 and 1.
"""
self._require_finalized()
return max(self._score_percent.values())
def get_count_max(self):
"""Get the number of resondents in the largest subgroup.
Returns:
The maximum number of respondents in an age / satisfaction score
bucket as an integer.
"""
self._require_finalized()
return max(self._age_count.values())
def get_average_satisfaction(self):
"""Get the average overall satisfaction score.
Returns:
Average overall satisfaction score across the entire dataset.
"""
self._require_finalized()
age_groups = self.get_age_groups()
weighted_score_total = 0
count_total = 0
for uses_ai in [True, False]:
for age_group in age_groups:
count = self.get_age_count(age_group, uses_ai)
satisfaction = self.get_age_satisfaction(age_group, uses_ai)
weighted = count * satisfaction
weighted_score_total += weighted
count_total += count
return weighted_score_total / count_total
def finalize(self):
"""Indicate that all of the data are loaded into the dataset."""
self._require_unfinalized()
self._finalized = True
self._age_groups = self._compute_age_groups()
self._satisfaction_scores = self._compute_satisfaction_scores()
def _get_required_value(self, key, target, key_descript, target_descript):
"""Get a dataset value, throwing an error if not available.
Args:
key: The key to look up.
target: The sub-dataset (dictionary) to look it up in.
key_descript: Description of the key being looked up.
target_descript: Description of the sub-dataset being queried.
"""
if not key in target:
msg_vals = (key_descript, key, target_decript)
raise RuntimeError('Could not find %s (%s) in %s' % msg_vals)
return target[key]
def _compute_age_groups(self):
"""Precompute the available keys for age groups.
Returns:
List of sorted keys.
"""
def get_age_groups(target):
return map(lambda x: x.split(':')[0], target)
keys_combined = [
self._age_satisfaction.keys(),
self._age_years_coding.keys(),
self._age_count.keys()
]
keys = map(get_age_groups, keys_combined)
sets = map(lambda x: set(x), keys)
intersection = functools.reduce(
lambda a, b: a.intersection(b),
sets
)
parsed = [int(x) for x in intersection]
parsed.sort()
return parsed
def _compute_satisfaction_scores(self):
"""Precompute the available keys for satisfaction scores.
Returns:
List of sorted keys.
"""
keys_combined = self._score_percent.keys()
keys = map(lambda x: x.split(':')[0], keys_combined)
scores = [float(x) for x in keys]
scores.sort()
return scores
def _require_finalized(self):
"""Throw an error if the dataset is not finalized."""
if not self._finalized:
raise RuntimeError('Dataset must be finalized before using getter.')
def _require_unfinalized(self):
"""Throw an error if the dataset is already finalized."""
if self._finalized:
raise RuntimeError('Dataset cannot be modified after finalization.')
def _key_age(self, age, uses_ai):
"""Get a string representing an age group.
Convert an age group identifier to a string representing that age within
this dataset.
Args:
age: The age group like 25 for which a key is required.
uses_ai: Boolean flag to get those using (True) or not using AI
(False).
Returns:
String identifying the given age group.
"""
uses_ai_int = 1 if uses_ai else 0
return '%d:%d' % (age, uses_ai_int)
def _key_satisfaction(self, score, uses_ai):
"""Get a string representing a satisfaction score group.
Convert a float representing a satisfaction score or a staisfaction
score group to a string representing the group that score would fall
within where groups happen at each increment of 0.1.
Args:
age: The age group like 25 for which a key is required.
uses_ai: Boolean flag to get those using (True) or not using AI
(False).
Returns:
String identifying the given age group.
"""
uses_ai_int = 1 if uses_ai else 0
return '%.1f:%d' % (score, uses_ai_int)
def _check_satisfaction(self, score):
"""Check that a satisfaction score is within the expected range.
Args:
score: The score to check.
"""
assert score >= 0
assert score <= 1
def _parse_percent(self, percent_raw):
"""Parse a string or float as a precentage.
Parse a string or float as a precentage, checking that it is valid
(within the expected range). If percent_raw is a string that ends with
a percentage sign, the percentage sign will be removed and the rest of
the string parsed as a float but divided by 100.
Args:
percent_raw: Float to check if in range or a string to parse as a
percent.
Returns:
Float between 0 and 1 representing 0% and 100% respectively.
"""
if isinstance(percent_raw, str):
percent = percent_raw.strip()
if percent.endswith('%'):
percent_cast = float(percent[:-1]) / 100
else:
percent_cast = float(percent)
return self._parse_percent(percent_cast)
else:
assert percent_raw >= 0
assert percent_raw <= 1
return percent_raw
def load_dataset(sketch):
"""Load a finalized Dataset using the data layer of the given sketch.
Args:
sketch: The Sketch2D through which to access the dataset source files.
Returns:
Loaded finalized Dataset.
"""
data_layer = sketch.get_data_layer()
dataset = Dataset()
main_body_csv = data_layer.get_csv('main_body.csv')
for row in main_body_csv:
age_group = int(row['Age Group'])
satisfaction_score = float(row['Satisfaction'])
years_coding = float(row['Years Coding'])
count = int(row['Count'])
uses_ai = int(row['Uses AI']) == 1
dataset.set_age_satisfaction(age_group, uses_ai, satisfaction_score)
dataset.set_age_years_coding(age_group, uses_ai, years_coding)
dataset.set_age_count(age_group, uses_ai, count)
context_csv = data_layer.get_csv('distribution_context.csv')
for row in context_csv:
satisfaction = float(row['Satisfaction'])
uses_ai = int(row['Uses AI']) == 1
percent_str = str(row['Percent'])
dataset.set_score_percent(satisfaction, uses_ai, percent_str)
dataset.finalize()
return dataset
"""Logic to draw a Stack Overflow Dev Satisfaction visualization.
Logic to draw a Stack Overflow Dev Satisfaction visualization, demonstrating
shared axes.
Author: A Samuel Pottinger
License: BSD-3-Clause
"""
import math
import sketchingpy
import model
BACKGROUND_COLOR = '#F0F0F0'
BODY_COLOR_AI = '#8da0cb'
BODY_COLOR_NO_AI = '#fc8d62'
TEXT_COLOR = '#333333'
CONTEXT_COLOR = '#555555'
FONT_NAME = 'PublicSans-Regular'
LEFT_PAD = 10
RIGHT_PAD = 70
BOTTOM_PAD = 10
WIDTH = 800
HEIGHT = 600
TITLE_HEIGHT = 40
LEFT_EXT_SIZE = 150
BOTTOM_EXT_SIZE = 100
VERT_SHARED_AXIS_SIZE = 30
HORIZ_SHARED_AXIS_SIZE = 30
BODY_START_X = LEFT_EXT_SIZE + VERT_SHARED_AXIS_SIZE + LEFT_PAD
BODY_START_Y = TITLE_HEIGHT
BODY_END_X = WIDTH - RIGHT_PAD
BODY_END_Y = HEIGHT - BOTTOM_EXT_SIZE - HORIZ_SHARED_AXIS_SIZE - BOTTOM_PAD
MAX_HALO_AREA = 500
CENTER_RADIUS = 2.5
LABEL_SATSIF_GROUP = 0.6
LEFT_LABEL_GROUP = 0.8
LABEL_AGE_GROUP = 65
TITLE = 'Satisfaction Slightly Increases by Age Regardless of AI (2024)'
USING_AI_TEMPLATE = '%.0f%% of AI users score %.1f'
NOT_USING_AI_TEMPLATE = '%.0f%% of non-AI users score %.1f'
AGE_GROUP_LABEL = 'Avg satisfaction for 65+: %.1f'
YEARS_CODE_MESSAGE = 'Avg years coding: %.0f'
class CombinedVisualizationPresenter:
"""Presenter which renders the visualization in a sketchingpy.Sketch2D"""
def __init__(self, sketch, dataset):
"""Create a new presenter.
Args:
sketch: The sketchingpy.Sketch2D to use to draw this visualization.
dataset: The dataset (model.Dataset) to draw.
"""
self._dataset = dataset
self._sketch = sketch
def draw(self):
"""Draw this visualization.
Draw the visualization using the Painter's Algorithm. Draws the non-data
conveying elements (title, grid, etc). Then, draws contextualizing
elements like the overall average indicator. Finally, draws overlays
which "paint with negative space" on top of glyphs to create a grid in
activated negative space.
"""
self._sketch.push_transform()
self._sketch.push_style()
self._draw_title()
self._draw_horiz_axis()
self._draw_vert_axis()
self._draw_bottom_extension_axis()
self._draw_left_extension_axis()
self._draw_circle_size_axis()
self._draw_color_axis()
self._draw_main_avg_context()
self._draw_main_body()
self._draw_bottom_extension_body()
self._draw_left_extension_body()
self._draw_bottom_overlay()
self._draw_left_overlay()
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_title(self):
"""Draw the main title at the top of the visualization.
Draw the TITLE at the top of the screen at the center of the main
graphic.
"""
self._sketch.push_transform()
self._sketch.push_style()
body_mid_x = (BODY_START_X + BODY_END_X) / 2
self._sketch.translate(body_mid_x, TITLE_HEIGHT)
self._sketch.clear_stroke()
self._sketch.set_fill(TEXT_COLOR)
self._sketch.set_text_font(FONT_NAME, 20)
self._sketch.set_text_align('center', 'bottom')
self._sketch.draw_text(0, -5, TITLE)
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_horiz_axis(self):
"""Draw the main horizontal axis with age groups.
Draw the text describing the horizontal axes which depcits age groups.
This will draw text representing each age group in sorted order except
for the midpoint which is used to label the axis.
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(
BODY_START_X,
BODY_END_Y + HORIZ_SHARED_AXIS_SIZE / 2
)
self._sketch.set_fill(TEXT_COLOR)
self._sketch.clear_stroke()
self._sketch.set_text_align('center', 'center')
self._sketch.set_text_font(FONT_NAME, 11)
age_groups = self._dataset.get_age_groups()
center_group = age_groups[round(len(age_groups) / 2)]
for age_group in age_groups:
group_x = self._get_main_x(age_group)
is_center_group = abs(age_group - center_group) < 0.0001
if is_center_group:
group_text = 'Age Group'
elif age_group == 18:
group_text = '<= 18'
elif age_group == 65:
group_text = '>=65'
else:
group_text = '%d - %d' % (age_group, age_group + 9)
self._sketch.draw_text(group_x, 0, group_text)
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_vert_axis(self):
"""Draw the main vertical axis with satisfaction scores.
Draw the main vertical axies which shows satisfaction score from 0 to 1
at increments of 0.1. This will draw labels for each satisfaction score
bucket except for the midpoint which is used to label the graphic.
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(
BODY_START_X - VERT_SHARED_AXIS_SIZE / 2,
BODY_START_Y
)
self._sketch.set_fill(TEXT_COLOR)
self._sketch.clear_stroke()
self._sketch.set_text_align('center', 'center')
self._sketch.set_text_font(FONT_NAME, 11)
self._sketch.set_angle_mode('degrees')
score_groups = self._dataset.get_satisfaction_scores()
for score_group in score_groups:
group_y = self._get_main_y(score_group)
is_label_group = abs(score_group - LABEL_SATSIF_GROUP) < 0.0001
score_group_str = '%.1f' % score_group
if is_label_group:
group_text = 'Satisfaction'
elif (score_group * 10) % 2 == 0:
group_text = score_group_str
else:
group_text = ''
self._sketch.push_transform()
self._sketch.translate(0, group_y)
if is_label_group:
self._sketch.rotate(-90)
self._sketch.draw_text(0, 0, group_text)
self._sketch.pop_transform()
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_bottom_extension_axis(self):
"""Draw the vertical axis for the years programming sub-graphic.
Draw a simplified axis for the bottom plot which shows average years
programming per age group.
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(
BODY_START_X,
BODY_END_Y + HORIZ_SHARED_AXIS_SIZE
)
self._sketch.set_fill(TEXT_COLOR)
self._sketch.clear_stroke()
self._sketch.set_text_align('right', 'center')
self._sketch.set_text_font(FONT_NAME, 11)
self._sketch.draw_text(0, 0, "0")
self._sketch.draw_text(0, BOTTOM_EXT_SIZE / 2 - 5, "Avg Years")
self._sketch.draw_text(0, BOTTOM_EXT_SIZE / 2 + 5, "Programming")
max_years_coding = math.ceil(self._dataset.get_max_years_coding())
self._sketch.draw_text(0, BOTTOM_EXT_SIZE, max_years_coding)
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_left_extension_axis(self):
"""Draw the horizontal axis for the histogram sub-graphic.
Draw a simplified axis for the left-side histogram which depicts the
percentage of respondents reporting each satisfaction score at 0.1
increments from 0 to 1.
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(LEFT_PAD, BODY_END_Y + 2)
self._sketch.set_fill(TEXT_COLOR)
self._sketch.clear_stroke()
self._sketch.set_text_align('center', 'top')
self._sketch.set_text_font(FONT_NAME, 11)
max_percent = math.ceil(self._dataset.get_max_score_percent() * 100)
self._sketch.draw_text(LEFT_PAD, 0, '%d%%' % max_percent)
self._sketch.draw_text(LEFT_EXT_SIZE / 2, 0, 'Count')
self._sketch.draw_text(LEFT_EXT_SIZE, 0, "0%")
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_circle_size_axis(self):
"""Draw the legend for circle area (num respondents).
Draw the axis which indicates that circle area is used to show the
number of respondents per sub-group. This indicates that area of circle
(not radius) is used to depict group size. This is a simple label using
outline to try to maintain the visual hierarchy on the page (the bubbles
of the bubble plot that this legend shares space with "pop" out over
these legend elements).
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(BODY_END_X - 200, BODY_END_Y - 100)
self._sketch.set_text_font(FONT_NAME, 11)
self._sketch.set_text_align('left', 'center')
self._sketch.set_ellipse_mode('radius')
count_max = self._dataset.get_count_max()
increment = count_max / 4
for i in range(0, 5):
count = round(increment * i)
radius = self._get_helo_radius(count)
self._sketch.clear_stroke()
self._sketch.set_fill(CONTEXT_COLOR + '50')
self._sketch.draw_ellipse(0, i * 13, CENTER_RADIUS, CENTER_RADIUS)
self._sketch.clear_fill()
self._sketch.set_stroke(CONTEXT_COLOR + '50')
self._sketch.draw_ellipse(0, i * 13, radius, radius)
if i == 0 or i == 4:
self._sketch.clear_stroke()
self._sketch.set_fill(CONTEXT_COLOR)
self._sketch.draw_text(10, i * 13, '%d Respondents' % count)
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_color_axis(self):
"""Draw the legend describing the color coding in the visualization.
Draw the legend describing color coding for uses / does not use AI which
is used across the graphic. This is a simple label using outline to
try to maintain the visual hierarchy on the page (the bubbles of the
bubble plot that this legend shares space with "pop" out over these
legend elements).
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(BODY_START_X + 100, BODY_END_Y - 100)
self._sketch.set_text_font(FONT_NAME, 11)
self._sketch.set_text_align('left', 'center')
self._sketch.set_ellipse_mode('radius')
self._sketch.set_rect_mode('radius')
self._sketch.set_stroke(BODY_COLOR_AI)
self._sketch.clear_fill()
self._sketch.draw_ellipse(0, 0, CENTER_RADIUS, CENTER_RADIUS)
self._sketch.set_fill(CONTEXT_COLOR)
self._sketch.clear_stroke()
self._sketch.draw_text(7, 0, 'Uses AI')
self._sketch.set_stroke(BODY_COLOR_NO_AI)
self._sketch.clear_fill()
self._sketch.draw_rect(0, 13, CENTER_RADIUS, CENTER_RADIUS)
self._sketch.set_fill(CONTEXT_COLOR)
self._sketch.clear_stroke()
self._sketch.draw_text(7, 13, 'Does Not Use AI')
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_main_body(self):
"""Draw the central bubble plot body.
Draw the data-carrying elements of the bubble plot at the center of
the graphic that uses the main x and y axes. Some of these bubbles are
direct labeled as controlled by LABEL_AGE_GROUP.
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(BODY_START_X, BODY_START_Y)
self._sketch.clear_stroke()
self._sketch.set_ellipse_mode('radius')
self._sketch.set_rect_mode('radius')
self._sketch.set_text_font(FONT_NAME, 10)
def draw_subgroup(color, uses_ai, use_square):
"""Draw a specific subgroup as an individual bubble.
Draw a specific subgroup as an individual bubble where each bubble
has a solid glyph in the center heling show position and type. It
then has a "halo" which is semi-transparent showing the size of the
group.
Args:
color: The color to ue to draw these glyphs. This will be made
semi-transparent when drawing the halo.
uses_ai: Flag indicating if this group uses AI in their work
and False otherwise.
use_square: Flag indicating if this group should be represented
with a square or circle. Pass True for square and False for
circle. This only impacts the glyph at the center of the
halo. Halos are always represented by circles.
"""
age_groups = self._dataset.get_age_groups()
for age_group in age_groups:
avg_satisfaction = self._dataset.get_age_satisfaction(
age_group,
uses_ai
)
count = self._dataset.get_age_count(age_group, uses_ai)
x = self._get_main_x(age_group)
y = self._get_main_y(avg_satisfaction)
radius = self._get_helo_radius(count)
self._sketch.set_fill(color)
if use_square:
self._sketch.draw_rect(
x,
y,
CENTER_RADIUS,
CENTER_RADIUS
)
else:
self._sketch.draw_ellipse(
x,
y,
CENTER_RADIUS,
CENTER_RADIUS
)
self._sketch.set_fill(color + '50')
self._sketch.draw_ellipse(x, y, radius, radius)
if age_group == LABEL_AGE_GROUP:
self._sketch.set_fill(color)
message = AGE_GROUP_LABEL % avg_satisfaction
if uses_ai:
self._sketch.set_text_align('center', 'bottom')
self._sketch.draw_text(x, y - 7, message)
else:
self._sketch.set_text_align('center', 'top')
self._sketch.draw_text(x, y + 7, message)
draw_subgroup(BODY_COLOR_NO_AI, False, True)
draw_subgroup(BODY_COLOR_AI, True, False)
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_bottom_extension_body(self):
"""Draw the body of the plot of years coding.
Draw the plot of years coding, drawing the data-conveying elements of
the graphic (bars). This is a simple grouped bar plot with color showing
uses AI / does not use AI. This will have other glyphs drawn on top by
_draw_bottom_overlay which "paints with whitespace" to create a grid-
like effect.
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(
BODY_START_X,
BODY_END_Y + HORIZ_SHARED_AXIS_SIZE
)
self._sketch.clear_stroke()
self._sketch.set_rect_mode('corners')
self._sketch.set_text_font(FONT_NAME, 10)
self._sketch.set_angle_mode('degrees')
def draw_subgroup(color, uses_ai, offset):
"""Draw years of coding for an individual age group.
Args:
color: The color to ue when drawing this bar.
uses_ai: Flag indicating if this group uses AI in their work
and False otherwise.
offset: How much to offset the bar in the horizontal position to
make room for multiple bars in the group.
"""
self._sketch.set_fill(color)
age_groups = self._dataset.get_age_groups()
for age_group in age_groups:
years_coding = self._dataset.get_age_years_coding(
age_group,
uses_ai
)
start_x = self._get_main_x(age_group) + offset
y = self._get_bottom_y(years_coding)
self._sketch.draw_rect(start_x - 2, 0, start_x + 2, y)
if age_group == LABEL_AGE_GROUP:
self._sketch.push_transform()
message = YEARS_CODE_MESSAGE % years_coding
if uses_ai:
self._sketch.translate(start_x + 5, 0)
self._sketch.rotate(90)
self._sketch.set_text_align('left', 'bottom')
self._sketch.draw_text(0, 0, message)
else:
self._sketch.translate(start_x - 5, 0)
self._sketch.rotate(90)
self._sketch.set_text_align('left', 'top')
self._sketch.draw_text(0, 0, message)
self._sketch.pop_transform()
draw_subgroup(BODY_COLOR_NO_AI, False, -3)
draw_subgroup(BODY_COLOR_AI, True, 2)
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_left_extension_body(self):
"""Draw the body of the left-side histogram.
Draw the plot of percent of population per satisfaction score, drawing
the data-conveying elements of the plot (the bars).
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(LEFT_PAD, BODY_START_Y)
self._sketch.clear_stroke()
self._sketch.set_rect_mode('corners')
def draw_subgroup(color, uses_ai, offset):
"""Draw a satsifaction score bar group.
Args:
color: The color to use in drawing the bar.
uses_ai: Flag indicating if this group uses AI in their work
and False otherwise.
offset: How much to offset the bar in the vertical position to
make room for multiple bars in the group.
"""
self._sketch.set_fill(color)
score_groups = self._dataset.get_satisfaction_scores()
for score_group in score_groups:
percent = self._dataset.get_score_percent(score_group, uses_ai)
start_x = self._get_left_x(percent * 100)
y = self._get_main_y(score_group) + offset
self._sketch.draw_rect(start_x, y - 2, LEFT_EXT_SIZE, y + 2)
draw_subgroup(BODY_COLOR_NO_AI, False, -3)
draw_subgroup(BODY_COLOR_AI, True, 2)
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_main_avg_context(self):
"""Draw an indication of the overall average satisfaction score.
Draw an indication of the overall average satisfaction score to be
displayed in the main bubble plot behind the bubbles via the Painter's
Algorithm.
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(BODY_START_X, BODY_START_Y)
avg_satisfaction = self._dataset.get_average_satisfaction()
y = self._get_main_y(avg_satisfaction)
self._sketch.clear_stroke()
self._sketch.set_fill(CONTEXT_COLOR)
self._sketch.set_text_font(FONT_NAME, 10)
self._sketch.set_text_align('right', 'center')
self._sketch.draw_text(49, y, 'Avg: %.2f' % avg_satisfaction)
for x in range(50, BODY_END_X - BODY_START_X + 1, 3):
self._sketch.draw_pixel(x, y)
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_bottom_overlay(self):
"""Draw the negative-space grid elements on the years of coding plot.
Draw a replacement for the regular chart grid elements by using the
background color of the sketch. This "draws in negative space" on top of
the bars in the bottom sub-plot.
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(
BODY_START_X,
BODY_END_Y + HORIZ_SHARED_AXIS_SIZE
)
self._sketch.clear_fill()
self._sketch.set_stroke(BACKGROUND_COLOR)
max_years_coding = math.ceil(self._dataset.get_max_years_coding())
years = range(0, max_years_coding + 1, 5)
for year in years:
y = self._get_bottom_y(year)
self._sketch.draw_line(0, y, BODY_END_X - BODY_START_X + 5, y)
self._sketch.pop_style()
self._sketch.pop_transform()
def _draw_left_overlay(self):
"""Draw negative-space grid elements on percent of pop per score plot.
Draw a replacement for the regular chart grid elements by using the
background color of the sketch. This "draws in negative space" on top of
the bars in the left-side histogram showing the percent of population
that provided each satisfaction score.
"""
self._sketch.push_transform()
self._sketch.push_style()
self._sketch.translate(LEFT_PAD, BODY_START_Y)
self._sketch.clear_fill()
self._sketch.set_stroke(BACKGROUND_COLOR)
self._sketch.set_rect_mode('corners')
max_percent = math.ceil(self._dataset.get_max_score_percent() * 100)
percentages = range(0, max_percent + 1, 5)
for percentage in percentages:
x = self._get_left_x(percentage)
self._sketch.draw_line(x, -5, x, BODY_END_Y - BODY_START_Y)
self._sketch.set_text_font(FONT_NAME, 10)
label_y = self._get_main_y(LEFT_LABEL_GROUP)
self._sketch.clear_stroke()
percent = self._dataset.get_score_percent(LEFT_LABEL_GROUP, True) * 100
vals = (percent, LEFT_LABEL_GROUP)
message = USING_AI_TEMPLATE % vals
self._sketch.set_fill(BODY_COLOR_AI)
self._sketch.set_text_align('right', 'top')
self._sketch.draw_text(LEFT_EXT_SIZE, label_y + 5, message)
percent = self._dataset.get_score_percent(LEFT_LABEL_GROUP, False) * 100
vals = (percent, LEFT_LABEL_GROUP)
message = NOT_USING_AI_TEMPLATE % vals
self._sketch.set_fill(BODY_COLOR_NO_AI)
self._sketch.set_text_align('right', 'bottom')
self._sketch.draw_text(LEFT_EXT_SIZE, label_y - 5, message)
self._sketch.pop_style()
self._sketch.pop_transform()
def _get_main_x(self, age_group):
"""Get the x coordinate for an age group.
Get the x coordinate within the main plot where an age group like 65+
should be placed.
Args:
age_group: Name of the age group as it apepars in the dataset.
Returns:
The horizontal position in pixels where the age group is located
within the main plot. May be re-used by shared axes.
"""
effective_width = BODY_END_X - BODY_START_X
age_groups = self._dataset.get_age_groups()
assert age_group in age_groups
index = age_groups.index(age_group)
percent = (index + 1) / len(age_groups)
return effective_width * percent
def _get_main_y(self, satisfaction_score):
"""Get the x coordinate for a satisfaction score.
Get the y coordinate within the main plot where a satisfaction score
like 0.8 should be placed.
Args:
satisfaction_score: Satisfaction score for which to get a position.
Returns:
The vertical position in pixels where the satisfaction score is
located within the main plot. May be re-used by shared axes.
"""
effective_height = BODY_END_Y - BODY_START_Y
satisfaction_scores = self._dataset.get_satisfaction_scores()
min_score = min(satisfaction_scores)
max_score = max(satisfaction_scores)
percent = (satisfaction_score - min_score) / (max_score - min_score)
percent_reverse = 1 - percent
return effective_height * percent_reverse
def _get_bottom_y(self, years_coding):
"""Get the y coordinate where a years of programming should be encoded.
Args:
years_coding: Number of years coding like 12.
Returns:
Vertical position at which the bar representing this number should
end in pixels.
"""
max_years_coding = math.ceil(self._dataset.get_max_years_coding())
height_percent = years_coding / max_years_coding
return BOTTOM_EXT_SIZE * height_percent
def _get_left_x(self, percent):
"""Get the x coordinate where a % of respondents should be encoded.
Args:
percent: Percentage of respondents as a number between 0 and 100.
Returns:
Horizontal position at which the bar representing this number should
reach in pixels.
"""
max_percent = math.ceil(self._dataset.get_max_score_percent() * 100)
width_percent = percent / max_percent
width_percent_reverse = 1 - width_percent
return LEFT_EXT_SIZE * width_percent_reverse
def _get_helo_radius(self, count):
"""Get the radius to use when drawing a circle representing a count.
Args:
count: The number of respondents in the group being represented.
Returns:
Float radius to use in drawing the circle.
"""
max_count = self._dataset.get_count_max()
area_percent = count / max_count
area = MAX_HALO_AREA * area_percent
return math.sqrt(area)
sketch = sketchingpy.Sketch2D(WIDTH, HEIGHT)
dataset = model.load_dataset(sketch)
sketch.clear(BACKGROUND_COLOR)
presenter = CombinedVisualizationPresenter(sketch, dataset)
presenter.draw()
sketch.show()
sketch.save_image('satisfaction_ai.png')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment