Skip to content

Commit

Permalink
Dev place page UI (datacommonsorg#4747)
Browse files Browse the repository at this point in the history
Updates dev place page to include:

* Topic tabs (Overview, Economics, etc)
* Place Overview panel that includes place overview text, map, and key
demographics
* Child places link section
* Charts for selected categories

## Screenshots
![Screenshot 2024-11-19 at 4 42
27 PM](https://github.com/user-attachments/assets/1ad752a5-3faa-485d-bba5-85eb6e672dc0)

![Screenshot 2024-11-19 at 4 42
36 PM](https://github.com/user-attachments/assets/868647cf-64e4-4014-a704-b62c60feb5a4)

## Still TODO:
* A better 404 page
* Only render the place overview panel if we have data to display in the
panel
* The "Places in <place>" link section can get really large. Limit it to
a maximum of "n" items
* Update chart section styles to match mockups
* Re-generate place summary text
* Merge /dev-place/ endpoint with /place/ endpoint
* Add experiment setup to only show the new place page for certain
places but not others
* Add user engagement tracking
* Add i18n

Includes changes from
datacommonsorg#4738 . I'll rebase this
PR once it's merged.
  • Loading branch information
dwnoble authored Dec 5, 2024
1 parent 496dd7d commit bdef654
Show file tree
Hide file tree
Showing 15 changed files with 890 additions and 208 deletions.
34 changes: 34 additions & 0 deletions packages/client/src/data_commons_web_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import {
ApiNodePropvalOutResponse,
ObservationDatesApiResponse,
PlaceChartsApiResponse,
PointApiResponse,
RelatedPlacesApiResponse,
SeriesApiResponse,
} from "./data_commons_web_client_types";
import { parseWebsiteApiRoot, toURLSearchParams } from "./utils";
Expand Down Expand Up @@ -203,6 +205,38 @@ class DataCommonsWebClient {
const response = await fetch(url);
return (await response.json()) as ObservationDatesApiResponse;
}

/**
* Gets place charts for the given category
* Uses /api/dev-place/charts/<placeDcid> endpoint
* @param params.category [optional] place category
* @param params.placeDcid place dcid to fetch data for
*/
async getPlaceCharts(params: {
category?: string;
placeDcid: string;
}): Promise<PlaceChartsApiResponse> {
const url = `${this.apiRoot || ""}/api/dev-place/charts/${
params.placeDcid
}${params.category ? "?category=" + params.category : ""}`;
const response = await fetch(url);
return (await response.json()) as PlaceChartsApiResponse;
}

/**
* Gets related place info charts for the given place
* Uses /api/dev-place/related-places/<placeDcid> endpoint
* @param params.placeDcid place dcid to fetch data for
*/
async getRelatedPLaces(params: {
placeDcid: string;
}): Promise<RelatedPlacesApiResponse> {
const url = `${this.apiRoot || ""}/api/dev-place/related-places/${
params.placeDcid
}`;
const response = await fetch(url);
return (await response.json()) as RelatedPlacesApiResponse;
}
}

export { DataCommonsWebClient };
43 changes: 43 additions & 0 deletions packages/client/src/data_commons_web_client_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,46 @@ export type ObservationDatesApiResponse = {
};
};
};

type ChartType = "BAR" | "LINE" | "MAP" | "RANKING";
export interface Chart {
type: ChartType;
title: string;
category: string;
description: string;
statisticalVariableDcids: string[];
topicDcids: string[];
denominator?: string; // Optional
unit?: string; // Optional
scaling?: number; // Optional
}

export interface Place {
dcid: string;
name: string;
types: string[];
}

/**
* Website API response for /api/dev-place/charts/<place_dcid>
*/
export interface PlaceChartsApiResponse {
charts: Chart[];
childPlaceType: string;
childPlaces: Place[];
nearbyPlaces: Place[];
place: Place;
similarPlaces: Place[];
translatedCategoryStrings: Record<string, string>;
}

