Skip to content

Commit

Permalink
Feature: Rent Stabilized Units Indicator (#838)
Browse files Browse the repository at this point in the history
* Add url params for timeline indicators (#830)

* add indicator param to timeline route, and update history shallow on selection

* fix optional route definition

* replace url rather than adding to history

* swap summary for timeline in route tests

* fix links to timeline, reorg helper funs

* reset url to default indicator if param is invalid or missing

* remove console.log

---------

Co-authored-by: kiwan <kiwan.sim@gmail.com>

* Add Rent Stabilized Units indicator to Timeline (#837)

* change rent stab latest year to 2022

* add 2022 as rsunitslatestyear, update yml dependenc, add csv for tests

* update nycdb src branc

* add nycdb test data for rentstab and hpd_complaints

* add rentstab indicator to timeline

* update MonthlyTimelineData type to match SQL

* remove stacked bar denoting market rate units, add horizontal line for latest total unit count

* add link to THE CITY's map visualization

* run prettier

* fussing around with viz UI

* change minor variable to trigger deploy

* yaxis max handled in switch statement

* run lingui extract

---------

Co-authored-by: Maxwell Austensen <m.austensen@gmail.com>

---------

Co-authored-by: Maxwell Austensen <maxwell@justfix.org>
Co-authored-by: Maxwell Austensen <m.austensen@gmail.com>
  • Loading branch information
3 people authored Dec 8, 2023
1 parent d9e312f commit 2f1055f
Show file tree
Hide file tree
Showing 18 changed files with 622 additions and 281 deletions.
1 change: 1 addition & 0 deletions client/src/components/APIDataTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export type MonthlyTimelineData = {
dobviolations_ecb: number;
dobviolations_total: number;
evictionfilings_total: number;
rentstabilizedunits_total: number;
};

export type IndicatorsHistoryResults = {
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/DetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { isPartOfGroupSale } from "./PortfolioTable";
import { Link, useLocation } from "react-router-dom";
import { LocaleLink } from "../i18n";
import BuildingStatsTable from "./BuildingStatsTable";
import { createWhoOwnsWhatRoutePaths, AddressPageRoutes } from "../routes";
import { createWhoOwnsWhatRoutePaths, AddressPageRoutes, removeIndicatorSuffix } from "../routes";
import { defaultLocale, SupportedLocale } from "../i18n-base";
import { withMachineInStateProps } from "state-machine";
import { Accordion } from "./Accordion";
Expand Down Expand Up @@ -285,7 +285,7 @@ class DetailViewWithoutI18n extends Component<Props, State> {
<BuildingStatsTable addr={detailAddr} />
<div className="card-body-timeline-link">
<Link
to={this.props.addressPageRoutes.timeline}
to={removeIndicatorSuffix(this.props.addressPageRoutes.timeline)}
className="btn btn-primary btn-block"
onClick={() => {
window.gtag("event", "view-data-over-time-overview-tab");
Expand Down
41 changes: 37 additions & 4 deletions client/src/components/Indicators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
import { NetworkErrorMessage } from "./NetworkErrorMessage";
import { Dropdown } from "./Dropdown";
import { AmplitudeEvent, logAmplitudeEvent } from "./Amplitude";
import { withRouter } from "react-router-dom";
import { RouteComponentProps } from "react-router";
import { AddressPageUrlParams, removeIndicatorSuffix } from "routes";

type TimeSpanTranslationsMap = {
[K in IndicatorsTimeSpan]: (i18n: I18n) => string;
Expand All @@ -49,10 +52,26 @@ const getDropdownWidthFromLongestSelection = (selections: string[]) => {
return Math.min(lengthOfLongestSelection * LETTER_WIDTH + MENU_BUFFER, MAX_WIDTH);
};

class IndicatorsWithoutI18n extends Component<IndicatorsProps, IndicatorsState> {
constructor(props: IndicatorsProps) {
type IndicatorsWithRouterProps = RouteComponentProps<AddressPageUrlParams> & IndicatorsProps;

export const validateIndicatorParam = (indicatorParam?: string) => {
const indicator = indicatorParam as IndicatorsDatasetId;
if (indicatorsDatasetIds.includes(indicator)) {
return indicator;
}
};

class IndicatorsWithoutI18n extends Component<IndicatorsWithRouterProps, IndicatorsState> {
constructor(props: IndicatorsWithRouterProps) {
super(props);
this.state = indicatorsInitialState;
const indicator =
validateIndicatorParam(props.match.params.indicator) || indicatorsInitialState.defaultVis;
this.state = {
...indicatorsInitialState,
activeVis: indicator,
defaultVis: indicator,
activeTimeSpan: indicator === "rentstabilizedunits" ? "year" : "quarter",
};
this.handleVisChange = this.handleVisChange.bind(this);
}

Expand Down Expand Up @@ -95,10 +114,20 @@ class IndicatorsWithoutI18n extends Component<IndicatorsProps, IndicatorsState>
}
}

setUrlIndicator(indicator: IndicatorsDatasetId) {
const timelinePath = removeIndicatorSuffix(this.props.addressPageRoutes.timeline);
this.props.history.replace(`${timelinePath}/${indicator}`);
}

handleVisChange(selectedVis: IndicatorsDatasetId) {
if (selectedVis === "rentstabilizedunits") {
this.handleTimeSpanChange("year");
}

this.setState({
activeVis: selectedVis,
});
this.setUrlIndicator(selectedVis);
}

/** Changes viewing timespan to be by 'year', 'quarter', or 'month' */
Expand Down Expand Up @@ -133,6 +162,9 @@ class IndicatorsWithoutI18n extends Component<IndicatorsProps, IndicatorsState>

this.updateData();

if (this.props.isVisible && !this.props.location.pathname.includes(this.state.activeVis)) {
this.setUrlIndicator(this.state.activeVis);
}
const newlyLoadedRawData =
!prevProps.state.matches({ portfolioFound: { timeline: "success" } }) &&
state.matches({ portfolioFound: { timeline: "success" } }) &&
Expand Down Expand Up @@ -289,6 +321,7 @@ class IndicatorsWithoutI18n extends Component<IndicatorsProps, IndicatorsState>
name={timespan}
checked={this.state.activeTimeSpan === timespan ? true : false}
onChange={() => this.handleTimeSpanChange(timespan)}
disabled={activeVis === "rentstabilizedunits" && timespan !== "year"}
/>
<i className="form-icon" /> {timeSpanTranslations[timespan](i18n)}
</label>
Expand Down Expand Up @@ -370,5 +403,5 @@ class IndicatorsWithoutI18n extends Component<IndicatorsProps, IndicatorsState>
}
}

const Indicators = withI18n()(IndicatorsWithoutI18n);
const Indicators = withRouter(withI18n()(IndicatorsWithoutI18n));
export default Indicators;
65 changes: 64 additions & 1 deletion client/src/components/IndicatorsDatasets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export interface IndicatorsDataset {
| "hpdviolations"
| "dobpermits"
| "dobviolations"
| "evictionfilings";
| "evictionfilings"
| "rentstabilizedunits";

/**
* The localized name for a particular "quantity" of the dataset, e.g.
Expand Down Expand Up @@ -238,6 +239,7 @@ export const INDICATORS_DATASETS: IndicatorsDatasetMap = {
</a>
) and not evictions carried out by NYC Marshals.
<br />
<br />
If you or someone you know is facing eviction and want to learn more about your rights, head
over to{" "}
<a
Expand All @@ -249,11 +251,72 @@ export const INDICATORS_DATASETS: IndicatorsDatasetMap = {
</a>
.
<br />
<br />
Due to privacy restrictions on the use of these data, eviction filings cannot be shown for
buildings with fewer than 11 units.
</Trans>
),
},
rentstabilizedunits: {
name: (i18n) => i18n._(t`Rent Stabilized Units`),
analyticsName: "rentstabilizedunits",
quantity: (i18n, value) => i18n._("Rent Stabilized Units registered since 2010"),
yAxisLabel: (i18n) => i18n._(t`Number of Units`),
explanation: () => (
<Trans render="span">
<a
href="https://rentguidelinesboard.cityofnewyork.us/resources/faqs/rent-stabilization/"
target="_blank"
rel="noopener noreferrer"
>
Rent stabilization
</a>{" "}
protects tenants by limiting rent increases and providing the right to lease renewals.
Landlords register rent-stabilized units each year with NYS Homes and Community Renewal
(HCR). Though the agency does not directly make this data available, the number of
registered rent-stabilized units appears on public city property tax bills. JustFix and
open-source community projects have extracted these numbers to compile a{" "}
<a
href="https://github.com/nycdb/nycdb/wiki/Dataset:-Rent-Stabilized-Buildings#provenance"
target="_blank"
rel="noopener noreferrer"
>
dataset of building-level counts of rent-stabilized units
</a>{" "}
from 2007 to 2022.
<br />
<br />A significant limitation of the data is that{" "}
<a
href="https://projects.thecity.nyc/rent-stabilized-map/"
target="_blank"
rel="noopener noreferrer"
>
landlords will sometimes fail to register the units
</a>{" "}
or do so late, and in the tax bills, it appears there are no rent-stabilized units. For this
reason, you may see a sudden drop of registered units to zero, but this doesn’t necessarily
reflect an actual loss of stabilized units. If you see a gradual decline in the number of
stabilized units that is more likely to represent a true destabilization of units,
especially if before 2019 when the passage of the Housing Stability and Tenant Protection
Act of 2019 (HSTPA) greatly limited the ways units could be legally destabilized. Even when
units are actually destabilized by landlords it does not mean that this was done legally.
The only way to know for sure whether your apartment is rent stabilized, was illegally
destabilized, or if you are being overcharged is to{" "}
<a href="https://app.justfix.org/rh" target="_blank" rel="noopener noreferrer">
request your rent history
</a>{" "}
from HCR. Once you receive it, you can use{" "}
<a
href="https://www.justfix.org/en/learn/rent-history-101"
target="_blank"
rel="noopener noreferrer"
>
this Learning Center article
</a>{" "}
to guide you in reading your rent history document.
</Trans>
),
},
};

const IndicatorsDatasetRadioWithoutI18n: React.FC<{
Expand Down
15 changes: 15 additions & 0 deletions client/src/components/IndicatorsTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const indicatorsDatasetIds = [
"dobpermits",
"dobviolations",
"evictionfilings",
"rentstabilizedunits",
] as const;
export type IndicatorsDatasetId = typeof indicatorsDatasetIds[number];

Expand Down Expand Up @@ -77,6 +78,12 @@ interface EvictionFilingsData extends IndicatorsData {
};
}

interface RentStabilizedUnitsData extends IndicatorsData {
values: {
total: number[] | null;
};
}

export type IndicatorsDataIndex = {
[k in IndicatorsDatasetId]: IndicatorsData;
};
Expand All @@ -87,6 +94,7 @@ export type IndicatorsDataFromAPI = IndicatorsDataIndex & {
dobpermits: DobPermitsData;
dobviolations: DobViolationsData;
evictionfilings: EvictionFilingsData;
rentstabilizedunits: RentStabilizedUnitsData;
};

export const indicatorsInitialDataStructure: IndicatorsDataFromAPI = {
Expand Down Expand Up @@ -132,6 +140,13 @@ export const indicatorsInitialDataStructure: IndicatorsDataFromAPI = {
total: null,
},
},

rentstabilizedunits: {
labels: null,
values: {
total: null,
},
},
};

// Types Relating to the State of the Indicators Component:
Expand Down
Loading

0 comments on commit 2f1055f

Please sign in to comment.