Notebooks 2.0 is here.

Public
Edited
Feb 15
Insert cell
Insert cell
import {vl} from "@vega/vega-lite-api-v5"
Insert cell
import {printTable} from "@uwdata/data-utilities"
Insert cell
Insert cell
mobility = FileAttachment("national_statistics_by_parent_income_percentile_gender_race.csv").csv()
Insert cell
transition = FileAttachment("transition_processed.csv").csv()
Insert cell
crosswalk = FileAttachment("crosswalk_between_income_percentiles_and_dollars.csv").csv()
Insert cell
transition_native_mom = FileAttachment("processed_transition_native_mom.csv").csv()
Insert cell
Insert cell
vl.markLine({size: 2.5})
.data(mobility)
.transform(
vl.fold(["kfr_aian_pooled", "kfr_asian_pooled", "kfr_black_pooled", "kfr_hisp_pooled", "kfr_white_pooled"])
.as("race", "kfr"),
vl.calculate("datum.race === 'kfr_aian_pooled' ? 'AIAN' : datum.race === 'kfr_asian_pooled' ? 'Asian' : datum.race === 'kfr_black_pooled' ? 'Black' : datum.race === 'kfr_hisp_pooled' ? 'Hispanic' : 'White'").as('race_name')
)
.encode(
vl.x().fieldO("par_pctile").sort(null).title("Parent Family Income Percentile (th)").axis({
labelExpr: "datum.value % 5 === 0 ? datum.value : ''"
}),
vl.y().fieldQ("kfr").title("Average Child Family Income Percentile (th)"),
vl.color().fieldN("race_name").legend({title: "Race Group", symbolSize: '200'}),
vl.tooltip().fieldN('race_name')
)
.title('Relationship Between Parent and Child Family Income Percentiles in the U.S., by Race')
.width(600)
.render()
Insert cell
Insert cell
vl.markPoint()
.data(crosswalk)
.encode(
vl.x().fieldQ('parent_hh_income').scale({type: "log", domain:[1000, 2000000]})
.title('Average Parent Household Income ($, log scale)'),
vl.y().fieldQ('kid_hh_income').title('Average Child Household Income ($)'),
)
.title("Relationship Between Parent and Child Household Income in the U.S. in 2015 Dollars")
.width(600)
.render()
Insert cell
Insert cell
vl.markArea()
.data(mobility)
.transform(
vl.fold(
["density_aian_pooled", "density_asian_pooled", "density_black_pooled", "density_hisp_pooled", "density_white_pooled"]
)
.as("race", "density"),
vl.calculate("datum.race === 'density_aian_pooled' ? 'AIAN' : datum.race === 'density_asian_pooled' ? 'Asian' : datum.race === 'density_black_pooled' ? 'Black' : datum.race === 'density_hisp_pooled' ? 'Hispanic' : 'White'")
.as("race_label")
)
.encode(
vl.x().fieldO("par_pctile").sort(null).title("Parent Income Percentile (th)").axis({
labelExpr: "datum.value % 5 === 0 ? datum.value : ''"
}),
vl.y().fieldQ("density").title('Proportion (%)'),
vl.facet().fieldN("race_label").columns(1).title(null),
vl.tooltip(
{field: 'race_label', type:'nominal', title: 'Race'})
)
.title('Proportion of Children Born to Families Across Income Percentiles by Race Group')
.width(600)
.height(70)
.render()
Insert cell
Insert cell
{
const label = "datum.value === 'count_aian_pooled' ? 'AIAN' : datum.value === 'count_asian_pooled' ? 'Asian' : datum.value === 'count_black_pooled' ? 'Black' : datum.value === 'count_hisp_pooled' ? 'Hispanic' : 'White'"
return vl.markBar()
.data(mobility)
.transform(
vl.fold(
["count_aian_pooled", "count_asian_pooled", "count_black_pooled", "count_hisp_pooled", "count_white_pooled"]
)
.as("race", "count")
)
.encode(
vl.x().fieldO("par_pctile").sort(null).title("Parent Income Percentile (th)").axis({
labelExpr: "datum.value % 5 === 0 ? datum.value : ''"
}),
vl.y().fieldQ("count").title("Number of Children").axis({grid: false}),
vl.color().fieldN("race").scale({scheme: 'tableau10'}).legend({
title: "Race Group",
labelExpr: label
}),
vl.opacity().value(0.8),
)
.title("The Number of Children Born to Families Across Income Percentiles by Race")
.width(600)
.height(500)
.render();
}
Insert cell
Insert cell
vl.markBar()
.data(transition)
.transform(
vl.filter("datum.gender == 'P' && datum.kid_race != 'All'"),
vl.fold(['fraction_kfr_par_same', 'fraction_kfr_par_up', 'fraction_kfr_par_down'])
.as('transition_type', 'fraction'),
vl.calculate('datum.fraction * 100').as('percentage'),
vl.calculate("datum.transition_type === 'fraction_kfr_par_down' ? 'Downward' : datum.transition_type === 'fraction_kfr_par_same' ? 'Unchanged' : 'Upward'").as('type')
)
.encode(
vl.y().fieldN('kid_race').sort(vl.fieldN('fraction_kfr_par_up').order('descending')).title('Race of the Kids'),
vl.x().fieldQ('percentage').aggregate('sum').title('Proportion of Children (%)').scale({domain: [0, 100]}),
vl.color().fieldN('type').scale({range: ['#385C73', '#F2A35E', '#D94343']}).legend({title: "Income Mobility"}),
vl.tooltip(
[{ field: 'kid_race', type:'nominal', title: 'Race' },
{ field: 'type', type: 'nominal', title: 'Mobility Type'},
{ field: 'percentage', type: 'quantitative', format: '.2f', title: 'Percentage (%)' }])
)
.title('Income Mobility Patterns Among Different Races')
.width(600)
.height(200)
.render()
Insert cell
Insert cell
vl.markBar()
.data(transition)
.transform(
vl.filter("datum.gender != 'P' && datum.kid_race != 'All'"),
vl.fold(['fraction_kfr_par_same', 'fraction_kfr_par_up', 'fraction_kfr_par_down'])
.as('transition_type', 'fraction'),
vl.calculate('datum.fraction * 100').as('percentage'),
vl.calculate("datum.transition_type === 'fraction_kfr_par_down' ? 'Downward' : datum.transition_type === 'fraction_kfr_par_same' ? 'Unchanged' : 'Upward'").as('type'),
vl.calculate("datum.gender === 'M' ? 'Male' : 'Female'").as('gender_name')
)
.encode(
vl.y().fieldN('kid_race').sort(vl.fieldN('fraction_kfr_par_up').order('descending')).title('Race of the Kids'),
vl.x().fieldQ('percentage').aggregate('sum').title('Proportion of Children (%)').scale({domain: [0, 100]}),
vl.yOffset().fieldN('gender').sort(['M', 'F']),
vl.color().fieldN('type').scale({range: ['#385C73', '#F2A35E', '#D94343']}).legend({
title: "Income Mobility"
}),
vl.opacity().fieldN('gender_name').scale({ domain: ['Male', 'Female'], range: [1, 0.6] }).legend({
title: 'Gender'
}),
vl.tooltip(
[{field: 'kid_race', type:'nominal', title: 'Race'},
{field: 'gender_name', type: 'nominal', title: 'Gender'},
{ field: 'type', type: 'nominal', title: 'Mobility Type'},
{ field: 'percentage', type: 'quantitative', format: '.2f', title: 'Percentage (%)' }])
)
.title('Income Mobility Patterns of Males and Females Among Different Races')
.width(600)
.height(300)
.render()
Insert cell
Insert cell
{
function quintile_transition(child_parent_compare) {
const title =
child_parent_compare === 'fraction_kfr_par_up' ? 'Upward' :
child_parent_compare === 'fraction_kfr_par_same' ? 'Unchanged' :
'Downward';
return vl.markBar()
.data(transition)
.transform(
vl.filter("datum.gender != 'P' && datum.kid_race != 'All'"),
vl.calculate("datum.gender === 'M' ? 'Male' : 'Female'").as('gender_name')
)
.encode(
vl.x().fieldQ(child_parent_compare).aggregate('sum').title("Proportion of Children Within Race Group"),
vl.y().fieldN('kid_race').title('Race')
.sort(vl.fieldQ(child_parent_compare).order('descending')),
vl.color().fieldN('gender_name')
.scale({ domain: ['Male', 'Female'], range: ['#0388A6', "#F288AF"]})
.legend({title: 'Gender'}),
vl.tooltip(
[{field: 'kid_race', type:'nominal', title: 'Race'},
{field: 'gender_name', type: 'nominal', title: 'Gender'},
{ field: child_parent_compare, type: 'quantitative', format: '.2f', title: 'Percentage' }])
)
.title(title)
.width(600)
}

return vl.vconcat(
quintile_transition('fraction_kfr_par_up'),
quintile_transition('fraction_kfr_par_same'),
quintile_transition('fraction_kfr_par_down'),
)
.title('Income Mobility Breakdown: Ranking Racial Groups Within Each Mobility Type')
.render()
}
Insert cell
Insert cell
vl.markBar()
.data(transition)
.transform(
vl.filter("datum.gender != 'P' && datum.kid_race != 'All' && datum.kid_race != 'Other'"),
vl.calculate("datum.gender === 'M' ? 'Male' : 'Female'").as('gender_name')
)
.encode(
vl.x().fieldQ('kfr_q1_cond_par_q5').title("Proportion of Top-Income Children Falling to the Lowest Quintile").aggregate('sum'),
vl.y().fieldN('kid_race')
.sort(vl.fieldQ('kfr_q1_cond_par_q5').order('descending')).title('Race'),
vl.color().fieldN('gender_name')
.scale({ domain: ['Male', 'Female'], range: ['#0388A6', "#F288AF"]})
.legend({title: 'Gender'}),
vl.tooltip(
[{field: 'kid_race', type:'nominal', title: 'Race'},
{field: 'gender_name', type: 'nominal', title: 'Gender'},
{ field: 'kfr_q1_cond_par_q5', type: 'quantitative', format: '.2f', title: 'Percentage' }])
)
.width(600)
.height(200)
.title('Drastic Downward Mobility: Who Falls the Furthest?')
.render()
Insert cell
Insert cell
vl.markBar()
.data(transition)
.transform(
vl.filter("datum.gender != 'P' && datum.kid_race != 'All' && datum.kid_race != 'Other'"),
vl.calculate("datum.gender === 'M' ? 'Male' : 'Female'").as('gender_name')
)
.encode(
vl.x().fieldQ('kfr_q5_cond_par_q1').title("Proportion of Lowest-Income Children Climbing to the Highest Quintile").aggregate('sum'),
vl.y().fieldN('kid_race')
.sort(vl.fieldQ('kfr_q5_cond_par_q1').order('descending')).title('Race'),
vl.color().fieldN('gender_name')
.scale({ domain: ['Male', 'Female'], range: ['#0388A6', "#F288AF"]})
.legend({title: 'Gender'}),
vl.tooltip(
[{field: 'kid_race', type:'nominal', title: 'Race'},
{ field: 'gender_name', type: 'nominal', title: 'Gender'},
{ field: 'kfr_q5_cond_par_q1', type: 'quantitative', format: '.2f', title: 'Percentage' }])
)
.width(600)
.height(200)
.title('From Poverty to Prosperity: Upward Mobility by Race')
.render()
Insert cell
Insert cell
{
const label = "datum.value === 'kid_jail_black_male' ? 'Black Male' : datum.value === 'kid_jail_black_female' ? 'Black Female' : datum.value === 'kid_jail_white_male' ? 'While Male' : 'White Female'"
return vl.markPoint({filled: true})
.data(mobility)
.transform(
vl.fold(
["kid_jail_black_male", "kid_jail_black_female", "kid_jail_white_male", "kid_jail_white_female"]
)
.as("race", "percentage")
)
.encode(
vl.x().fieldO("par_pctile").sort(null).title("Parent Income Percentile (th)").axis({
labelExpr: "datum.value % 5 === 0 ? datum.value : ''"
}),
vl.y().fieldQ("percentage").title("Percentage of Children incarcerated (%, log scale)").scale({type: 'log'}),
vl.color().fieldN("race").scale({
domain: ["kid_jail_black_male", "kid_jail_black_female", "kid_jail_white_male", "kid_jail_white_female"],
scheme: 'set1'
}).legend({
title: "Race Group",
labelExpr: label
}),
)
.width(600)
.height(500)
.title('Criminal Rate of Children Across Family Income Percentiles by Race and Gender')
.render();
}
Insert cell
Insert cell
{
const label = "datum.value === 'kid_college_black_male' ? 'Black Male' : datum.value === 'kid_college_black_female' ? 'Black Female' : datum.value === 'kid_college_white_male' ? 'While Male' : 'White Female'"
return vl.markPoint({filled: true})
.data(mobility)
.transform(
vl.filter("datum.kid_college_black_female != ''"),
vl.fold(
["kid_college_black_male", "kid_college_black_female", "kid_college_white_male", "kid_college_white_female"]
)
.as("race", "percentage")
)
.encode(
vl.x().fieldO("par_pctile").sort(null).title("Parent Income Percentile (th)").axis({
labelExpr: "datum.value % 5 === 0 ? datum.value : ''"
}),
vl.y().fieldQ("percentage").title("Percentage of Children with College Attendance (%)"),
vl.color().fieldN("race").scale({
domain: ["kid_college_black_male", "kid_college_black_female", "kid_college_white_male", "kid_college_white_female"],
scheme: 'set1'
}).legend({
title: "Race Group",
labelExpr: label
}),
)
.width(600)
.height(500)
.title('College Rate of Children Across Family Income Percentiles by Race and Gender')
.render();
}
Insert cell
Insert cell
vl.markBar()
.data(transition_native_mom)
.transform(
vl.filter("datum.gender == 'P' && datum.kid_race != 'All'"),
vl.fold(['fraction_kfr_par_same', 'fraction_kfr_par_up', 'fraction_kfr_par_down'])
.as('transition_type', 'fraction'),
vl.calculate('datum.fraction * 100').as('percentage'),
vl.calculate("datum.transition_type === 'fraction_kfr_par_down' ? 'Downward' : datum.transition_type === 'fraction_kfr_par_same' ? 'Unchanged' : 'Upward'").as('type')
)
.encode(
vl.y().fieldN('kid_race').sort(vl.fieldN('fraction_kfr_par_up').order('descending')).title('Race of the Kids'),
vl.x().fieldQ('percentage').aggregate('sum').title('Proportion of Children (%)').scale({domain: [0, 100]}),
vl.color().fieldN('type').scale({range: ['#385C73', '#F2A35E', '#D94343']}).legend({title: "Income Mobility"}),
vl.tooltip(
[{field: 'kid_race', type:'nominal', title: 'Race'},
{ field: 'type', type: 'nominal', title: 'Mobility Type'},
{ field: 'percentage', type: 'quantitative', format: '.2f', title: 'Percentage (%)' }])
)
.title('Income Mobility Patterns of Children with U.S.-Native Moms Among Different Races')
.width(600)
.height(200)
.render()

Insert cell
Insert cell
vl.markBar()
.data(transition)
.transform(
vl.filter("datum.kid_race == 'All'"),
vl.fold(['kfr_q1', 'kfr_q2', 'kfr_q3', 'kfr_q4', 'kfr_q5', 'par_q1', 'par_q2', 'par_q3', 'par_q4', 'par_q5'])
.as('income_type', 'fraction'),
)
.encode(
vl.y().fieldN('income_type').title('Parent/Children Household Income Quintile'),
vl.x().fieldQ('fraction').title('The Fraction of Children')
)
.title('Data Quality Issue: Intergenerational Transition Matrices by Race and Gender')
.width(600)
.height(300)
.render()
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more