Last active
March 3, 2025 21:55
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""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