From 8af8b0f8a24ff70452d4cf99c599d9a4e5a21f83 Mon Sep 17 00:00:00 2001 From: Kavi Gupta Date: Tue, 13 Aug 2024 22:30:28 -0400 Subject: [PATCH] migrate template to typescript, migrate mapper panel using luke's changes --- react/src/about.js | 2 +- react/src/components/article-panel.js | 2 +- react/src/components/comparison-panel.js | 2 +- react/src/components/header.tsx | 8 +- react/src/components/mapper-panel.tsx | 281 +++++++++--------- react/src/components/quiz-panel.js | 2 +- react/src/components/screenshot.tsx | 2 +- react/src/components/statistic-panel.js | 2 +- react/src/data-credit.js | 2 +- .../{template.js => template.tsx} | 42 ++- 10 files changed, 173 insertions(+), 172 deletions(-) rename react/src/page_template/{template.js => template.tsx} (75%) diff --git a/react/src/about.js b/react/src/about.js index 07aa3fcc4..4c125aec9 100644 --- a/react/src/about.js +++ b/react/src/about.js @@ -3,7 +3,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import "./style.css"; import "./common.css"; -import { PageTemplate } from "./page_template/template.js"; +import { PageTemplate } from "./page_template/template"; import { headerTextClass } from './utils/responsive'; diff --git a/react/src/components/article-panel.js b/react/src/components/article-panel.js index a36da2135..d2ba7792b 100644 --- a/react/src/components/article-panel.js +++ b/react/src/components/article-panel.js @@ -5,7 +5,7 @@ import React, { useRef } from 'react'; import { StatisticRowRaw } from "./table"; import { Map } from "./map"; import { Related } from "./related-button"; -import { PageTemplate } from "../page_template/template.js"; +import { PageTemplate } from "../page_template/template"; import "../common.css"; import "./article.css"; import { load_article } from './load-article'; diff --git a/react/src/components/comparison-panel.js b/react/src/components/comparison-panel.js index 1c3615fbd..7630b8e5b 100644 --- a/react/src/components/comparison-panel.js +++ b/react/src/components/comparison-panel.js @@ -4,7 +4,7 @@ import React from 'react'; import { StatisticRowRaw, StatisticRowRawCellContents, StatisticRow } from "./table"; import { MapGeneric } from "./map"; -import { PageTemplate, PageTemplateClass } from "../page_template/template.js"; +import { PageTemplate, PageTemplateClass } from "../page_template/template"; import "../common.css"; import "./article.css"; import { load_article } from './load-article'; diff --git a/react/src/components/header.tsx b/react/src/components/header.tsx index cabf53174..df96e22d9 100644 --- a/react/src/components/header.tsx +++ b/react/src/components/header.tsx @@ -16,7 +16,7 @@ export function Header(props: { hamburger_open: boolean, set_hamburger_open: (newValue: boolean) => void, has_universe_selector: boolean, - all_universes: string[], + all_universes?: string[], has_screenshot: boolean, screenshot_mode: boolean, initiate_screenshot: (curr_universe: string) => void @@ -36,7 +36,7 @@ export function Header(props: { {!mobileLayout() && props.has_universe_selector ?
: undefined} @@ -79,7 +79,7 @@ function TopLeft(props: { hamburger_open: boolean, set_hamburger_open: (newValue: boolean) => void, has_universe_selector: boolean, - all_universes: string[], + all_universes?: string[], }) { if (mobileLayout()) { return ( @@ -89,7 +89,7 @@ function TopLeft(props: { { props.has_universe_selector ? : } diff --git a/react/src/components/mapper-panel.tsx b/react/src/components/mapper-panel.tsx index 8296239c3..24ed9a5f1 100644 --- a/react/src/components/mapper-panel.tsx +++ b/react/src/components/mapper-panel.tsx @@ -1,24 +1,43 @@ -export { MapperPanel }; - -import React from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Statistic } from "./table"; -import { MapGeneric } from "./map"; -import { PageTemplateClass } from "../page_template/template.js"; +import { MapGeneric, MapGenericProps } from "./map"; +import { PageTemplate } from "../page_template/template"; import "../common.css"; import "./article.css"; import { loadProtobuf } from '../load_json'; import { consolidated_shape_link, consolidated_stats_link } from '../navigation/links'; import { interpolate_color } from '../utils/color'; -import { parse_ramp } from "../mapper/ramps"; -import { MapperSettings, default_settings, parse_color_stat } from "../mapper/settings"; +import { Keypoints, Ramp, parse_ramp } from "../mapper/ramps"; +import { ColorStat, ColorStatDescriptor, LineStyle, MapSettings, MapperSettings, StatisticsForGeography, default_settings, parse_color_stat } from "../mapper/settings"; import { gunzipSync, gzipSync } from "zlib"; +import { NormalizeProto } from "../utils/types"; +import { AllStats, Feature } from "../utils/protos"; import { headerTextClass } from '../utils/responsive'; -class DisplayedMap extends MapGeneric { - constructor(props) { +interface EmpiricalRamp { + ramp: Keypoints, interpolations: number[] +} + +interface Filter { enabled: boolean, function: ColorStatDescriptor } + +interface DisplayedMapProps extends MapGenericProps { + underlying_shapes: Promise<{ longnames: string[], shapes: NormalizeProto[] }>; + line_style: LineStyle; + underlying_stats: Promise<{ stats: StatisticsForGeography, longnames: string[] }>; + filter?: ColorStat; + color_stat: ColorStat; + ramp: Ramp; + ramp_callback: (ramp: EmpiricalRamp) => void; +} + +class DisplayedMap extends MapGeneric { + + private name_to_index?: Record + + constructor(props: DisplayedMapProps) { super(props); this.name_to_index = undefined; } @@ -27,15 +46,15 @@ class DisplayedMap extends MapGeneric { if (this.name_to_index === undefined) { const result = (await this.props.underlying_shapes).longnames; this.name_to_index = {}; - for (let i in result) { + for (let i = 0; i < result.length; i++) { this.name_to_index[result[i]] = i; } } } - async loadShape(name) { + async loadShape(name: string) { await this.guarantee_name_to_index(); - const index = this.name_to_index[name]; + const index = this.name_to_index![name]; const data = (await this.props.underlying_shapes).shapes[index]; return data; } @@ -76,13 +95,13 @@ class DisplayedMap extends MapGeneric { }) ); const metas = stat_vals.map((x) => { return { statistic: x } }); - return [names, styles, metas, -1]; + return [names, styles, metas, -1] as const; } async mapDidRender() { // zoom map to fit united states // do so instantly - this.map.fitBounds([ + this.map!.fitBounds([ [49.3457868, -124.7844079], [24.7433195, -66.9513812] ], { animate: false }); @@ -90,7 +109,7 @@ class DisplayedMap extends MapGeneric { } -function Colorbar(props) { +function Colorbar(props: { ramp?: EmpiricalRamp, name: string }) { // do this as a table with 10 columns, each 10% wide and // 2 rows. Top one is the colorbar, bottom one is the // labels. @@ -103,8 +122,7 @@ function Colorbar(props) { const range = max - min; const values = props.ramp.interpolations; - - const create_value = (stat) => { + const create_value = (stat: number) => { return
} - return (
@@ -130,7 +147,7 @@ function Colorbar(props) { @@ -155,11 +172,22 @@ function Colorbar(props) { ); } -function MapComponent(props) { +type MapComponentProps = { + name_to_index: Record, + color_stat: ColorStatDescriptor | undefined, + filter: Filter, + settings: MapSettings; + height?: string; + map_ref: React.RefObject +} & Pick + +function MapComponent(props: MapComponentProps) { const color_stat = parse_color_stat(props.name_to_index, props.color_stat); const filter = props.filter.enabled ? parse_color_stat(props.name_to_index, props.filter.function) : undefined; + const [empiricalRamp, setEmpiricialRamp] = useState(undefined); + return (
props.set_empirical_ramp(ramp)} + ramp_callback={setEmpiricialRamp} ref={props.map_ref} line_style={props.line_style} basemap={props.basemap} @@ -185,14 +212,14 @@ function MapComponent(props) {
) } -function saveAsFile(filename, data, type) { +function saveAsFile(filename: string, data: BlobPart, type: string) { const blob = new Blob([data], { type: type }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); @@ -203,7 +230,8 @@ function saveAsFile(filename, data, type) { document.body.removeChild(link); } -function Export(props) { +function Export(props: { map_ref: React.RefObject }) { + const exportAsSvg = async () => { if (props.map_ref.current === null) { return; @@ -220,13 +248,10 @@ function Export(props) { saveAsFile("map.geojson", geojson, "application/geo+json"); } + return
- - + +
} -function mapSettingsFromURLParams() { +function mapSettingsFromURLParams(): MapSettings { const params = new URLSearchParams(window.location.search); const encoded_settings = params.get("settings"); - var settings = {} + let settings = {} if (encoded_settings !== null) { const jsoned_settings = gunzipSync(Buffer.from(encoded_settings, 'base64')).toString(); settings = JSON.parse(jsoned_settings); } - default_settings(settings); - return settings; + return default_settings(settings); } -class MapperPanel extends PageTemplateClass { - constructor(props) { - super(props); - this.names = require("../data/statistic_name_list.json"); - this.valid_geographies = require("../data/mapper/used_geographies.json"); - this.name_to_index = {}; - for (let i in this.names) { - this.name_to_index[this.names[i]] = i; - } +export function MapperPanel() { - const map_settings = mapSettingsFromURLParams(); + const names = useMemo(() => require("../data/statistic_name_list.json") as string[], []); + const valid_geographies = useMemo(() => require("../data/mapper/used_geographies.json"), []); - this.state = { - ...this.state, - map_settings: map_settings - }; - this.geography_kind = undefined; - this.underlying_shapes = undefined; - this.underlying_stats = undefined; - this.map_ref = React.createRef(); - } + const name_to_index = Object.fromEntries(names.map((name, i) => [name, i])) - update_geography_kind() { - const geography_kind = this.state.map_settings.geography_kind; - if (this.geography_kind !== geography_kind) { - this.geography_kind = geography_kind; - - if (this.valid_geographies.includes(geography_kind)) { - this.underlying_shapes = loadProtobuf( - consolidated_shape_link(this.geography_kind), - "ConsolidatedShapes" - ); - this.underlying_stats = loadProtobuf( - consolidated_stats_link(this.geography_kind), - "ConsolidatedStatistics" - ); + const [map_settings, set_map_settings] = useState(mapSettingsFromURLParams) - } + const valid = valid_geographies.includes(map_settings.geography_kind); + + const underlying_shapes: DisplayedMapProps['underlying_shapes'] = useMemo(async () => { + if (!valid) { + return { longnames: [], shapes: [] } } - } + const consolidateShapes = await loadProtobuf( + consolidated_shape_link(map_settings.geography_kind), + "ConsolidatedShapes" + ) + return { + ...consolidateShapes, + shapes: consolidateShapes.shapes.map(Feature.create) as NormalizeProto[] + } + }, [map_settings.geography_kind]); + + const underlying_stats: DisplayedMapProps['underlying_stats'] = useMemo(async () => { + if (!valid) { + return { stats: [], longnames: [] } + } + const consolidatedStatistics = await loadProtobuf( + consolidated_stats_link(map_settings.geography_kind), + "ConsolidatedStatistics" + ) + return { + ...consolidatedStatistics, + stats: consolidatedStatistics.stats.map(AllStats.create) as NormalizeProto[] + } + }, [map_settings.geography_kind]) - set_map_settings(settings) { - this.setState({ - map_settings: settings - }); + const map_ref = useRef(null) - const jsoned_settings = JSON.stringify(settings); + useEffect(() => { + const jsoned_settings = JSON.stringify(map_settings); // gzip then base64 encode const encoded_settings = gzipSync(jsoned_settings).toString("base64"); // convert to parameters like ?settings=... @@ -303,85 +323,56 @@ class MapperPanel extends PageTemplateClass { params.set("settings", encoded_settings); // window.history.replaceState(null, null, "?" + params.toString()); // back button should work - window.history.pushState(null, null, "?" + params.toString()); - } + window.history.pushState(null, '', "?" + params.toString()); + }, [map_settings]) - get_map_settings() { - if (this.state.map_settings === undefined) { - throw new Error("MapperPanel.main_content: map settings not set"); - } - return this.state.map_settings; - } - - render() { - this.update_geography_kind(); - if (new URLSearchParams(window.location.search).get("view") === "true") { - return this.mapper_panel("100%"); - } - return super.render(); - } + useEffect(() => { + const listener = () => set_map_settings(mapSettingsFromURLParams()) + window.addEventListener('popstate', listener); + return () => window.removeEventListener('popstate', listener) + }) - main_content(template_info) { - if (this.state.map_settings === undefined) { - throw new Error("MapperPanel.main_content: map settings not set"); - } - const geography_kind = this.state.map_settings.geography_kind; - const valid = this.valid_geographies.includes(geography_kind); - return ( -
-
Urban Stats Mapper (beta)
- this.set_map_settings(settings)} - /> - - { - !valid ?
Invalid geography kind
: - this.mapper_panel(undefined) // use default height - } -
- ); - } - mapper_panel(height) { - const ramp = parse_ramp(this.state.map_settings.ramp); - const geography_kind = this.state.map_settings.geography_kind; - const color_stat = this.state.map_settings.color_stat; - const filter = this.state.map_settings.filter; + const mapperPanel = (height?: string) => { + const ramp = parse_ramp(map_settings.ramp); return this.state.empirical_ramp} - set_empirical_ramp={(ramp) => this.set_empirical_ramp(ramp)} - color_stat={color_stat} - filter={filter} - map_ref={this.map_ref} - line_style={this.state.map_settings.line_style} - basemap={this.state.map_settings.basemap} + color_stat={map_settings.color_stat} + filter={map_settings.filter} + map_ref={map_ref} + line_style={map_settings.line_style} + basemap={map_settings.basemap} height={height} + settings={map_settings} /> } - componentDidUpdate() { - const self = this; - window.onpopstate = e => { - self.setState({ - map_settings: mapSettingsFromURLParams() - }); - } + if (new URLSearchParams(window.location.search).get("view") === "true") { + return mapperPanel("100%"); } - set_empirical_ramp(ramp) { - if (JSON.stringify(ramp) != JSON.stringify(this.state.empirical_ramp)) { - this.setState({ empirical_ramp: ramp }); - } - } -} + const mainContent = ( +
+
Urban Stats Mapper (beta)
+ set_map_settings(settings)} + names={names} + /> + + { + !valid ?
Invalid geography kind
: + mapperPanel(undefined) // use default height + } +
+ ); + + return mainContent} /> +} diff --git a/react/src/components/quiz-panel.js b/react/src/components/quiz-panel.js index 19a005c47..0162cc781 100644 --- a/react/src/components/quiz-panel.js +++ b/react/src/components/quiz-panel.js @@ -2,7 +2,7 @@ export { QuizPanel, a_correct }; import React from 'react'; -import { PageTemplateClass } from "../page_template/template.js"; +import { PageTemplateClass } from "../page_template/template"; import "../common.css"; import "./quiz.css"; import { reportToServer, reportToServerRetro } from '../quiz/statistics.js'; diff --git a/react/src/components/screenshot.tsx b/react/src/components/screenshot.tsx index e5aeff22f..c8b8755ee 100644 --- a/react/src/components/screenshot.tsx +++ b/react/src/components/screenshot.tsx @@ -54,7 +54,7 @@ export interface ScreencapElements { elements_to_render: HTMLElement[] } -export async function create_screenshot(config: ScreencapElements, universe: string) { +export async function create_screenshot(config: ScreencapElements, universe?: string) { const overall_width = config.overall_width; async function screencap_element(ref: HTMLElement): Promise<[string, number]> { diff --git a/react/src/components/statistic-panel.js b/react/src/components/statistic-panel.js index 95b13d595..c798138f6 100644 --- a/react/src/components/statistic-panel.js +++ b/react/src/components/statistic-panel.js @@ -2,7 +2,7 @@ export { StatisticPanel }; import React from 'react'; -import { PageTemplateClass } from "../page_template/template.js"; +import { PageTemplateClass } from "../page_template/template"; import "../common.css"; import "./article.css"; import { headerTextClass, subHeaderTextClass } from '../utils/responsive'; diff --git a/react/src/data-credit.js b/react/src/data-credit.js index 2aae8871c..fd0a7254e 100644 --- a/react/src/data-credit.js +++ b/react/src/data-credit.js @@ -3,7 +3,7 @@ import React, { useEffect } from 'react'; import ReactDOM from 'react-dom/client'; import "./style.css"; import "./common.css"; -import { PageTemplate } from "./page_template/template.js"; +import { PageTemplate } from "./page_template/template"; import { headerTextClass } from './utils/responsive'; const industry_occupation_table = require("./data/explanation_industry_occupation_table.json"); diff --git a/react/src/page_template/template.js b/react/src/page_template/template.tsx similarity index 75% rename from react/src/page_template/template.js rename to react/src/page_template/template.tsx index 026a77250..fab7b4234 100644 --- a/react/src/page_template/template.js +++ b/react/src/page_template/template.tsx @@ -17,14 +17,22 @@ import { Sidebar } from "../components/sidebar"; import "../common.css"; import "../components/article.css"; import { mobileLayout } from '../utils/responsive'; -import { create_screenshot } from '../components/screenshot'; +import { create_screenshot, ScreencapElements } from '../components/screenshot'; -class PageTemplateClass extends React.Component { +interface TemplateParams { + universes?: string[]; +} + +interface TemplateInfo { + screenshot_mode: boolean; +} + +class PageTemplateClass extends React.Component { render() { return this.main_content(template_info)} + main_content={(template_info: TemplateInfo) => this.main_content(template_info)} /> } @@ -33,33 +41,35 @@ class PageTemplateClass extends React.Component { return undefined; } - main_content(template_info) { + main_content(template_info: TemplateInfo) { // not implemented, should be overridden return (
); } } -function PageTemplate({ - screencap_elements, - universes, - main_content, +type ScreencapElementsProvider = () => ScreencapElements; + +function PageTemplate(props: { + screencap_elements?: ScreencapElementsProvider, + universes?: string[], + main_content: (template_info: TemplateInfo) => JSX.Element }) { - const has_universe_selector = universes != undefined; + const has_universe_selector = props.universes != undefined; const [hamburger_open, set_hamburger_open] = useState(false); const [screenshot_mode, set_screenshot_mode] = useState(false); - const has_screenshot_button = screencap_elements != undefined; + const has_screenshot_button = props.screencap_elements != undefined; - const screencap = async (curr_universe) => { + const screencap = async (curr_universe: string) => { try { console.log("Creating screenshot..."); - await create_screenshot(screencap_elements(), has_universe_selector ? curr_universe : undefined); + await create_screenshot(props.screencap_elements!(), has_universe_selector ? curr_universe : undefined); } catch (e) { console.error(e); } } - const initiate_screenshot = async curr_universe => { + const initiate_screenshot = async (curr_universe: string) => { set_screenshot_mode(true) setTimeout(async () => { await screencap(curr_universe); @@ -80,14 +90,14 @@ function PageTemplate({ set_hamburger_open={set_hamburger_open} has_screenshot={has_screenshot_button} has_universe_selector={has_universe_selector} - all_universes={universes} + all_universes={props.universes} screenshot_mode={screenshot_mode} initiate_screenshot={curr_universe => initiate_screenshot(curr_universe)} />
@@ -118,7 +128,7 @@ function OtherCredits() { } -function BodyPanel(props) { +function BodyPanel(props: {hamburger_open: boolean, main_content: JSX.Element}) { if (props.hamburger_open) { return }