/**
* Website API response for /api/dev-place/related-places/<place_dcid>
*/
export interface RelatedPlacesApiResponse {
childPlaceType: string;
childPlaces: Place[];
nearbyPlaces: Place[];
place: Place;
similarPlaces: Place[];
}
2 changes: 1 addition & 1 deletion server/lib/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ def multiple_property_values(nodes: List[str],
Fetches specified properties for given nodes using the /v2/node API.
Args:
nodes (List[str]): List of node identifiers.
nodes (List[str]): List of node dcids.
props (List[str]): Properties to retrieve for each node.
out (bool): If True, fetches outgoing properties; otherwise, incoming (default: True).
Expand Down
1 change: 1 addition & 0 deletions server/routes/dev_place/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def dev_place(place_dcid=None):
place_dcid)
place_names = place_api.get_i18n_name([place_dcid]) or {}
place_name = place_names.get(place_dcid, place_dcid)
# Place summaries are currently only supported in English
if g.locale == DEFAULT_LOCALE:
place_summary = get_place_summaries(place_dcid).get(place_dcid,
{}).get("summary", "")
Expand Down
4 changes: 3 additions & 1 deletion server/routes/dev_place/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,13 @@ def chart_config_to_overview_charts(chart_config, child_place_type: str):
unit=page_config_related_chart.get("unit"),
)
if page_config_related_chart.get("denominator"):
# Related charts only specify one denominator, so we repeat it for each stat var
related_chart.denominator = [
page_config_related_chart.get("denominator")
]
] * len(chart.statisticalVariableDcids)
if is_map_chart:
related_chart.childPlaceType = child_place_type
charts.append(related_chart)

return charts

Expand Down
14 changes: 11 additions & 3 deletions server/templates/dev_place.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
{% set description = _('Statistics about {category} in {place_name}.'.format(category=category, place_name=place_name)) %}
{% endif %}
{% set place_category = category %}
{% set is_show_header_search_bar = true %}

{% block head %}
<link rel="stylesheet" href={{url_for('static', filename='css/dev_place_page.min.css' , t=config['VERSION'])}}>
Expand All @@ -41,10 +42,17 @@

{% block content %}
<div id="body" class="container-fluid">
<div id="page-metadata"
style="display:none;"
data-place-dcid="{{place_dcid}}"
data-place-name="{{place_name}}"
data-place-links="{{place_type_with_parent_places_links}}"
data-place-summary="{{place_summary}}"
data-place-subheader="{{place_type_with_parent_places_links}}"
></div>
<div id="main" class="container">
<div id="nl-search-bar"></div>
<h1>{{ place_name }}</h1>
<p class="subheader">{{ place_type_with_parent_places_links | safe }}</p>
<div id="place-page-content" class="page-content-container">
</div>
</div>
</div>
{% endblock %}
Expand Down
174 changes: 18 additions & 156 deletions server/tests/routes/api/dev_place_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,63 +17,14 @@

from server.routes.dev_place.types import Place
import server.routes.dev_place.utils as place_utils
from server.tests.routes.api.mock_data import MULTIPLE_PROPERTY_VALUES_RESPONSE
from server.tests.routes.api.mock_data import \
MULTIPLE_PROPERTY_VALUES_RESPONSE_WITH_LANGUAGES
from server.tests.routes.api.mock_data import OSERVATION_POINT_RESPONSE
from server.tests.routes.api.mock_data import OSERVATION_WITHIN_POINT_RESPONSE
from server.tests.routes.api.mock_data import SAMPLE_PLACE_PAGE_CHART_CONFIG
from web_app import app

SAMPLE_CHART_CONFIG = [{
"category": "Crime",
"titleId": "CHART_TITLE-Total_crime",
"title": "Total crime",
"description": "Total number of criminal incidents",
"statsVars": ["Count_CriminalActivities_CombinedCrime"],
"isOverview": True,
"relatedChart": {
"titleId": "CHART_TITLE-Crime_rate",
"title": "Crimes per 100,000 people",
"description": "Total number of criminal incidents per 100,000 people",
"scale": True,
"denominator": "Count_Person",
"scaling": 100000
}
}, {
"category":
"Education",
"titleId":
"CHART_TITLE-Educational_attainment",
"title":
"Education attainment",
"denominator": [
"Sample_Denominator_1", "Sample_Denominator_2", "Sample_Denominator_3",
"Sample_Denominator_4", "Sample_Denominator_5"
],
"description":
"Number of people who have attained various educational milestones, e.g. completed high school or have a bachelor's degree",
"statsVars": [
"Count_Person_EducationalAttainmentNoSchoolingCompleted",
"Count_Person_EducationalAttainmentRegularHighSchoolDiploma",
"Count_Person_EducationalAttainmentBachelorsDegree",
"Count_Person_EducationalAttainmentMastersDegree",
"Count_Person_EducationalAttainmentDoctorateDegree"
],
"isOverview":
True,
"relatedChart": {
"titleId":
"CHART_TITLE-Educational_attainment_rate",
"title":
"Education attainment rate",
"description":
"Percentage of the adult population who have attained various educational milestones, e.g. completed high school or have a bachelor's degree",
"scale":
True,
"denominator":
"Count_Person_25OrMoreYears",
"scaling":
100,
"unit":
"%"
}
}]


class TestPlaceAPI(unittest.TestCase):

