- Shows the number of searches performed by the {subjectObserving()}, broken down by search
- type and race / ethnicity.
+ Shows the number of searches performed {subjectObserving()}, broken down by search type
+ and race / ethnicity.
@@ -201,7 +209,7 @@ function Searches(props) {
modalConfig={{
tableHeader: 'Searches By Count',
tableSubheader: getLineChartModalSubHeading(
- `Shows the number of searches performed by the ${subjectObserving()}, broken down by search type and race / ethnicity`
+ `Shows the number of searches performed ${subjectObserving()}, broken down by search type and race / ethnicity`
),
agencyName: chartState.data[AGENCY_DETAILS].name,
chartTitle: getLineChartModalHeading('Searches By Count', true),
diff --git a/frontend/src/Components/Charts/TrafficStops/TrafficStops.js b/frontend/src/Components/Charts/TrafficStops/TrafficStops.js
index 65590ff2..0380ea9b 100644
--- a/frontend/src/Components/Charts/TrafficStops/TrafficStops.js
+++ b/frontend/src/Components/Charts/TrafficStops/TrafficStops.js
@@ -102,12 +102,16 @@ function TrafficStops(props) {
const renderMetaTags = useMetaTags();
const [renderTableModal, { openModal }] = useTableModal();
- const [stopPurposeGroupsData, setStopPurposeGroups] = useState({ labels: [], datasets: [] });
+ const [stopPurposeGroupsData, setStopPurposeGroups] = useState({
+ labels: [],
+ datasets: [],
+ loading: true,
+ });
const [stopsGroupedByPurposeData, setStopsGroupedByPurpose] = useState({
labels: [],
- safety: { labels: [], datasets: [] },
- regulatory: { labels: [], datasets: [] },
- other: { labels: [], datasets: [] },
+ safety: { labels: [], datasets: [], loading: true },
+ regulatory: { labels: [], datasets: [], loading: true },
+ other: { labels: [], datasets: [], loading: true },
max_step_size: null,
});
const groupedPieChartConfig = {
@@ -126,6 +130,7 @@ function TrafficStops(props) {
...groupedPieChartConfig,
},
],
+ loading: true,
},
regulatory: {
labels: groupedPieChartLabels,
@@ -135,6 +140,7 @@ function TrafficStops(props) {
...groupedPieChartConfig,
},
],
+ loading: true,
},
other: {
labels: groupedPieChartLabels,
@@ -144,6 +150,7 @@ function TrafficStops(props) {
...groupedPieChartConfig,
},
],
+ loading: true,
},
});
@@ -193,6 +200,7 @@ function TrafficStops(props) {
const [trafficStopsByCount, setTrafficStopsByCount] = useState({
labels: [],
datasets: [],
+ loading: true,
});
const createDateForRange = (yr) =>
@@ -252,6 +260,7 @@ function TrafficStops(props) {
}, []);
const buildPercentages = (data, ds) => {
+ if (!data.length) return [0, 0, 0, 0, 0, 0];
const dsTotal = data[ds].datasets
.map((s) => s.data.reduce((a, b) => a + b, 0))
.reduce((a, b) => a + b, 0);
@@ -279,7 +288,11 @@ function TrafficStops(props) {
.catch((err) => console.log(err));
}, []);
- const [stopsByPercentageData, setStopsByPercentageData] = useState({ labels: [], datasets: [] });
+ const [stopsByPercentageData, setStopsByPercentageData] = useState({
+ labels: [],
+ datasets: [],
+ loading: true,
+ });
useEffect(() => {
let url = `/api/agency/${agencyId}/stops-by-percentage/`;
@@ -436,12 +449,12 @@ function TrafficStops(props) {
const subjectObserving = () => {
if (officerId) {
- return 'officer';
+ return 'by this officer';
}
- if (agencyId) {
- return 'department';
+ if (agencyId === '-1') {
+ return 'for the entire state';
}
- return '';
+ return 'by this department';
};
const updateStopsByCount = (val) => {
@@ -528,7 +541,7 @@ function TrafficStops(props) {
const pieChartTitle = () => {
let subject = stopsChartState.data[AGENCY_DETAILS].name;
- if (subjectObserving() === 'officer') {
+ if (officerId) {
subject = `Officer ${officerId}`;
}
return `Traffic Stops By Percentage for ${subject} ${
@@ -538,12 +551,12 @@ function TrafficStops(props) {
const getPieChartModalSubHeading = (title) => {
const yearSelected = year && year !== 'All' ? ` in ${year}` : '';
- return `${title} by this ${subjectObserving()}${yearSelected}.`;
+ return `${title} ${subjectObserving()}${yearSelected}.`;
};
const getPieChartModalHeading = (stopPurpose) => {
let subject = stopsChartState.data[AGENCY_DETAILS].name;
- if (subjectObserving() === 'officer') {
+ if (officerId) {
subject = `Officer ${officerId}`;
}
return `Traffic Stops By ${stopPurpose} and Race Count for ${subject} ${
@@ -561,12 +574,12 @@ function TrafficStops(props) {
? ` for ${STOP_TYPES[trafficStopsByCountPurpose - 1]}`
: '';
}
- return `${title} by this ${subjectObserving()}${stopPurposeSelected}.`;
+ return `${title} ${subjectObserving()}${stopPurposeSelected}.`;
};
const getLineChartModalHeading = (title, showStopPurpose = false) => {
let subject = stopsChartState.data[AGENCY_DETAILS].name;
- if (subjectObserving() === 'officer') {
+ if (officerId) {
subject = `Officer ${officerId}`;
}
let stopPurposeSelected = '';
@@ -583,7 +596,7 @@ function TrafficStops(props) {
const stopsByPercentageModalTitle = () => {
let subject = stopsChartState.data[AGENCY_DETAILS].name;
- if (subjectObserving() === 'officer') {
+ if (officerId) {
subject = `Officer ${officerId}`;
}
return `Traffic Stops by Percentage for ${subject} since ${stopsByPercentageData.labels[0]}`;
@@ -601,8 +614,7 @@ function TrafficStops(props) {
/>
- Shows the race/ethnic composition of drivers stopped by this {subjectObserving()} over
- time.
+ Shows the race/ethnic composition of drivers stopped {subjectObserving()} over time.
{getChartDetailedBreakdown()}
@@ -616,7 +628,7 @@ function TrafficStops(props) {
tooltipLabelCallback={formatTooltipValue}
modalConfig={{
tableHeader: 'Traffic Stops By Percentage',
- tableSubheader: `Shows the race/ethnic composition of drivers stopped by this ${subjectObserving()} over time.`,
+ tableSubheader: `Shows the race/ethnic composition of drivers stopped ${subjectObserving()} over time.`,
agencyName: stopsChartState.data[AGENCY_DETAILS].name,
chartTitle: stopsByPercentageModalTitle(),
}}
diff --git a/frontend/src/Components/Charts/UseOfForce/UseOfForce.js b/frontend/src/Components/Charts/UseOfForce/UseOfForce.js
index 07b4d18f..413cd08c 100644
--- a/frontend/src/Components/Charts/UseOfForce/UseOfForce.js
+++ b/frontend/src/Components/Charts/UseOfForce/UseOfForce.js
@@ -36,7 +36,11 @@ function UseOfForce(props) {
const [year, setYear] = useState(YEARS_DEFAULT);
- const [useOfForceBarData, setUseOfForceBarData] = useState({ labels: [], datasets: [] });
+ const [useOfForceBarData, setUseOfForceBarData] = useState({
+ labels: [],
+ datasets: [],
+ loading: true,
+ });
const [useOfForcePieData, setUseOfForcePieData] = useState({
labels: pieChartLabels,
datasets: [
@@ -52,17 +56,17 @@ function UseOfForce(props) {
const subjectObserving = () => {
if (officerId) {
- return 'whom this officer';
+ return 'by this officer';
}
- if (agencyId) {
- return 'whom law enforcement officers';
+ if (agencyId === '-1') {
+ return 'for the entire state';
}
- return '';
+ return 'by this department';
};
useEffect(() => {
let url = `/api/agency/${agencyId}/use-of-force/`;
- if (officerId !== null) {
+ if (officerId) {
url = `${url}?officer=${officerId}`;
}
axios
@@ -119,7 +123,7 @@ function UseOfForce(props) {
const chartModalTitle = (displayYear = true) => {
let subject = chartState.data[AGENCY_DETAILS].name;
- if (subjectObserving() === 'officer') {
+ if (officerId) {
subject = `Officer ${officerId}`;
}
let yearOf = `since ${useOfForceBarData.labels[0]}`;
diff --git a/frontend/src/Components/Charts/chartUtils.js b/frontend/src/Components/Charts/chartUtils.js
index 5923a6f0..a434c565 100644
--- a/frontend/src/Components/Charts/chartUtils.js
+++ b/frontend/src/Components/Charts/chartUtils.js
@@ -69,9 +69,6 @@ export function reduceYearsToTotal(data, ethnicGroup) {
}));
}
-export function filterSinglePurpose(data, purpose) {
- return data.filter((d) => d.purpose === purpose);
-}
/**
* Given an Array of objects with shape { year, asian, black, etc. }, reduce to percentages of total by race.
* provide Theme object to provide fill colors.
diff --git a/frontend/src/Components/NewCharts/HorizontalBarChart.js b/frontend/src/Components/NewCharts/HorizontalBarChart.js
index 4d259467..2873e976 100644
--- a/frontend/src/Components/NewCharts/HorizontalBarChart.js
+++ b/frontend/src/Components/NewCharts/HorizontalBarChart.js
@@ -5,6 +5,7 @@ import { usePopper } from 'react-popper';
import { tooltipLanguage } from '../../util/tooltipLanguage';
import styled from 'styled-components';
import ChartModal from './ChartModal';
+import { EmptyMessage } from '../Charts/ChartSections/EmptyChartMessage';
export const Tooltip = styled.div`
background: #333;
@@ -132,10 +133,6 @@ export default function HorizontalBarChart({
popperElement.removeAttribute('data-show');
};
- if (!data.labels.length) {
- return ;
- }
-
const whiteBackground = {
id: 'customBarCanvasBackgroundColor',
beforeDraw: (chart, args, config) => {
@@ -167,6 +164,12 @@ export default function HorizontalBarChart({
const barChartModalPlugins = [whiteBackground];
const barChartModalOptions = createModalOptions(options);
+ if (data.loading) {
+ return ;
+ }
+
+ const noData = data.datasets.every((d) => d.data.every((v) => v === 0));
+
return (
<>
{displayStopPurposeTooltips && (
@@ -181,6 +184,7 @@ export default function HorizontalBarChart({
>
)}
+ {noData && }
;
}
@@ -182,6 +183,7 @@ export default function LineChart({
>
)}
+ {!data.labels.length && }
;
}
diff --git a/frontend/src/Components/NewCharts/VerticalBarChart.js b/frontend/src/Components/NewCharts/VerticalBarChart.js
index 48c52911..3b675dbc 100644
--- a/frontend/src/Components/NewCharts/VerticalBarChart.js
+++ b/frontend/src/Components/NewCharts/VerticalBarChart.js
@@ -2,6 +2,7 @@ import { Bar } from 'react-chartjs-2';
import DataLoading from '../Charts/ChartPrimitives/DataLoading';
import React, { useRef, useState } from 'react';
import ChartModal from './ChartModal';
+import { EmptyMessage } from '../Charts/ChartSections/EmptyChartMessage';
export default function VerticalBarChart({
data,
@@ -77,10 +78,6 @@ export default function VerticalBarChart({
const [isChartOpen, setIsChartOpen] = useState(false);
const zoomedLineChartRef = useRef(null);
- if (!data.labels.length) {
- return ;
- }
-
const whiteBackground = {
id: 'customVerticalBarCanvasBackgroundColor',
beforeDraw: (chart, args, config) => {
@@ -106,9 +103,16 @@ export default function VerticalBarChart({
const barChartModalPlugins = [whiteBackground];
const barChartModalOptions = createModalOptions(options);
+ if (data.loading) {
+ return ;
+ }
+
return (
<>
-
+
+ {!data.labels.length && }
+
+
setIsChartOpen(false)}
diff --git a/nc/urls.py b/nc/urls.py
index bfaae6ee..6a9a23ad 100755
--- a/nc/urls.py
+++ b/nc/urls.py
@@ -51,27 +51,27 @@
name="search-rate",
),
path(
- "api/agency//contraband/",
+ "api/agency//contraband/",
views.AgencyContrabandView.as_view(),
name="contraband-percentages",
),
path(
- "api/agency//contraband-types/",
+ "api/agency//contraband-types/",
views.AgencyContrabandTypesView.as_view(),
name="contraband-type-percentages",
),
path(
- "api/agency//contraband-stop-purpose/",
+ "api/agency//contraband-stop-purpose/",
views.AgencyContrabandStopPurposeView.as_view(),
name="contraband-percentages-stop-purpose-groups",
),
path(
- "api/agency//contraband-grouped-stop-purpose/",
+ "api/agency//contraband-grouped-stop-purpose/",
views.AgencyContrabandGroupedStopPurposeView.as_view(),
name="contraband-percentages-grouped-stop-purpose",
),
path(
- "api/agency//contraband-grouped-stop-purpose/modal/",
+ "api/agency//contraband-grouped-stop-purpose/modal/",
views.AgencyContrabandStopGroupByPurposeModalView.as_view(),
name="contraband-percentages-grouped-stop-purpose-modal",
),
diff --git a/nc/views.py b/nc/views.py
index 6f0f4b0d..153dad09 100644
--- a/nc/views.py
+++ b/nc/views.py
@@ -544,6 +544,9 @@ def get(self, request, agency_id):
if officer:
qs = qs.filter(officer_id=officer)
+ if qs.count() == 0:
+ return Response(data={"labels": [], "datasets": []}, status=200)
+
if date_precision == "year":
qs = qs.annotate(year=ExtractYear("date"))
else:
@@ -556,8 +559,6 @@ def get(self, request, agency_id):
qs_values = [date_precision] + qs_df_cols
qs = qs.values(*qs_values).annotate(count=Sum("count")).order_by(date_precision)
- if qs.count() == 0:
- return Response(data={"labels": [], "datasets": []}, status=200)
df = pd.DataFrame(qs)
unique_x_range = df[date_precision].unique()
pivot_df = df.pivot(index=date_precision, columns=qs_df_cols, values="count").fillna(
@@ -691,7 +692,16 @@ def get(self, request, agency_id):
.order_by("year")
)
if qs.count() == 0:
- return Response(data={"labels": [], "datasets": []}, status=200)
+ return Response(
+ data={
+ "labels": [],
+ "safety": {"labels": [], "datasets": []},
+ "regulatory": {"labels": [], "datasets": []},
+ "other": {"labels": [], "datasets": []},
+ "max_step_size": 0,
+ },
+ status=200,
+ )
df = pd.DataFrame(qs)
unique_years = df.year.unique()
pivot_table = pd.pivot_table(
@@ -1308,9 +1318,13 @@ def get(self, request, agency_id):
total_search = 0
total_stop = 0
for c in columns:
- total_search += search_df[c][year]
- total_stop += stops_df[c][year]
- search_df[c][year] = search_df[c][year] / stops_df[c][year]
+ if c in search_df and c in stops_df:
+ total_search += search_df[c][year] or 0
+ total_stop += stops_df[c][year] or 0
+ try:
+ search_df[c][year] = float(search_df[c][year]) / float(stops_df[c][year])
+ except (ValueError, ZeroDivisionError):
+ search_df[c][year] = 0
search_df["Average"][year] = total_search / total_stop
data = self.build_response(search_df, unique_x_range)
@@ -1473,14 +1487,14 @@ def get(self, request, agency_id):
search_qs = search_qs.filter(year=year)
stop_qs = stop_qs.filter(year=year)
+ if search_qs.count() == 0:
+ return Response(data={"labels": [], "datasets": []}, status=200)
+
search_qs = search_qs.values("stop_purpose", "driver_race_comb").annotate(
count=Sum("count")
)
stop_qs = stop_qs.values("stop_purpose", "driver_race_comb").annotate(count=Sum("count"))
- if search_qs.count() == 0:
- return Response(data={"labels": [], "datasets": []}, status=200)
-
search_df = pd.DataFrame(search_qs)
stops_df = pd.DataFrame(stop_qs)