Skip to content

Commit

Permalink
[node server] switch node server to use chart url (#2927)
Browse files Browse the repository at this point in the history
- add new /nodejs/chart endpoint on node server that takes information
about a chart and returns that chart
- update /nodejs/query to return a url to /nodejs/chart for each tile
instead of returning the actual svg. i.e., in the response object,
replace the `svg` field with `chartUrl`. This is behind a flag
&chartUrl=1

![screen-recording-2023-07-12T102022
847](https://github.com/datacommonsorg/website/assets/69875368/f9eac388-1e5c-4e7d-ba19-3490783e4b7e)
![screen-recording-2023-07-12T102126
924](https://github.com/datacommonsorg/website/assets/69875368/a75d4296-c7ae-429b-b4eb-1a60182dadac)
![screen-recording-2023-07-12T102110
422](https://github.com/datacommonsorg/website/assets/69875368/3f4b9cc8-57df-4cf8-a237-81899b0ed521)

Additional change:
- updated parent place api call to take dcid as a req parameter instead
of part of the request path to fix server side forgery vulnerability
  • Loading branch information
chejennifer authored Jul 13, 2023
1 parent c2ff54e commit 306a672
Show file tree
Hide file tree
Showing 22 changed files with 931 additions and 219 deletions.
7 changes: 4 additions & 3 deletions server/routes/shared_api/place.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,10 @@ def child_fetch(parent_dcid):
return result


@bp.route('/parent/<path:dcid>')
@cache.cache.memoize(timeout=cache.TIMEOUT)
def api_parent_places(dcid):
@bp.route('/parent')
@cache.cache.cached(timeout=cache.TIMEOUT, query_string=True)
def api_parent_places():
dcid = request.args.get("dcid")
result = parent_places([dcid])[dcid]
return Response(json.dumps(result), 200, mimetype='application/json')

Expand Down
42 changes: 26 additions & 16 deletions static/js/components/subject_page/disaster_event_block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,24 @@ export const DisasterEventBlock = memo(function DisasterEventBlock(
props.eventTypeSpec,
props.columns
);
fetchDisasterEventData(props, fetchData).then((disasterData) => {
fetchDisasterEventData(
props.id,
blockEventTypeSpecs.current,
props.place.dcid,
fetchData
).then((disasterData) => {
setDisasterEventData(disasterData);
});

function handleHashChange() {
const spinnerId = getSpinnerId();
loadSpinner(spinnerId);
fetchDisasterEventData(props, fetchData).then((disasterData) => {
fetchDisasterEventData(
props.id,
blockEventTypeSpecs.current,
props.place.dcid,
fetchData
).then((disasterData) => {
setDisasterEventData(disasterData);
removeSpinner(spinnerId);
});
Expand Down Expand Up @@ -213,37 +223,37 @@ function getDataFetchCacheKey(dataOptions: DisasterDataOptions): string {

/**
* Fetches disaster event data for a disaster event block
* @param props the props for a disaster event block
* @param blockId id of the block
* @param eventTypeSpecs event type specs used in current block
* @param placeDcid dcid of the place to
* @param fetchData function to use to fetch data with a given cache key and
* data promise
*/
export function fetchDisasterEventData(
props: DisasterEventBlockPropType,
blockId: string,
eventTypeSpecs: Record<string, EventTypeSpec>,
placeDcid: string,
fetchData?: (
cacheKey: string,
dataPromise: () => Promise<any>
) => Promise<any>
) => Promise<any>,
apiRoot?: string
): Promise<Record<string, DisasterEventPointData>> {
const promises = [];
// list of spec ids that correspond to the spec id used for the promise at
// that index in the list of promises.
const specIds = [];
const blockEventTypeSpecs = getBlockEventTypeSpecs(
props.eventTypeSpec,
props.columns
);
Object.values(blockEventTypeSpecs).forEach((spec) => {
Object.values(eventTypeSpecs).forEach((spec) => {
const specDataOptions = {
eventTypeSpec: spec,
selectedDate: getDate(props.id),
severityFilters: getSeverityFilters(props.eventTypeSpec, props.id),
selectedDate: getDate(blockId),
severityFilters: getSeverityFilters(eventTypeSpecs, blockId),
useCache: getUseCache(),
place: props.place.dcid,
place: placeDcid,
};
specIds.push(spec.id);
const cacheKey = getDataFetchCacheKey(specDataOptions);
const promiseFn = () =>
fetchDisasterEventPoints(specDataOptions, props.apiRoot);
const promiseFn = () => fetchDisasterEventPoints(specDataOptions, apiRoot);
const promise = fetchData ? fetchData(cacheKey, promiseFn) : promiseFn();
promises.push(promise);
});
Expand All @@ -264,7 +274,7 @@ export function fetchDisasterEventData(
}

// Gets all the relevant event type specs for a list of columns
function getBlockEventTypeSpecs(
export function getBlockEventTypeSpecs(
fullEventTypeSpec: Record<string, EventTypeSpec>,
columns: ColumnConfig[]
): Record<string, EventTypeSpec> {
Expand Down
2 changes: 1 addition & 1 deletion static/js/components/tiles/bar_tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export interface BarTilePropType {
yAxisMargin?: number;
}

interface BarChartData {
export interface BarChartData {
dataGroup: DataGroup[];
sources: Set<string>;
unit: string;
Expand Down
2 changes: 1 addition & 1 deletion static/js/components/tiles/bivariate_tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export const fetchData = async (
)
.then((resp) => resp.data);
const parentPlacesPromise = axios
.get(`/api/place/parent/${place.dcid}`)
.get(`/api/place/parent?dcid=${place.dcid}`)
.then((resp) => resp.data);
try {
const [placeStats, population, placeNames, geoJson, parentPlaces] =
Expand Down
4 changes: 2 additions & 2 deletions static/js/components/tiles/disaster_event_map_tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const PLACE_TYPE_GEOJSON_PROP = {
EurostatNUTS3: "geoJsonCoordinatesDP1",
};

interface DisasterEventMapTilePropType {
export interface DisasterEventMapTilePropType {
// Id for this tile
id: string;
// Title for this tile
Expand All @@ -104,7 +104,7 @@ interface DisasterEventMapTilePropType {
apiRoot?: string;
}

interface DisasterMapChartData {
export interface DisasterMapChartData {
// Geojson data for the base map
baseMapGeoJson: GeoJsonData;
// Whether the base map is a map of just the current place. If false, the base
Expand Down
2 changes: 1 addition & 1 deletion static/js/components/tiles/line_tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export interface LineTilePropType {
svgChartWidth?: number;
}

interface LineChartData {
export interface LineChartData {
dataGroup: DataGroup[];
sources: Set<string>;
unit: string;
Expand Down
4 changes: 2 additions & 2 deletions static/js/components/tiles/map_tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ interface RawData {
borderGeoJson?: GeoJsonData;
}

interface MapChartData {
export interface MapChartData {
dataValues: { [dcid: string]: number };
metadata: { [dcid: string]: DataPointMetadata };
sources: Set<string>;
Expand Down Expand Up @@ -257,7 +257,7 @@ export const fetchData = async (
const parentPlacesPromise = props.parentPlaces
? Promise.resolve(props.parentPlaces)
: axios
.get(`${props.apiRoot || ""}/api/place/parent/${props.place.dcid}`)
.get(`${props.apiRoot || ""}/api/place/parent?dcid=${props.place.dcid}`)
.then((resp) => resp.data);
try {
const [geoJson, placeStat, population, parentPlaces, borderGeoJsonData] =
Expand Down
2 changes: 1 addition & 1 deletion static/js/components/tiles/scatter_tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { getStatVarName, ReplacementStrings } from "../../utils/tile_utils";
import { ChartTileContainer } from "./chart_tile";
import { useDrawOnResize } from "./use_draw_on_resize";

interface ScatterTilePropType {
export interface ScatterTilePropType {
id: string;
title: string;
place: NamedTypedPlace;
Expand Down
2 changes: 1 addition & 1 deletion static/js/tools/download/mock_functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export function axiosMock(): void {

// get parent place for geoId/06
when(axios.get)
.calledWith("/api/place/parent/geoId/06")
.calledWith("/api/place/parent?dcid=geoId/06")
.mockResolvedValue({
data: [{ dcid: "country/USA", type: "Country", name: "United States" }],
});
Expand Down
2 changes: 1 addition & 1 deletion static/js/tools/scatter/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ function mockAxios(): void {
});

when(axios.get)
.calledWith("/api/place/parent/geoId/10")
.calledWith("/api/place/parent?dcid=geoId/10")
.mockResolvedValue({
data: [{ dcid: "country/USA", type: "Country", name: "United States" }],
});
Expand Down
2 changes: 1 addition & 1 deletion static/js/utils/place_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function getParentPlacesPromise(
apiRoot?: string
): Promise<Array<NamedTypedPlace>> {
return axios
.get(`${apiRoot || ""}/api/place/parent/${placeDcid}`)
.get(`${apiRoot || ""}/api/place/parent?dcid=${placeDcid}`)
.then((resp) => {
const parentsData = resp.data;
const filteredParentsData = parentsData.filter((parent) => {
Expand Down
118 changes: 95 additions & 23 deletions static/nodejs_server/bar_tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
*/

/**
* Functions for getting tile result for a bar tile
* Functions for getting results for a bar tile
*/

import _ from "lodash";

import {
BarChartData,
BarTilePropType,
draw,
fetchData,
getReplacementStrings,
Expand All @@ -30,35 +32,26 @@ import { NamedTypedPlace, StatVarSpec } from "../js/shared/types";
import { TileConfig } from "../js/types/subject_page_proto_types";
import { dataGroupsToCsv } from "../js/utils/chart_csv_utils";
import { getChartTitle } from "../js/utils/tile_utils";
import { DOM_ID, SVG_HEIGHT, SVG_WIDTH } from "./constants";
import { CHART_ID, DOM_ID, SVG_HEIGHT, SVG_WIDTH } from "./constants";
import { TileResult } from "./types";
import { getProcessedSvg, getSources } from "./utils";
import { getChartUrl, getProcessedSvg, getSources, getSvgXml } from "./utils";

/**
* Gets the Tile Result for a bar tile
* @param id id of the chart
* @param tileConfig config for the bar tile
* @param place place to show the bar chart for
* @param enclosedPlaceType enclosed place type to use for bar chart
* @param statVarSpec list of stat var specs to show in the bar chart
* @param apiRoot API root to use to fetch data
*/
export async function getBarTileResult(
function getTileProp(
id: string,
tileConfig: TileConfig,
place: NamedTypedPlace,
enclosedPlaceType: string,
statVarSpec: StatVarSpec[],
apiRoot: string
): Promise<TileResult> {
): BarTilePropType {
const comparisonPlaces = tileConfig.comparisonPlaces
? tileConfig.comparisonPlaces.map((p) =>
p == SELF_PLACE_DCID_PLACEHOLDER ? place.dcid : p
)
: undefined;
const useLollipop =
tileConfig.barTileSpec && tileConfig.barTileSpec.useLollipop;
const tileProp = {
return {
id,
title: tileConfig.title,
place,
Expand All @@ -69,23 +62,56 @@ export async function getBarTileResult(
comparisonPlaces,
useLollipop,
};
}

function getBarChartSvg(
tileProp: BarTilePropType,
chartData: BarChartData
): SVGSVGElement {
const tileContainer = document.createElement("div");
tileContainer.setAttribute("id", tileProp.id);
document.getElementById(DOM_ID).appendChild(tileContainer);
draw(tileProp, chartData, tileContainer, SVG_WIDTH);
return getProcessedSvg(tileContainer.querySelector("svg"));
}

/**
* Gets the Tile Result for a bar tile
* @param id id of the chart
* @param tileConfig config for the bar tile
* @param place place to show the bar chart for
* @param enclosedPlaceType enclosed place type to use for bar chart
* @param statVarSpec list of stat var specs to show in the bar chart
* @param apiRoot API root to use to fetch data
*/
export async function getBarTileResult(
id: string,
tileConfig: TileConfig,
place: NamedTypedPlace,
enclosedPlaceType: string,
statVarSpec: StatVarSpec[],
apiRoot: string,
urlRoot: string,
useChartUrl: boolean
): Promise<TileResult> {
const tileProp = getTileProp(
id,
tileConfig,
place,
enclosedPlaceType,
statVarSpec,
apiRoot
);
try {
const chartData = await fetchData(tileProp);
const tileContainer = document.createElement("div");
tileContainer.setAttribute("id", id);
document.getElementById(DOM_ID).appendChild(tileContainer);
draw(tileProp, chartData, tileContainer, SVG_WIDTH);
let legend = [];
if (
!_.isEmpty(chartData.dataGroup) &&
!_.isEmpty(chartData.dataGroup[0].value)
) {
legend = chartData.dataGroup[0].value.map((dp) => dp.label);
}
const svg = getProcessedSvg(tileContainer.querySelector("svg"));
tileContainer.remove();
return {
svg,
const result: TileResult = {
data_csv: dataGroupsToCsv(chartData.dataGroup),
srcs: getSources(chartData.sources),
legend,
Expand All @@ -95,8 +121,54 @@ export async function getBarTileResult(
),
type: "BAR",
};
if (useChartUrl) {
result.chartUrl = getChartUrl(
tileConfig,
place.dcid,
statVarSpec,
enclosedPlaceType,
null,
urlRoot
);
return result;
}
const svg = getBarChartSvg(tileProp, chartData);
result.svg = getSvgXml(svg);
return result;
} catch (e) {
console.log("Failed to get bar tile result for: " + id);
return null;
}
}

/**
* Gets the bar chart for a given tile config
* @param tileConfig the tile config for the chart
* @param place the place to get the chart for
* @param enclosedPlaceType the enclosed place type to get the chart for
* @param statVarSpec list of stat var specs to show in the chart
* @param apiRoot API root to use to fetch data
*/
export async function getBarChart(
tileConfig: TileConfig,
place: NamedTypedPlace,
enclosedPlaceType: string,
statVarSpec: StatVarSpec[],
apiRoot: string
): Promise<SVGSVGElement> {
const tileProp = getTileProp(
CHART_ID,
tileConfig,
place,
enclosedPlaceType,
statVarSpec,
apiRoot
);
try {
const chartData = await fetchData(tileProp);
return getBarChartSvg(tileProp, chartData);
} catch (e) {
console.log("Failed to get bar chart");
return null;
}
}
10 changes: 10 additions & 0 deletions static/nodejs_server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const SVG_HEIGHT = 300;
// Width of the svg to render.
export const SVG_WIDTH = 500;
export const DOM_ID = "dom-id";
// id to use for drawing charts.
export const CHART_ID = "chart-id";
// Font family to use for all the text on the charts. If this is updated, need
// to also update CHAR_WIDTHS and CHAR_AVG_WIDTHS.
export const FONT_FAMILY = "Roboto";
Expand All @@ -38,3 +40,11 @@ export const FONT_SIZE = "10px";
// Width of the constant sized part of the map legend
export const MAP_LEGEND_CONSTANT_WIDTH =
LEGEND_IMG_WIDTH + LEGEND_MARGIN_RIGHT + LEGEND_TICK_LABEL_MARGIN;
// Url params used for getting a single chart
export const CHART_URL_PARAMS = {
TILE_CONFIG: "config",
PLACE: "place",
ENCLOSED_PLACE_TYPE: "enclosedPlaceType",
STAT_VAR_SPEC: "svSpec",
EVENT_TYPE_SPEC: "eventTypeSpec",
};
Loading

0 comments on commit 306a672

Please sign in to comment.