Skip to content

Commit

Permalink
Merge pull request #211 from episphere/demographics-page
Browse files Browse the repository at this point in the history
Prototype of demographics page and new style.
  • Loading branch information
LKMason authored Jan 9, 2024
2 parents 4190bfc + 1bc6685 commit eefd4b6
Show file tree
Hide file tree
Showing 13 changed files with 47,827 additions and 161 deletions.
35 changes: 27 additions & 8 deletions data/conceptMappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"race": "Race/Ethnicity",
"sex": "Sex",
"county": "County",
"state": "State"
"state": "State",
"age_group": "Age Group"
},
"measures": {
"crude_rate": "Crude Mortality Rate (per 100,000)",
Expand Down Expand Up @@ -779,13 +780,31 @@
"unit": "Proportion"
}
},
"races": {
"race": {
"Hispanic": {"formatted": "Hispanic"},
"Non-Hispanic American Indian or Alaska Native": {"formatted": "American Indian \n or Alaska Native \n (Non-Hispanic)"},
"Non-Hispanic Black or African American": {"formatted": "Black or \nAfrican American \n (Non-Hispanic)"},
"Non-Hispanic More than one race": {"formatted": "More than one race \n (Non-Hispanic)"},
"Non-Hispanic Native Hawaiian or Other Pacific Islander": {"formatted": "Native Hawaiian or \n Other Pacific Islander \n (Non-Hispanic)"},
"Non-Hispanic White": {"formatted": "White \n (Non-Hispanic)"},
"Non-Hispanic Asian": {"formatted": "Asian \n (Non-Hispanic)"}
"Non-Hispanic American Indian or Alaska Native": {
"formatted": "American Indian \n or Alaska Native \n (Non-Hispanic)",
"half_short": "American Indian or Alaska Native (NH)"
},
"Non-Hispanic Black or African American": {
"formatted": "Black or \nAfrican American \n (Non-Hispanic)",
"half_short": "Black or African American (NH)"
},
"Non-Hispanic More than one race": {
"formatted": "More than one race \n (Non-Hispanic)",
"half_short": "More than one race (NH)"
},
"Non-Hispanic Native Hawaiian or Other Pacific Islander": {
"formatted": "Native Hawaiian or \n Other Pacific Islander \n (Non-Hispanic)",
"half_short": "Native Hawaiian or Other Pacific Islander (NH)"
},
"Non-Hispanic White": {
"formatted": "White \n (Non-Hispanic)",
"half_short": "White (NH)"
},
"Non-Hispanic Asian": {
"formatted": "Asian \n (Non-Hispanic)",
"half_short": "Asian (NH)"
}
}
}
46,674 changes: 46,674 additions & 0 deletions data/demographic/demographic_data_2020.csv

Large diffs are not rendered by default.

Binary file added data/demographic/demographic_data_2020.csv.zip
Binary file not shown.
339 changes: 339 additions & 0 deletions src/pages/prototypePage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
/**
* @file The input and basic control logic for the demograpics page.
* @author Lee Mason <[email protected]>
*/

import { EpiTrackerData } from "../utils/EpiTrackerData.js"
import { State } from "../utils/State.js"
import { createDropdownDownloadButton, createOptionSorter, formatCauseName, formatName } from "../utils/helper.js";
import choices from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';
import * as d3 from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';
import { hookCheckbox, hookSelectChoices } from "../utils/input2.js";
import { plotDemographicPlots } from "../utils/demographicPlots.js";
import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/[email protected]/+esm";


window.onload = async () => {
init()
};

/**
* Defining some of the necessary configuration options and default values.
*/
const COMPARABLE_FIELDS = ["race", "sex", "age_group"]
const DATA_YEARS = ["2018", "2019", "2020", "2018-2020"]
const NUMERIC_MEASURES = ["crude_rate", "age_adjusted_rate"]

// The default state, shown if no URL params.
const INITIAL_STATE = {
compareBar: "sex",
compareFacet: "none",
sex: "All",
race: "All",
year: "2020",
ageGroup: "All",
measure: "age_adjusted_rate",
cause: "All",
areaState: "All",
startZero: true,
}

let state, dataManager
let elements, url, names

export function init() {
state = new State
dataManager = new EpiTrackerData()
elements = {
barContainer: document.getElementById("bar-container"),
sidebar: document.getElementById("ex-sidebar"),
title: document.getElementById("graph-title")
}

initializeState()
}

