diff --git a/website/src/app/shared/components/visualization-page/shared/visualization-6-data-handler.ts b/website/src/app/shared/components/visualization-page/shared/visualization-6-data-handler.ts index 5a2297b8..6a2f1b60 100644 --- a/website/src/app/shared/components/visualization-page/shared/visualization-6-data-handler.ts +++ b/website/src/app/shared/components/visualization-page/shared/visualization-6-data-handler.ts @@ -12,6 +12,7 @@ interface DataEntry { CASE_NUMBER: string; RANK: number; AGE: number; + SEX: string; PERIOD: number; TIME_BEFORE_DEATH: number; @@ -47,6 +48,7 @@ const fakeEntries: DataEntry[] = [ CASE_NUMBER: '', RANK: 0, AGE: 0, + SEX: '', PERIOD: 0, TIME_BEFORE_DEATH: 0, @@ -73,6 +75,7 @@ const fakeEntries: DataEntry[] = [ CASE_NUMBER: '', RANK: 0, AGE: 0, + SEX: '', PERIOD: 0, TIME_BEFORE_DEATH: 120, @@ -111,6 +114,7 @@ export class Visualization6DataHandler implements DataHandler { private data?: DataEntry[]; private sortBy: SortField = 'HEALTH_RANK'; + private clusterBy: keyof DataEntry = 'SEX'; private sortRanks: Record> = {}; private ranks?: number[]; private ranksLookup?: Set; @@ -153,6 +157,7 @@ export class Visualization6DataHandler implements DataHandler { this.data = undefined; this.sortBy = 'HEALTH_RANK'; + this.clusterBy = 'SEX'; this.sortRanks = {}; this.ranks = undefined; this.ranksLookup = undefined; @@ -181,6 +186,9 @@ export class Visualization6DataHandler implements DataHandler { private updateData(): void { let { data = [] } = this; + data = this.clusterData(data); + this.sortRanks = this.compileSortRanks(data); + data = this.filterByRank(data); data = this.filterByAge(data); data = this.filterByEncounters(data); @@ -201,10 +209,150 @@ export class Visualization6DataHandler implements DataHandler { for (const { CASE_NUMBER, AGE_RANK, HEALTH_RANK, OVERDOSE_RANK, TIME_FIRST_OD, TIME_FIRST_RX, OD_DIFF, RX_DIFF, INCARCERATIONS_RANK, PRESCRIPTIONS_RANK } of data) { sortRanks[CASE_NUMBER] ??= { AGE_RANK, HEALTH_RANK, OVERDOSE_RANK, TIME_FIRST_OD, TIME_FIRST_RX, OD_DIFF, RX_DIFF, INCARCERATIONS_RANK, PRESCRIPTIONS_RANK }; } - return sortRanks; } + private clusterData(data: DataEntry[]): DataEntry[] { + const { clusterBy } = this; + const clusteredData: Record = {}; + let maxValue: number; + switch (this.clusterBy) { + case 'AGE': + maxValue = 100; + break; + case 'SEX': + maxValue = 1; + break; + default: + maxValue = Math.max(...data.map(o => o[clusterBy] as number), 0); + } + + for (const entry of data) { + const value = entry[clusterBy]; + let cluster = 0; + if (clusterBy === 'SEX') { + cluster = value === 'M' ? 0 : 1; // M=0, F=1 + } else { + cluster = Math.floor(value as number/maxValue * 10); + } + if (clusteredData[cluster]) { + clusteredData[cluster].push(entry); + } else { + clusteredData[cluster] = [entry]; + } + } + + return this.createClusterCases(clusteredData); + } + + private createClusterCases(clusteredData: Record): DataEntry[] { + let result: DataEntry[] = []; + const keys = Object.keys(clusteredData); + + for (const key of keys) { + const binNum = parseInt(key); + result = result.concat(this.createClusterCase(clusteredData[binNum], binNum)); + } + + return result; + } + + private createClusterCase(dataCluster: DataEntry[], binNum: number): DataEntry[] { + const result: DataEntry[] = []; + const periodCases: Record = {}; + + for (const entry of dataCluster) { + if (!periodCases[entry.PERIOD]) { + periodCases[entry.PERIOD] = [entry]; + } else { + periodCases[entry.PERIOD].push(entry); + } + } + + const keys = Object.keys(periodCases); + for (const key of keys) { + const k = parseInt(key); + result.push(this.createPeriodEntry(periodCases[k], binNum)); + } + + return result; + } + + private createPeriodEntry(periodData: DataEntry[], binNum: number): DataEntry { + const length = periodData.length; + let representativeCase: DataEntry = { + CASE_NUMBER: binNum.toString(), + RANK: 0, + AGE: 0, + SEX: periodData[0].SEX, + PERIOD: periodData[0].PERIOD, + TIME_BEFORE_DEATH: 0, + ALL_TYPES: 0, + HEALTH_ENCOUNTERS: 0, + OPIOID_PRESCRIPTIONS: 0, + INCARCERATIONS: 0, + OVERDOSES: 0, + NUM_ENCOUNTERS_TOTAL: 0, + AGE_RANK: 0, + HEALTH_RANK: 0, + OVERDOSE_RANK: 0, + INCARCERATIONS_RANK: 0, + PRESCRIPTIONS_RANK: 0, + FINAL_RANK: 0, + TIME_FIRST_OD: 0, + TIME_FIRST_RX: 0, + OD_DIFF: 0, + RX_DIFF: 0 + }; + + periodData.forEach(value => { + representativeCase.RANK += value.RANK; + representativeCase.AGE += value.AGE; + representativeCase.TIME_BEFORE_DEATH += value.TIME_BEFORE_DEATH; + representativeCase.ALL_TYPES += value.ALL_TYPES; + representativeCase.HEALTH_ENCOUNTERS += value.HEALTH_ENCOUNTERS; + representativeCase.OPIOID_PRESCRIPTIONS += value.OPIOID_PRESCRIPTIONS; + representativeCase.INCARCERATIONS += value.INCARCERATIONS; + representativeCase.OVERDOSES += value.OVERDOSES; + representativeCase.NUM_ENCOUNTERS_TOTAL += value.NUM_ENCOUNTERS_TOTAL; + representativeCase.AGE_RANK += value.AGE_RANK; + representativeCase.HEALTH_RANK += value.HEALTH_RANK; + representativeCase.OVERDOSE_RANK += value.OVERDOSE_RANK; + representativeCase.INCARCERATIONS_RANK += value.INCARCERATIONS_RANK; + representativeCase.PRESCRIPTIONS_RANK += value.PRESCRIPTIONS_RANK; + representativeCase.FINAL_RANK += value.FINAL_RANK; + representativeCase.TIME_FIRST_OD += value.TIME_FIRST_OD; + representativeCase.TIME_FIRST_RX += value.TIME_FIRST_RX; + representativeCase.OD_DIFF += value.OD_DIFF; + representativeCase.RX_DIFF += value.RX_DIFF; + }); + + representativeCase = { + ...representativeCase, + RANK: representativeCase.RANK/length, + AGE: representativeCase.AGE/length, + TIME_BEFORE_DEATH: representativeCase.TIME_BEFORE_DEATH/length, + ALL_TYPES: representativeCase.ALL_TYPES/length, + HEALTH_ENCOUNTERS: representativeCase.HEALTH_ENCOUNTERS/length, + OPIOID_PRESCRIPTIONS: representativeCase.OPIOID_PRESCRIPTIONS/length, + INCARCERATIONS: representativeCase.INCARCERATIONS/length, + OVERDOSES: representativeCase.OVERDOSES/length, + NUM_ENCOUNTERS_TOTAL: representativeCase.NUM_ENCOUNTERS_TOTAL/length, + AGE_RANK: representativeCase.AGE_RANK/length, + HEALTH_RANK: representativeCase.HEALTH_RANK/length, + OVERDOSE_RANK: representativeCase.OVERDOSE_RANK/length, + INCARCERATIONS_RANK: representativeCase.INCARCERATIONS_RANK/length, + PRESCRIPTIONS_RANK: representativeCase.PRESCRIPTIONS_RANK/length, + FINAL_RANK: representativeCase.FINAL_RANK/length, + TIME_FIRST_OD: representativeCase.TIME_FIRST_OD/length, + TIME_FIRST_RX: representativeCase.TIME_FIRST_RX/length, + OD_DIFF: representativeCase.OD_DIFF/length, + RX_DIFF: representativeCase.RX_DIFF/length + }; + + return representativeCase; + } + private filterByRank(data: DataEntry[]): DataEntry[] { const { ranks } = this; if (ranks === undefined) { diff --git a/website/src/app/shared/components/visualization-page/visualization-page.component.scss b/website/src/app/shared/components/visualization-page/visualization-page.component.scss index 70f808fd..aadd8bd7 100644 --- a/website/src/app/shared/components/visualization-page/visualization-page.component.scss +++ b/website/src/app/shared/components/visualization-page/visualization-page.component.scss @@ -45,6 +45,10 @@ z-index: 2; left: 50%; top: 50%; + + &.smooth-hide { + z-index: 0; + } } .visualization { diff --git a/website/src/assets/pages/vis6-maps-of-health/data.sql b/website/src/assets/pages/vis6-maps-of-health/data.sql index 96aa6e51..196cd99b 100644 --- a/website/src/assets/pages/vis6-maps-of-health/data.sql +++ b/website/src/assets/pages/vis6-maps-of-health/data.sql @@ -156,4 +156,4 @@ WITH SELECT * FROM FINAL GROUP BY CASE_NUMBER, PERIOD -ORDER BY RANK ASC, PERIOD ASC \ No newline at end of file +ORDER BY RANK ASC, PERIOD ASC diff --git a/website/src/assets/pages/vis6-maps-of-health/vis3.vl.json b/website/src/assets/pages/vis6-maps-of-health/vis3.vl.json index 65b954f8..041fe503 100644 --- a/website/src/assets/pages/vis6-maps-of-health/vis3.vl.json +++ b/website/src/assets/pages/vis6-maps-of-health/vis3.vl.json @@ -319,6 +319,10 @@ "field": "RX_DIFF_VALUE", "type": "quantitative", "title": "Days from 1st prescription to death" + }, + { + "field": "HEALTH_ENCOUNTERS", + "type": "quantitative" } ] } @@ -532,42 +536,46 @@ ] }, { - "mark": "text", - "data": { - "name": "sortable" - }, - "view": { - "strokeWidth": 0, - "strokeOpacity": 0 - }, - "params": [ + "vconcat": [ { - "name": "sort_by__raw", - "select": { - "type": "point", - "toggle": false + "mark": "text", + "data": { + "name": "sortable" }, - "bind": "legend", - "value": [ + "view": { + "strokeWidth": 0, + "strokeOpacity": 0 + }, + "params": [ { - "LABEL": "Health encounters" + "name": "sort_by__raw", + "select": { + "type": "point", + "toggle": false + }, + "bind": "legend", + "value": [ + { + "LABEL": "Health encounters" + } + ] + } + ], + "encoding": { + "opacity": { + "field": "LABEL", + "title": "Sort By", + "type": "ordinal", + "legend": { + "orient": "right", + "legendX": 0, + "legendY": 0, + "symbolSize": 0 + } } - ] - } - ], - "encoding": { - "opacity": { - "field": "LABEL", - "title": "Sort By", - "type": "ordinal", - "legend": { - "orient": "right", - "legendX": 0, - "legendY": 0, - "symbolSize": 0 } } - } + ] } ], "config": {