-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add arrests percentage stops by contraband type graph (#286)
* Add arrests by percentage view + component * add arrests percentage of searches graph * add graph for stop counts with or without driver arrested * Add graph for percentage of stops with arrests per stop purpose group * Add percentage of stops with arrests per stop purpose * Add percentage of searches for stop purpose groups graph * add arrests percentage of searches per stop purpose graph * add percentage of stops per contraband type * Update counts of stops w/o arrests graph
- Loading branch information
Showing
19 changed files
with
1,909 additions
and
8 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import ArrestsStyled from './Arrests.styles'; | ||
|
||
// Util | ||
import { YEARS_DEFAULT } from '../chartUtils'; | ||
|
||
// Hooks | ||
import useMetaTags from '../../../Hooks/useMetaTags'; | ||
import useTableModal from '../../../Hooks/useTableModal'; | ||
|
||
// Children | ||
import DataSubsetPicker from '../ChartSections/DataSubsetPicker/DataSubsetPicker'; | ||
import PercentageOfStops from './Charts/PercentageOfStops'; | ||
import useYearSet from '../../../Hooks/useYearSet'; | ||
import PercentageOfSearches from './Charts/PercentageOfSearches'; | ||
import CountOfStopsAndArrests from './Charts/CountOfStopsAndArrests'; | ||
import PercentageOfStopsForStopPurposeGroup from './Charts/PercentageOfStopsForPurposeGroup'; | ||
import PercentageOfStopsForStopPurpose from './Charts/PercentageOfStopsPerStopPurpose'; | ||
import PercentageOfSearchesForStopPurposeGroup from './Charts/PercentageOfSearchesForPurposeGroup'; | ||
import PercentageOfSearchesPerStopPurpose from './Charts/PercentageOfSearchesPerStopPurpose'; | ||
import PercentageOfStopsPerContrabandType from './Charts/PercentageOfStopsPerContrabandType'; | ||
import Switch from 'react-switch'; | ||
import { SwitchContainer } from '../TrafficStops/TrafficStops.styled'; | ||
|
||
function Arrests(props) { | ||
const [year, setYear] = useState(YEARS_DEFAULT); | ||
const [yearRange] = useYearSet(); | ||
const [togglePercentageOfStops, setTogglePercentageOfStops] = useState(true); | ||
const [togglePercentageOfSearches, setTogglePercentageOfSearches] = useState(true); | ||
|
||
const renderMetaTags = useMetaTags(); | ||
const [renderTableModal] = useTableModal(); | ||
|
||
useEffect(() => { | ||
if (window.location.hash) { | ||
document.querySelector(`${window.location.hash}`).scrollIntoView(); | ||
} | ||
}, []); | ||
|
||
const handleYearSelect = (y) => { | ||
if (y === year) return; | ||
setYear(y); | ||
}; | ||
|
||
return ( | ||
<ArrestsStyled> | ||
{renderMetaTags()} | ||
{renderTableModal()} | ||
<div style={{ display: 'flex', justifyContent: 'center' }}> | ||
<DataSubsetPicker | ||
label="Year" | ||
value={year} | ||
onChange={handleYearSelect} | ||
options={[YEARS_DEFAULT].concat(yearRange)} | ||
dropDown | ||
/> | ||
</div> | ||
<PercentageOfStops {...props} year={year} /> | ||
<PercentageOfSearches {...props} year={year} /> | ||
<CountOfStopsAndArrests {...props} year={year} /> | ||
|
||
<SwitchContainer> | ||
<span> | ||
Switch to view {togglePercentageOfStops ? 'all stop purposes' : 'grouped stop purposes '} | ||
</span> | ||
<Switch | ||
onChange={() => setTogglePercentageOfStops(!togglePercentageOfStops)} | ||
checked={togglePercentageOfStops} | ||
className="react-switch" | ||
/> | ||
</SwitchContainer> | ||
{togglePercentageOfStops ? ( | ||
<PercentageOfStopsForStopPurposeGroup {...props} year={year} /> | ||
) : ( | ||
<PercentageOfStopsForStopPurpose {...props} year={year} /> | ||
)} | ||
|
||
<SwitchContainer> | ||
<span> | ||
Switch to view{' '} | ||
{togglePercentageOfSearches ? 'all stop purposes' : 'grouped stop purposes '} | ||
</span> | ||
<Switch | ||
onChange={() => setTogglePercentageOfSearches(!togglePercentageOfSearches)} | ||
checked={togglePercentageOfSearches} | ||
className="react-switch" | ||
/> | ||
</SwitchContainer> | ||
|
||
{togglePercentageOfSearches ? ( | ||
<PercentageOfSearchesForStopPurposeGroup {...props} year={year} /> | ||
) : ( | ||
<PercentageOfSearchesPerStopPurpose {...props} year={year} /> | ||
)} | ||
|
||
<PercentageOfStopsPerContrabandType {...props} year={year} /> | ||
</ArrestsStyled> | ||
); | ||
} | ||
|
||
export default Arrests; | ||
|
||
export const ARRESTS_TABLE_COLUMNS = [ | ||
{ | ||
Header: 'Year', | ||
accessor: 'year', // accessor is the "key" in the data | ||
}, | ||
{ | ||
Header: 'White*', | ||
accessor: 'white', | ||
}, | ||
{ | ||
Header: 'Black*', | ||
accessor: 'black', | ||
}, | ||
{ | ||
Header: 'Native American*', | ||
accessor: 'native_american', | ||
}, | ||
{ | ||
Header: 'Asian*', | ||
accessor: 'asian', | ||
}, | ||
{ | ||
Header: 'Other*', | ||
accessor: 'other', | ||
}, | ||
{ | ||
Header: 'Hispanic', | ||
accessor: 'hispanic', | ||
}, | ||
{ | ||
Header: 'Total', | ||
accessor: 'total', | ||
}, | ||
]; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import styled from 'styled-components'; | ||
import ChartPageBase from '../ChartSections/ChartPageBase'; | ||
import { smallerThanDesktop, smallerThanTabletLandscape } from '../../../styles/breakpoints'; | ||
|
||
export default styled(ChartPageBase)``; | ||
|
||
export const ChartWrapper = styled.div` | ||
width: 100%; | ||
height: auto; | ||
`; | ||
|
||
export const HorizontalBarWrapper = styled.div` | ||
display: flex; | ||
flex-direction: row; | ||
flex-wrap: no-wrap; | ||
width: 100%; | ||
margin: 0 auto; | ||
justify-content: space-evenly; | ||
@media (${smallerThanDesktop}) { | ||
flex-wrap: wrap; | ||
} | ||
`; | ||
|
||
export const BarContainer = styled.div` | ||
width: 100%; | ||
height: 500px; | ||
@media (${smallerThanTabletLandscape}) { | ||
width: 100%; | ||
} | ||
display: ${(props) => (props.visible ? 'block' : 'none')}; | ||
`; |
150 changes: 150 additions & 0 deletions
150
frontend/src/Components/Charts/Arrest/Charts/CountOfStopsAndArrests.js
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import * as S from '../../ChartSections/ChartsCommon.styled'; | ||
|
||
// Children | ||
import { P } from '../../../../styles/StyledComponents/Typography'; | ||
import ChartHeader from '../../ChartSections/ChartHeader'; | ||
import HorizontalBarChart from '../../../NewCharts/HorizontalBarChart'; | ||
import axios from '../../../../Services/Axios'; | ||
import useOfficerId from '../../../../Hooks/useOfficerId'; | ||
import { ChartWrapper } from '../Arrests.styles'; | ||
import NewModal from '../../../NewCharts/NewModal'; | ||
import { ARRESTS_TABLE_COLUMNS } from '../Arrests'; | ||
|
||
function CountOfStopsAndArrests(props) { | ||
const { agencyId, agencyName, showCompare, year } = props; | ||
|
||
const officerId = useOfficerId(); | ||
|
||
const initArrestData = { | ||
labels: [], | ||
datasets: [], | ||
isModalOpen: false, | ||
tableData: [], | ||
csvData: [], | ||
loading: true, | ||
}; | ||
const [arrestData, setArrestData] = useState(initArrestData); | ||
|
||
useEffect(() => { | ||
const params = []; | ||
if (year && year !== 'All') { | ||
params.push({ param: 'year', val: year }); | ||
} | ||
if (officerId) { | ||
params.push({ param: 'officer', val: officerId }); | ||
} | ||
|
||
const urlParams = params.map((p) => `${p.param}=${p.val}`).join('&'); | ||
const url = `/api/agency/${agencyId}/arrests-stops-driver-arrested/?${urlParams}`; | ||
axios | ||
.get(url) | ||
.then((res) => { | ||
const tableData = []; | ||
const resTableData = res.data.table_data.length | ||
? JSON.parse(res.data.table_data) | ||
: { data: [] }; | ||
resTableData.data.forEach((e) => { | ||
const dataCounts = { ...e }; | ||
delete dataCounts.year; | ||
// Need to assign explicitly otherwise the download data orders columns by alphabet. | ||
tableData.unshift({ | ||
year: e.year, | ||
white: e.white, | ||
black: e.black, | ||
native_american: e.native_american, | ||
asian: e.asian, | ||
other: e.other, | ||
hispanic: e.hispanic, | ||
total: Object.values(dataCounts).reduce((a, b) => a + b, 0), | ||
}); | ||
}); | ||
const labels = ['Stops With Arrests', 'Stops Without Arrests']; | ||
const colors = ['#96a0fa', '#5364f4']; | ||
|
||
const datasets = res.data.arrest_counts.map((dataset, i) => ({ | ||
axis: 'y', | ||
label: labels[i], | ||
data: dataset.data, | ||
fill: false, | ||
backgroundColor: colors[i], | ||
borderColor: colors[i], | ||
hoverBackgroundColor: colors[i], | ||
borderWidth: 1, | ||
})); | ||
const data = { | ||
labels: ['White', 'Black', 'Hispanic', 'Asian', 'Native American', 'Other'], | ||
datasets, | ||
isModalOpen: false, | ||
tableData, | ||
csvData: tableData, | ||
}; | ||
|
||
setArrestData(data); | ||
}) | ||
.catch((err) => console.log(err)); | ||
}, [year]); | ||
|
||
const formatTooltipValue = (ctx) => ctx.raw; | ||
|
||
const subjectObserving = () => { | ||
if (officerId) { | ||
return 'by this officer'; | ||
} | ||
if (agencyId === '-1') { | ||
return 'for the entire state'; | ||
} | ||
return 'by this department'; | ||
}; | ||
|
||
const getYearPhrase = () => (year && year !== 'All' ? ` in ${year}` : ''); | ||
|
||
const getBarChartModalSubHeading = (title) => `${title} ${subjectObserving()}${getYearPhrase()}`; | ||
|
||
return ( | ||
<S.ChartSection> | ||
<ChartHeader | ||
chartTitle="Stop Counts With/Without Arrests" | ||
handleViewData={() => setArrestData((state) => ({ ...state, isOpen: true }))} | ||
/> | ||
<S.ChartDescription> | ||
<P>Percentage of stops that led to an arrest for a given race / ethnic group.</P> | ||
<NewModal | ||
tableHeader="Stop Counts With/Without Arrests" | ||
tableSubheader="Shows what numbers of stops led to an arrest for a given race / ethnic group." | ||
agencyName={agencyName} | ||
tableData={arrestData.tableData} | ||
csvData={arrestData.csvData} | ||
columns={ARRESTS_TABLE_COLUMNS} | ||
tableDownloadName="Arrests_By_Percentage" | ||
isOpen={arrestData.isOpen} | ||
closeModal={() => setArrestData((state) => ({ ...state, isOpen: false }))} | ||
/> | ||
</S.ChartDescription> | ||
<S.ChartSubsection showCompare={showCompare}> | ||
<ChartWrapper> | ||
<HorizontalBarChart | ||
title="Stop Counts With/Without Arrests" | ||
data={arrestData} | ||
pinMaxValue={false} | ||
xStacked | ||
yStacked | ||
tickStyle={null} | ||
stepSize={50000} | ||
tooltipLabelCallback={formatTooltipValue} | ||
modalConfig={{ | ||
tableHeader: 'Stop Counts With/Without Arrests', | ||
tableSubheader: getBarChartModalSubHeading( | ||
'Shows what number of stops led to an arrest for a given race / ethnic group' | ||
), | ||
agencyName, | ||
chartTitle: getBarChartModalSubHeading('Stop Counts With/Without Arrests'), | ||
}} | ||
/> | ||
</ChartWrapper> | ||
</S.ChartSubsection> | ||
</S.ChartSection> | ||
); | ||
} | ||
|
||
export default CountOfStopsAndArrests; |
Oops, something went wrong.