function initializeState() {
const initialState = { ...INITIAL_STATE }

url = new URL(window.location.href)
for (const [paramName, paramValue] of url.searchParams) {
initialState[paramName] = paramValue
}

state.defineProperty("compareBar", initialState.compareBar)
state.defineProperty("compareBarOptions", null)
state.defineProperty("compareFacet", initialState.compareFacet)
state.defineProperty("compareFacetOptions", null)
state.defineProperty("year", initialState.year)
state.defineProperty("yearOptions", DATA_YEARS)
state.defineProperty("cause", initialState.cause)
state.defineProperty("causeOptions", null)
state.defineProperty("ageGroup", initialState.ageGroup)
state.defineProperty("ageGroupOptions", null)
state.defineProperty("areaState", initialState.areaState)
state.defineProperty("areaStateOptions", null)

// The compareBar and compareFacet properties can't be the same value (unless they are 'none'), handle that logic here.
for (const [childProperty, parentProperty] of [["compareBar", "compareFacet"], ["compareFacet", "compareBar"]]) {
state.linkProperties(childProperty, parentProperty)
state.subscribe(parentProperty, () => {
if (state[parentProperty] == state[childProperty] && state[childProperty] != "none") {
state[childProperty] = "none"
}
})
}

// The values for the selections are dependent on the compares (e.g. if we are comparing by race, then the race select
// must be equal to "all").
state.defineProperty("race", initialState.race, ["compareBar", "compareFacet"])
state.defineProperty("raceOptions", [])
state.defineProperty("sex", initialState.sex, ["compareBar", "compareFacet"])
state.defineProperty("sexOptions", [])
for (const compareProperty of ["compareBar", "compareFacet"]) {
state.subscribe(compareProperty, () => {
if (COMPARABLE_FIELDS.includes(state[compareProperty])) {
state[state[compareProperty]] = "All"
}
})
}

state.defineProperty("measureOptions", null, ["compareBar", "compareFacet"])
for (const compareProperty of ["compareBar", "compareFacet"]) {
state.subscribe(compareProperty, () => {
let measureOptions = null
let measure = state.measure
if (["compareBar", "compareFacet"].some(d => state[d] == "age_group")) {
measureOptions = ["crude_rate"]
measure = "crude_rate"
} else {
measureOptions = NUMERIC_MEASURES
}
state.measureOptions = measureOptions.map(field => ({ value: field, label: names.measures[field] }))
state.measure = measure
})
}
state.defineProperty("measure", initialState.measure, ["measureOptions"])


state.defineProperty("startZero", initialState.startZero)

state.defineJointProperty("query", ["compareBar", "compareFacet", "areaState", "cause", "race", "sex", "year", "ageGroup"])
state.defineProperty("legendCheckValues", null, "query")
state.defineProperty("mortalityData", null, ["query"])
state.defineJointProperty("plotConfig", ["mortalityData", "query", "measure", "startZero"])

for (const param of Object.keys(initialState)) {
if (state.hasProperty(param)) {
state.subscribe(param, updateURLParam)
}
}

for (const inputSelectConfig of [
{ id: "#select-compare-bar", propertyName: "compareBar" },
{ id: "#select-compare-facet", propertyName: "compareFacet" },
{ id: "#select-select-race", propertyName: "race" },
{ id: "#select-select-sex", propertyName: "sex" },
{ id: "#select-select-state", propertyName: "areaState", searchable: true },
{ id: "#select-select-cause", propertyName: "cause", searchable: true },
{ id: "#select-select-year", propertyName: "year", forceEnd: "2018-2020" },
{ id: "#select-select-age", propertyName: "ageGroup" },
{ id: "#select-measure", propertyName: "measure" },
]) {
const sorter = createOptionSorter(["All", "None"], inputSelectConfig.propertyName == "year" ? ["2018-2020"] : [])

choices[inputSelectConfig.id] = hookSelectChoices(inputSelectConfig.id, state,
inputSelectConfig.propertyName, inputSelectConfig.propertyName + "Options", d => d, inputSelectConfig.searchable, sorter)
}

hookCheckbox("#check-start-zero", state, "startZero")

state.subscribe("query", queryUpdated)
state.subscribe("plotConfig", plotConfigUpdated)

// Load the data
Promise.all([
d3.json("../data/conceptMappings.json"),
dataManager.getDemographicMortalityData({ year: state.year })
]).then(([nameMappings, mortalityData]) => {
initialDataLoad(mortalityData, nameMappings)
})
}


function initialDataLoad(mortalityData, nameMappings) {
names = nameMappings

// Initialise the input state from the data
state.compareBarOptions = ["none", ...COMPARABLE_FIELDS]
.map(field => ({ value: field, label: names.fields[field] }))
state.compareFacetOptions = ["none", ...COMPARABLE_FIELDS]
.map(field => ({ value: field, label: names.fields[field] }))
state.causeOptions = [...new Set(mortalityData.map(d => d.cause))]
state.areaStateOptions = [...new Set(mortalityData.map(d => d.state_fips))]
.map(stateCode => ({ value: stateCode, label: nameMappings.states[stateCode]?.name }))
state.sexOptions = [...new Set(mortalityData.map(d => d.sex))]
state.raceOptions = [...new Set(mortalityData.map(d => d.race))]
state.ageGroupOptions = [...new Set(mortalityData.map(d => d.age_group))]
state.measureOptions = NUMERIC_MEASURES
.map(field => ({ value: field, label: nameMappings.measures[field] }))

setInputsEnabled()
state.trigger("race")

let resizeTimeout
const resizeObserver = new ResizeObserver((resizeWrapper) => {
if (resizeTimeout) {
clearTimeout(resizeTimeout);
}

resizeTimeout = setTimeout(() => {
state.trigger("plotConfig")
}, 25)
})
resizeObserver.observe(elements.barContainer)

addDownloadButton()
}

