Skip to content

Commit

Permalink
feat: default all totals panel
Browse files Browse the repository at this point in the history
  • Loading branch information
pjsier committed Nov 5, 2024
1 parent cedc912 commit d6cede6
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 16 deletions.
40 changes: 40 additions & 0 deletions client/src/actions/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const UPDATE_DETAILED_SEARCH = "UPDATE_DETAILED_SEARCH"
export const UPDATE_SEARCH_BAR = "UPDATE_SEARCH_BAR"
export const UPDATE_VIEWER_POSITION = "UPDATE_VIEWER_POSITION"
export const GET_DOWNLOAD_DATA = "GET_DOWNLOAD_DATA"
export const UPDATE_ALL_TOTALS = "UPDATE_ALL_TOTALS"

/* General action to set search state
use this sparingly as the action
Expand Down Expand Up @@ -56,6 +57,13 @@ function getViewerPosition(viewerCoords) {
}
}

function updateAllTotals(allTotals) {
return {
type: UPDATE_ALL_TOTALS,
payload: { ...allTotals },
}
}

export function handlePrimarySearchQuery(
{ searchType, searchTerm, searchCoordinates, searchYear },
route
Expand Down Expand Up @@ -174,3 +182,35 @@ export function handleGetViewerPosition(coords) {
}
}
}

export function handleAllTotalsQuery(year) {
return async (dispatch) => {
try {
const res = await APISearchQueryFromRoute(
`/api/general?type=all-totals&year=${year}`
)
const yearRecord = res.speculationByYear.find(
({ year: recYear }) => +year == recYear
)

dispatch(
updateAllTotals({
timelineData: res.speculationByYear
.map(({ year: recYear, count }) => ({
x: +recYear,
y: +count,
}))
.sort((a, b) => a.recYear - b.recYear),
totalSpeculators: +yearRecord.speculator_count,
totalParcels: +yearRecord.count,
topSpeculators: res.topSpeculators,
})
)
} catch (err) {
dispatch(triggerFetchError(true))
console.error(
`An error occured searching search bar years. Message: ${err}`
)
}
}
}
120 changes: 120 additions & 0 deletions client/src/components/Search/AllParcels.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from "react"
import { useSelector } from "react-redux"
import { Link } from "react-router-dom"
import {
capitalizeFirstLetter,
createQueryStringFromParams,
} from "../../utils/helper"
import {
VictoryAxis,
VictoryChart,
VictoryLine,
VictoryScatter,
VictoryTheme,
VictoryTooltip,
} from "victory"
import { DEFAULT_YEAR, MINIMUM_YEAR } from "../../utils/constants"
import questionMarkRose from "../../assets/img/question_mark_rose.svg"
import mapMarkerRose from "../../assets/img/map_marker_rose.svg"
import infoIcon from "../../assets/img/info-icon.png"

function AllParcels(props) {
const year = props.queryParams?.year || DEFAULT_YEAR
const { timelineData, totalSpeculators, totalParcels, topSpeculators } =
useSelector((state) => state.searchState.allTotals)
const { drawerIsOpen } = useSelector(
(state) => state.searchState.detailedSearch
)

return (
totalSpeculators && (
<div className="results-inner scroller">
<div style={drawerIsOpen ? { display: "block" } : { display: "none" }}>
<div className="detailed-title">
<img src={mapMarkerRose} alt="A map marker icon" />
<span>All Speculators {year}</span>
</div>
<div className="detailed-properties">
<p>
<span>{totalSpeculators}</span> speculators owned {totalParcels}{" "}
properties in Detroit in the year <span>{` ${year}. `}</span>
</p>
</div>
<VictoryChart
theme={VictoryTheme.material}
domainPadding={5}
padding={{ top: 25, bottom: 50, left: 60, right: 10 }}
domain={{
x: [+MINIMUM_YEAR, +DEFAULT_YEAR],
y: [
0,
Math.max(...(timelineData || [{ y: 10 }]).map(({ y }) => y)),
],
}}
>
<VictoryAxis tickFormat={(tick) => `${tick}`} />
<VictoryAxis tickFormat={(tick) => `${tick}`} dependentAxis />
<VictoryLine
style={{
data: { stroke: "#e4002c", strokeWidth: 3 },
parent: { border: "1px solid #ccc" },
}}
data={timelineData}
/>
<VictoryScatter
style={{
data: { fill: "#e4002c", strokeWidth: 35 },
}}
size={4}
data={(timelineData || []).map((datum) => ({
...datum,
label: datum.y,
}))}
labelComponent={<VictoryTooltip />}
/>
</VictoryChart>
<div className="detailed-title">
<img src={questionMarkRose} alt="A question mark icon" />
<span>Top 10 Speculators</span>
</div>
<div className="detailed-zipcode">
{topSpeculators.slice(0, 10).map((record, index) => {

Check failure on line 81 in client/src/components/Search/AllParcels.jsx

View workflow job for this annotation

GitHub Actions / client

'index' is defined but never used
return (
<div className="speculator-item" key={record.own_id}>
<div>
<Link
to={createQueryStringFromParams(
{
type: "speculator",
ownid: record.own_id,
coordinates: null,
year,
},
"/map"
)}
>
<span
title={`Search ${capitalizeFirstLetter(
record.own_id
)}'s properties`}
>
<img src={infoIcon} alt="More Information"></img>
{capitalizeFirstLetter(record.own_id)}
</span>
</Link>
</div>
<div>
<div>{`${record.count} properties`}</div>
<div>{`${Math.round(record.per)}% ownership`}</div>
</div>
</div>
)
})}
</div>
</div>
</div>
)
)
}

export default AllParcels
35 changes: 23 additions & 12 deletions client/src/components/Search/DetailedResultsContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { useSelector, useDispatch } from "react-redux"
import { withRouter } from "../../utils/router"
import { CSSTransition } from "react-transition-group"
import queryString from "query-string"
import { updateDetailedSearch } from "../../actions/search"
import {
handleAllTotalsQuery,
updateDetailedSearch,
} from "../../actions/search"
import { getDetailsFromGeoJSON } from "../../utils/helper"
import DetailedSearchResults from "./DetailedSearchResults"
import { DEFAULT_YEAR } from "../../utils/constants"

function useQueryParams(props) {
const { searchQuery } = props
Expand All @@ -22,26 +26,33 @@ function useQueryParams(props) {

function DetailedResultsContainer() {
const { ppraxis } = useSelector((state) => state.mapData)
const { drawerIsOpen, results, resultsType } = useSelector(
const { drawerIsOpen, resultsType } = useSelector(
(state) => state.searchState.detailedSearch
)
const { totalSpeculators } = useSelector(
(state) => state.searchState.allTotals
)
const { details, detailsCount, detailsZip, detailsType } =
getDetailsFromGeoJSON(ppraxis)
const dispatch = useDispatch()

const queryParams = useQueryParams({ searchQuery: window.location.search })
useEffect(() => {
dispatch(
updateDetailedSearch({
results: details,
resultsZip: detailsZip,
resultsCount: detailsCount,
resultsType: detailsType,
})
)
if (!resultsType && !totalSpeculators) {
dispatch(handleAllTotalsQuery(queryParams?.year || DEFAULT_YEAR))
} else {
dispatch(
updateDetailedSearch({
results: details,
resultsZip: detailsZip,
resultsCount: detailsCount,
resultsType: detailsType,
})
)
}
}, [JSON.stringify(details), detailsZip, detailsCount, detailsType])

if (results && resultsType && queryParams) {
if (resultsType || totalSpeculators) {
return (
<CSSTransition
in={drawerIsOpen} //set false on load
Expand All @@ -64,7 +75,7 @@ function DetailedResultsContainer() {
}
>
<DetailedSearchResults
detailsType={detailsType}
detailsType={!resultsType ? null : detailsType}
queryParams={queryParams}
/>
</CSSTransition>
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/Search/DetailedSearchResults.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "../../utils/helper"
import MapViewerV4 from "./MapViewerV4"
import TimeGraph from "./TimeGraph"
import AllParcels from "./AllParcels"
import infoIcon from "../../assets/img/info-icon.png"
import { APISearchQueryFromRoute } from "../../utils/api"
import mapMarkerRose from "../../assets/img/map_marker_rose.svg"
Expand Down Expand Up @@ -296,7 +297,7 @@ function ContentSwitch({ detailsType, queryParams }) {
} else if (results && results.length === 0) {
return <NoResults />
} else {
return <div>ERROR</div>
return <AllParcels queryParams={queryParams} />
}
}

Expand Down
4 changes: 1 addition & 3 deletions client/src/components/Search/SearchBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,9 @@ class SearchBar extends Component {
updateDetailedSearch({
results: null,
resultsType: null,
drawerIsOpen: false,
contentIsVisible: false,
})
)
this.props.router?.navigate(`/map`)
this.props.router?.navigate(`/map?`)
}

_handleYearSelect = (e) => {
Expand Down
17 changes: 17 additions & 0 deletions client/src/reducers/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
UPDATE_PRIMARY_SEARCH,
UPDATE_DETAILED_SEARCH,
UPDATE_SEARCH_BAR,
UPDATE_ALL_TOTALS,
} from "../actions/search"
import { DEFAULT_YEAR } from "../utils/constants"

Expand All @@ -28,6 +29,12 @@ const initialSearchState = {
resultsType: null,
recordYears: null,
},
allTotals: {
timelineData: [],
totalSpeculators: null,
totalParcels: null,
topSpeculators: [],
},
downloadData: null,
viewerCoords: {
lat: null,
Expand Down Expand Up @@ -72,6 +79,16 @@ export default function searchState(state = initialSearchState, action) {
...state,
detailedSearch: { ...state.detailedSearch, ...action.payload },
}
case UPDATE_ALL_TOTALS:
return {
...state,
allTotals: { ...state.allTotals, ...action.payload },
detailedSearch: {
...state.detailedSearch,
contentIsVisible: true,
drawerIsOpen: true,
},
}
default:
return state
}
Expand Down
49 changes: 49 additions & 0 deletions server/routes/generalDBSearch.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,57 @@
const Router = require("express-promise-router")
const SQL = require("sql-template-strings")
const router = new Router()
const queries = require("../utils/queries")
const db = require("../db")

/* This route covers all requests that
are not part of the primary or detailed
search groups. */

