- {
- /* We want to render this header element even if title is empty
- to keep the space on the page */
- props.title && (
-
- {props.isLoading ? (
- <>
-
- {title ? "" : " Loading..."}
- >
- ) : null}{" "}
- {title}
-
- )
- }
+
{props.subtitle && !props.isInitialLoading ? (
{props.subtitle}
diff --git a/static/js/components/tiles/line_tile.tsx b/static/js/components/tiles/line_tile.tsx
index 251a4368ac..19ed453a2b 100644
--- a/static/js/components/tiles/line_tile.tsx
+++ b/static/js/components/tiles/line_tile.tsx
@@ -18,8 +18,7 @@
* Component for rendering a line type tile.
*/
-import { ISO_CODE_ATTRIBUTE } from "@datacommonsorg/client";
-import { isDateInRange } from "@datacommonsorg/client";
+import { isDateInRange, ISO_CODE_ATTRIBUTE } from "@datacommonsorg/client";
import _ from "lodash";
import React, { useCallback, useEffect, useRef, useState } from "react";
@@ -31,7 +30,6 @@ import { URL_PATH } from "../../constants/app/visualization_constants";
import { CSV_FIELD_DELIMITER } from "../../constants/tile_constants";
import { SeriesApiResponse } from "../../shared/stat_types";
import { NamedTypedPlace, StatVarSpec } from "../../shared/types";
-import { loadSpinner, removeSpinner } from "../../shared/util";
import { computeRatio } from "../../tools/shared_util";
import {
getContextStatVar,
@@ -83,8 +81,6 @@ export interface LineTilePropType {
svgChartWidth?: number;
// Whether or not to show the explore more button.
showExploreMore?: boolean;
- // Whether or not to show a loading spinner when fetching data.
- showLoadingSpinner?: boolean;
// Whether to show tooltip on hover
showTooltipOnHover?: boolean;
// Function used to get processed stat var names.
@@ -117,14 +113,19 @@ export interface LineChartData {
export function LineTile(props: LineTilePropType): JSX.Element {
const svgContainer = useRef(null);
const [chartData, setChartData] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (!chartData || !_.isEqual(chartData.props, props)) {
- loadSpinner(props.id);
(async () => {
- const data = await fetchData(props);
- if (props && _.isEqual(data.props, props)) {
- setChartData(data);
+ setIsLoading(true);
+ try {
+ const data = await fetchData(props);
+ if (props && _.isEqual(data.props, props)) {
+ setChartData(data);
+ }
+ } finally {
+ setIsLoading(false);
}
})();
}
@@ -135,37 +136,31 @@ export function LineTile(props: LineTilePropType): JSX.Element {
return;
}
draw(props, chartData, svgContainer.current);
- removeSpinner(props.id);
}, [props, chartData]);
useDrawOnResize(drawFn, svgContainer.current);
return (
- {props.showLoadingSpinner && (
-
- )}
-
+ >
);
}
diff --git a/static/js/components/tiles/loading_header.tsx b/static/js/components/tiles/loading_header.tsx
new file mode 100644
index 0000000000..f657109768
--- /dev/null
+++ b/static/js/components/tiles/loading_header.tsx
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * A container for any tile containing a chart.
+ */
+
+import React from "react";
+import { Spinner } from "reactstrap";
+
+/**
+ * Header with loading indicator
+ * @param props.isLoading true if the component is loading
+ * @param props.title Header text
+ * @returns
+ */
+export function LoadingHeader(props: {
+ isLoading: boolean;
+ title?: string;
+}): JSX.Element {
+ const { isLoading, title, ...headerProps } = props;
+ return (
+
+ {props.isLoading ? (
+ <>
+
+ {title ? title : "Loading..."}
+ >
+ ) : (
+ title
+ )}
+
+ );
+}
diff --git a/static/js/components/tiles/ranking_tile.tsx b/static/js/components/tiles/ranking_tile.tsx
index c085957aa5..32881653df 100644
--- a/static/js/components/tiles/ranking_tile.tsx
+++ b/static/js/components/tiles/ranking_tile.tsx
@@ -29,11 +29,7 @@ import {
import { ChartEmbed } from "../../place/chart_embed";
import { PointApiResponse, SeriesApiResponse } from "../../shared/stat_types";
import { StatVarSpec } from "../../shared/types";
-import {
- getCappedStatVarDate,
- loadSpinner,
- removeSpinner,
-} from "../../shared/util";
+import { getCappedStatVarDate } from "../../shared/util";
import {
RankingData,
RankingGroup,
@@ -51,6 +47,7 @@ import {
getStatVarName,
transformCsvHeader,
} from "../../utils/tile_utils";
+import { LoadingHeader } from "./loading_header";
import { SvRankingUnits } from "./sv_ranking_units";
import { ContainedInPlaceMultiVariableTileProp } from "./tile_types";
@@ -66,7 +63,6 @@ export interface RankingTilePropType
hideFooter?: boolean;
onHoverToggled?: (placeDcid: string, hover: boolean) => void;
rankingMetadata: RankingTileSpec;
- showLoadingSpinner?: boolean;
footnote?: string;
// Optional: Override sources for this tile
sources?: string[];
@@ -77,13 +73,18 @@ export function RankingTile(props: RankingTilePropType): JSX.Element {
const [rankingData, setRankingData] = useState
(null);
const embedModalElement = useRef(null);
const chartContainer = useRef(null);
+ const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
- loadSpinner(getSpinnerId());
- fetchData(props).then((rankingData) => {
- setRankingData(rankingData);
- removeSpinner(getSpinnerId());
- });
+ (async () => {
+ setIsLoading(true);
+ try {
+ const rankingData = await fetchData(props);
+ setRankingData(rankingData);
+ } finally {
+ setIsLoading(false);
+ }
+ })();
}, [props]);
const numRankingLists = getNumRankingLists(
@@ -136,7 +137,9 @@ export function RankingTile(props: RankingTilePropType): JSX.Element {
}
return (
{
return (
-
+
);
})}
{rankingData &&
@@ -162,38 +166,28 @@ export function RankingTile(props: RankingTilePropType): JSX.Element {
: "";
return (
);
})}
- {props.showLoadingSpinner && (
-
- )}
);
-
- function getSpinnerId(): string {
- return `ranking-spinner-${props.id}`;
- }
}
export async function fetchData(
diff --git a/static/js/components/tiles/scatter_tile.tsx b/static/js/components/tiles/scatter_tile.tsx
index 87cf37139c..7394f9b22d 100644
--- a/static/js/components/tiles/scatter_tile.tsx
+++ b/static/js/components/tiles/scatter_tile.tsx
@@ -35,7 +35,6 @@ import { ChartQuadrant } from "../../constants/scatter_chart_constants";
import { CSV_FIELD_DELIMITER } from "../../constants/tile_constants";
import { PointApiResponse, SeriesApiResponse } from "../../shared/stat_types";
import { NamedTypedPlace, StatVarSpec } from "../../shared/types";
-import { loadSpinner, removeSpinner } from "../../shared/util";
import { SHOW_POPULATION_OFF } from "../../tools/scatter/context";
import { getStatWithinPlace } from "../../tools/scatter/util";
import { ScatterTileSpec } from "../../types/subject_page_proto_types";
@@ -77,8 +76,6 @@ export interface ScatterTilePropType {
apiRoot?: string;
// Whether or not to show the explore more button.
showExploreMore?: boolean;
- // Whether or not to show a loading spinner when fetching data.
- showLoadingSpinner?: boolean;
// Text to show in footer
footnote?: string;
// The property to use to get place names.
@@ -119,17 +116,22 @@ export function ScatterTile(props: ScatterTilePropType): JSX.Element {
const [scatterChartData, setScatterChartData] = useState<
ScatterChartData | undefined
>(null);
+ const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (scatterChartData && areDataPropsEqual()) {
// only re-fetch if the props that affect data fetch are not equal
return;
}
- loadSpinner(getSpinnerId());
(async () => {
- const data = await fetchData(props);
- if (props && data && _.isEqual(data.props, props)) {
- setScatterChartData(data);
+ setIsLoading(true);
+ try {
+ const data = await fetchData(props);
+ if (props && data && _.isEqual(data.props, props)) {
+ setScatterChartData(data);
+ }
+ } finally {
+ setIsLoading(false);
}
})();
}, [props, scatterChartData]);
@@ -145,25 +147,25 @@ export function ScatterTile(props: ScatterTilePropType): JSX.Element {
tooltip.current,
props.scatterTileSpec || {}
);
- removeSpinner(getSpinnerId());
}, [props.svgChartHeight, props.scatterTileSpec, scatterChartData]);
useDrawOnResize(drawFn, svgContainer.current);
return (
- {props.showLoadingSpinner && (
-
- )}
);
- function getSpinnerId(): string {
- return `scatter-spinner-${props.id}`;
- }
-
function areDataPropsEqual(): boolean {
const oldDataProps = [
scatterChartData.props.place,
diff --git a/static/js/components/tiles/sv_ranking_units.tsx b/static/js/components/tiles/sv_ranking_units.tsx
index ee7988a52f..c74951bb2b 100644
--- a/static/js/components/tiles/sv_ranking_units.tsx
+++ b/static/js/components/tiles/sv_ranking_units.tsx
@@ -57,6 +57,7 @@ interface SvRankingUnitsProps {
footnote?: string;
// Optional: Override sources for this tile
sources?: string[];
+ isLoading?: boolean;
}
/**
@@ -69,7 +70,6 @@ export function SvRankingUnits(props: SvRankingUnitsProps): JSX.Element {
const rankingGroup = rankingData[statVar];
const highestRankingUnitRef = useRef();
const lowestRankingUnitRef = useRef();
-
/**
* Build content and triggers export modal window
*/
@@ -112,7 +112,8 @@ export function SvRankingUnits(props: SvRankingUnitsProps): JSX.Element {
highestRankingUnitRef,
props.onHoverToggled,
props.errorMsg,
- props.sources
+ props.sources,
+ props.isLoading
)}
{!props.hideFooter && (
,
onHoverToggled?: (placeDcid: string, hover: boolean) => void,
errorMsg?: string,
- sources?: string[]
+ sources?: string[],
+ isLoading?: boolean
): JSX.Element {
const { topPoints, bottomPoints } = getRankingUnitPoints(
rankingMetadata,
@@ -322,6 +326,7 @@ export function getRankingUnit(
bottomPoints={bottomPoints}
numDataPoints={rankingGroup.numDataPoints}
isHighest={isHighest}
+ isLoading={isLoading}
svNames={
rankingMetadata.showMultiColumn ? rankingGroup.svName : undefined
}