async function queryUpdated(query) {
if (query.compareBar == "race" || query.compareFacet == "race") {
choices["#select-select-race"].disable()
} else {
choices["#select-select-race"].enable()
}
if (query.compareBar == "sex" || query.compareFacet == "sex") {
choices["#select-select-sex"].disable()
} else {
choices["#select-select-sex"].enable()
}
if (query.compareBar == "age_group" || query.compareFacet == "age_group") {
choices["#select-select-age"].disable()
} else {
choices["#select-select-age"].enable()
}

const dataQuery = {
year: query.year,
cause: query.cause,
race: query.race,
sex: query.sex,
age_group: query.ageGroup,
state_fips: query.areaState,
}

if (query.compareBar != "none") dataQuery[query.compareBar] = "*"
if (query.compareFacet != "none") dataQuery[query.compareFacet] = "*"

let mortalityData = await dataManager.getDemographicMortalityData(dataQuery, { includeTotals: false })
state.mortalityData = mortalityData
updateTitle()
}

function plotConfigUpdated() {
if (!state.mortalityData) {
return
}

const xFormat = d => formatName(names, state.compareBar, d)
const tickFormat = d => formatName(names, state.compareFacet, d)

const barContainer = elements.barContainer
plotDemographicPlots(barContainer, state.mortalityData, {
compareBar: state.compareBar != "none" ? state.compareBar : null,
compareFacet: state.compareFacet != "none" ? state.compareFacet : null,
measure: state.measure,
plotOptions: {
x: {tickFormat: xFormat, label: formatName(names, "fields", state.compareBar)},
fx: {tickFormat: tickFormat, label: formatName(names, "fields", state.compareFacet)},
y: {label: formatName(names, "measures", state.measure)}
},
yStartZero: state.startZero,
})
}

function updateURLParam(value, param) {
if (INITIAL_STATE[param] != value) {
url.searchParams.set(param, value)
} else {
url.searchParams.delete(param)
}
history.replaceState({}, '', url.toString())
}

function setInputsEnabled(enabled) {
for (const input of [
"select-compare-bar",
"select-compare-facet",
"select-select-race",
"select-select-sex",
"select-select-cause",
"select-select-year",
"select-select-age",
"select-measure",
]) {
const element = document.getElementById(input)
if (enabled) {
element.removeAttribute("disabled")
} else {
element.setAttribute("disabled", "")
}
}

for (const choice of Object.values(choices)) {
choice.enable()
}
}

function addDownloadButton() {
const baseFilename = "epitracker_data"

const groupDownloadContainer = document.getElementById("download-container")
const downloadButton = createDropdownDownloadButton(false, [
{label: "Download data (CSV)", listener: () => downloadMortalityData(state.mortalityData, baseFilename, "csv")},
{label: "Download data (TSV)", listener: () => downloadMortalityData(state.mortalityData, baseFilename, "tsv")},
{label: "Download data (JSON)", listener: () => downloadMortalityData(state.mortalityData, baseFilename, "json")},
{label: "Download plot (PNG)", listener: downloadGraph},
])
groupDownloadContainer.appendChild(downloadButton)
}


function updateTitle() {
const level = state.spatialLevel == "county" ? "US county-level" : "US state-level"
let compareString = [state.compareBar, state.compareFacet]
.filter(d => d != "none")
.map(d => names.fields[d].toLowerCase())
.join(" and ")

if (compareString != "") {
compareString = " by " + compareString
}
const compareSet = new Set([state.compareBar, state.compareFacet])
const selects = [
{name: "Year", value: state.year},
{name: "Location", value: (() => {
return state.areaState == "All" ? "US" : names.states[state.areaState].name
})()},
{name: "Cause of death", value: formatCauseName(state.cause)},
{name: names.fields.sex, value: state.sex, exclude: compareSet.has("sex")},
{name: names.fields.race, value: state.race, exclude: compareSet.has("race")},
{name: names.fields.age_group, value: state.ageGroup, exclude: compareSet.has("age_group")}
]
const selectsString = selects
.filter(d => !d.exclude)
.map(d => `${d.name}: ${d.value}`)
.join("&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp")


const title = `${level} ${names.measures[state.measure].toLowerCase()} ${compareString}. </br> ${selectsString}`
elements.title.innerHTML = title
}

function downloadMortalityData() {

}

function downloadGraph() {

}
Loading

0 comments on commit eefd4b6

Please sign in to comment.