async function queryAllTotals(year) {
const { rows: speculationByYear } = await db.query(SQL`
SELECT
year,
SUM(count) AS count,
(COUNT(*) FILTER (WHERE own_id IS NOT NULL)) AS speculator_count
FROM owner_count
GROUP BY year
ORDER BY year ASC
`)

const { rows: topSpeculators } = await db.query(SQL`
SELECT
p.own_id,
COUNT(*) AS count,
COUNT(*) AS total,
(COUNT(*) * 100.0 / total.total_parcel_count) AS per
FROM
parcels p
JOIN (
SELECT
year,
COUNT(*) AS total_parcel_count
FROM
parcels
WHERE
year = ${year}
GROUP BY
year
) total ON p.year = total.year
WHERE
p.year = ${year}
GROUP BY
p.own_id, p.year, total.total_parcel_count
ORDER BY
total DESC
LIMIT 10;
`)
return {
speculationByYear,
topSpeculators,
}
}

router.get("/", async (req, res) => {
try {
const { type = null, year = null, code = null } = req.query
Expand All @@ -25,6 +71,9 @@ router.get("/", async (req, res) => {
})
clientData = Object.values(pgData.data[0])
break
case "all-totals":
clientData = await queryAllTotals(year)
break
default:
clientData = null
break
Expand Down

0 comments on commit d6cede6

Please sign in to comment.