Expand All @@ -91,72 +42,15 @@ def test_dev_place_charts(self, mock_obs_point_within, mock_obs_point,
place_dcid = "country/USA"

# Override the CHART_CONFIG with sample values
app.config['CHART_CONFIG'] = SAMPLE_CHART_CONFIG
app.config['CHART_CONFIG'] = SAMPLE_PLACE_PAGE_CHART_CONFIG

# Mock obs_point call with a properly structured response
mock_obs_point.return_value = {
"byVariable": {
"Count_CriminalActivities_CombinedCrime": {
"byEntity": {
place_dcid: {
"dates": {
"2022": {
"value": 1000
}
}
}
}
},
# Include one education stat var to simulate data availability
"Count_Person_EducationalAttainmentBachelorsDegree": {
"byEntity": {
place_dcid: {
"dates": {
"2022": {
"value": 500
}
}
}
}
}
}
}
mock_obs_point.return_value = OSERVATION_POINT_RESPONSE

# Mock obs_point_within for finding child places existence check for map-based stat vars
mock_obs_point_within.return_value = {
"byVariable": {
"Count_CriminalActivities_CombinedCrime": {
"byEntity": {
"geoId/123": {
"dates": {
"2022": {
"value": 200
}
}
}
}
},
# Simulate a child place with data for an educational stat var
"Count_Person_EducationalAttainmentMastersDegree": {
"byEntity": {
"geoId/456": {
"dates": {
"2022": {
"value": 50
}
}
}
}
}
}
}
mock_obs_point_within.return_value = OSERVATION_WITHIN_POINT_RESPONSE

mock_multiple_property_values.return_value = {
"country/USA": {
"typeOf": ["Country"],
"name": ['United States']
}
}
mock_multiple_property_values.return_value = MULTIPLE_PROPERTY_VALUES_RESPONSE

# Mock fetch.raw_property_values to return empty lists (no nearby or similar places)
mock_raw_property_values.return_value = {place_dcid: []}
Expand All @@ -174,8 +68,9 @@ def test_dev_place_charts(self, mock_obs_point_within, mock_obs_point,
self.assertIn('translatedCategoryStrings', response_json)

# Check that the 'charts' field contains the expected number of charts
# Since only two charts have data (Crime and one Education stat var), we expect two charts
self.assertEqual(len(response_json['charts']), 2)
# Two charts have data (Crime and one Education stat var), and each has a
# related chart, so we expect four charts
self.assertEqual(len(response_json['charts']), 4)

# Optionally, check that the charts have the correct titles
chart_titles = [chart['title'] for chart in response_json['charts']]
Expand All @@ -192,7 +87,10 @@ def test_dev_place_charts(self, mock_obs_point_within, mock_obs_point,
self.assertIn('Education', response_json['translatedCategoryStrings'])

# Ensure the denominator is present in chart results
self.assertEqual(5, len(response_json["charts"][1]["denominator"]))
self.assertEqual(None, response_json["charts"][0]["denominator"])
self.assertEqual(1, len(response_json["charts"][1]["denominator"]))
self.assertEqual(5, len(response_json["charts"][2]["denominator"]))
self.assertEqual(5, len(response_json["charts"][3]["denominator"]))

@patch('server.routes.dev_place.utils.fetch.raw_property_values')
@patch('server.routes.dev_place.utils.fetch.multiple_property_values')
Expand All @@ -207,43 +105,7 @@ def test_related_places(self, mock_descendent_places,
place_dcid = 'country/USA'

# Define side effects for mock_multiple_property_values
mock_multiple_property_values.return_value = {
'country/USA': {
'typeOf': ['Country'],
'name': ['United States of America'],
'nameWithLanguage': []
},
'country/CAN': {
'typeOf': ['Country'],
'name': ['Canada'],
'nameWithLanguage': []
},
'country/MEX': {
'typeOf': ['Country'],
'name': ['Mexico'],
'nameWithLanguage': []
},
'country/GBR': {
'typeOf': ['Country'],
'name': ['United Kingdom'],
'nameWithLanguage': []
},
'country/AUS': {
'typeOf': ['Country'],
'name': ['Australia'],
'nameWithLanguage': []
},
'geoId/06': {
'typeOf': ['State'],
'name': ['California'],
'nameWithLanguage': []
},
'geoId/07': {
'typeOf': ['State'],
'name': ['New York'],
'nameWithLanguage': []
}
}
mock_multiple_property_values.return_value = MULTIPLE_PROPERTY_VALUES_RESPONSE_WITH_LANGUAGES

# Mock descendent_places to return child places
mock_descendent_places.return_value = {
Expand Down
Loading

0 comments on commit bdef654

Please sign in to comment.