diff --git a/static/css/base.scss b/static/css/base.scss index 25957ee92b..4e8be6ba7e 100644 --- a/static/css/base.scss +++ b/static/css/base.scss @@ -202,7 +202,7 @@ $headings-font-family: $font-family-sans-serif; background: $full-footer-bg; padding-top: 75px; /* Trick to get the footer to extend to the bottom of short pages */ - box-shadow: 0 50vh 0 50vh var(--footer-bg); + box-shadow: 0 50vh 0 50vh var(--footer-background); .top-footer { padding-bottom: 30px; } @@ -250,7 +250,7 @@ $headings-font-family: $font-family-sans-serif; #main-footer #sub-footer { align-items: space-between; - background: var(--footer-bg); + background: var(--footer-background); border-top: 1px solid $gray-300; display: flex; flex-direction: row; @@ -328,7 +328,7 @@ $headings-font-family: $font-family-sans-serif; position: absolute; left: 0; top: 0; - background: rgba(0, 0, 0, 0.4); + background: rgba(0, 0, 0, 0.02); z-index: 100; width: 100%; height: 100%; @@ -374,7 +374,7 @@ $headings-font-family: $font-family-sans-serif; margin-top: 150px; margin-left: 20px; font-size: 20px; - color: white; + color: var(--gm-3-ref-neutral-neutral-40); opacity: 0; /* Initially hidden */ animation: fadeIn 0.5s forwards 3s; /* Animation after 3s delay */ } diff --git a/static/css/base/_color.scss b/static/css/base/_color.scss index 43fac94dd4..38a11b9eaa 100644 --- a/static/css/base/_color.scss +++ b/static/css/base/_color.scss @@ -51,7 +51,8 @@ $dc-primary-color: #467bd5; // Following vars are overwritten in custom dc modes --dc-primary: #{$dc-primary-color}; --link-color: #0b57d0; - --footer-bg: #f5f9fe; + --loading-background: #f8f9fa; + --footer-background: #f5f9fe; // Theming of styled components --button-text-color: var(--link-color); --button-background-color: var(--gm-3-white); diff --git a/static/css/shared/_ranking_unit.scss b/static/css/shared/_ranking_unit.scss index d422a5ef51..5e116a692f 100644 --- a/static/css/shared/_ranking_unit.scss +++ b/static/css/shared/_ranking_unit.scss @@ -22,6 +22,7 @@ .chart-footnote { font-weight: 500; margin: 8px 0 0 0; + padding: 0 24px; } } diff --git a/static/css/shared/_tiles.scss b/static/css/shared/_tiles.scss index e759bff1f6..3edb87b9a4 100644 --- a/static/css/shared/_tiles.scss +++ b/static/css/shared/_tiles.scss @@ -122,11 +122,16 @@ $box-shadow-8dp: 0 8px 10px 1px rgba(0, 0, 0, 0.14), .chart-container.disaster-event-map-tile { border: var(--border-primary); border-radius: var(--border-radius-primary); + overflow: hidden; padding: 0; } .chart-container.ranking-tile { - padding-top: 15.5px; + padding: 0; + .loading { + flex-grow: 1; + padding: 24px 24px 16px; + } } .svg-container, @@ -144,7 +149,7 @@ $box-shadow-8dp: 0 8px 10px 1px rgba(0, 0, 0, 0.14), } .initial-loading-placeholder { - background-color: var(--light); + background-color: var(--loading-background) !important; } /** footnotes */ @@ -283,19 +288,15 @@ $box-shadow-8dp: 0 8px 10px 1px rgba(0, 0, 0, 0.14), font-weight: 600; font-size: 0.8rem; } - - footer { - padding: 16px 0; - } } .ranking-unit-container { - padding: 0 0.5rem; display: flex; flex-direction: column; .ranking-list { flex-grow: 1; + padding: 24px 24px 16px 24px; } } diff --git a/static/css/tools/visualization.scss b/static/css/tools/visualization.scss index 2ae748369d..448955647e 100644 --- a/static/css/tools/visualization.scss +++ b/static/css/tools/visualization.scss @@ -594,7 +594,6 @@ $selector-footer-height: 40px; .ranking-tile { display: flex; flex-wrap: wrap; - padding: 1rem; h4 { text-align: left; diff --git a/static/custom_dc/climate_trace/overrides.css b/static/custom_dc/climate_trace/overrides.css index 79842a384d..a217bf4837 100644 --- a/static/custom_dc/climate_trace/overrides.css +++ b/static/custom_dc/climate_trace/overrides.css @@ -17,7 +17,7 @@ :root { --dc-primary: #110c45; --link-color: #4c49b0; - --footer-bg: white; + --footer-background: white; --dc-red-fade: #40d4e033; } @@ -45,4 +45,4 @@ #main-footer { border-top: 1px solid #efefef; -} \ No newline at end of file +} diff --git a/static/custom_dc/custom/overrides.css b/static/custom_dc/custom/overrides.css index 97d4e045c3..6e1516eaaf 100644 --- a/static/custom_dc/custom/overrides.css +++ b/static/custom_dc/custom/overrides.css @@ -17,7 +17,7 @@ :root { --dc-primary: #5d5f65; --link-color: #514ea3; - --footer-bg: white; + --footer-background: white; --dc-red-fade: #40d4e033; } @@ -45,4 +45,4 @@ #main-footer { border-top: 1px solid #efefef; -} \ No newline at end of file +} diff --git a/static/custom_dc/iitm/style.css b/static/custom_dc/iitm/style.css index a177229623..d4dafda997 100644 --- a/static/custom_dc/iitm/style.css +++ b/static/custom_dc/iitm/style.css @@ -9,7 +9,7 @@ --iitm-section-text: #0d0952; --dc-primary: #80296e; --link-color: #80296e; - --footer-bg: #0b1434; + --footer-background: #0b1434; } header.masthead { @@ -59,7 +59,7 @@ header.masthead p { .nav-item .nav-link, #main-nav .navbar-brand .sep { - color: rgba(255,255,255,.5) + color: rgba(255, 255, 255, 0.5); } } diff --git a/static/custom_dc/unsdg/overrides.css b/static/custom_dc/unsdg/overrides.css index 0630c175b4..a217bf4837 100644 --- a/static/custom_dc/unsdg/overrides.css +++ b/static/custom_dc/unsdg/overrides.css @@ -17,7 +17,7 @@ :root { --dc-primary: #110c45; --link-color: #4c49b0; - --footer-bg: white; + --footer-background: white; --dc-red-fade: #40d4e033; } diff --git a/static/js/apps/visualization/vis_type_configs/map_config.tsx b/static/js/apps/visualization/vis_type_configs/map_config.tsx index 29f1235c57..0533703f28 100644 --- a/static/js/apps/visualization/vis_type_configs/map_config.tsx +++ b/static/js/apps/visualization/vis_type_configs/map_config.tsx @@ -143,7 +143,6 @@ export function getChartArea( hover ); }} - showLoadingSpinner={true} /> diff --git a/static/js/apps/visualization/vis_type_configs/scatter_config.tsx b/static/js/apps/visualization/vis_type_configs/scatter_config.tsx index 45db3d220c..c3b6d8ae22 100644 --- a/static/js/apps/visualization/vis_type_configs/scatter_config.tsx +++ b/static/js/apps/visualization/vis_type_configs/scatter_config.tsx @@ -174,7 +174,6 @@ function getChartArea( showPlaceLabels: appContext.displayOptions.scatterPlaceLabels, showQuadrants: appContext.displayOptions.scatterQuadrants, }} - showLoadingSpinner={true} />
-

{props.title}

+

+ {props.isLoading ? ( + + ) : null} + {props.title} +

{props.headerChild}
{props.errorMsg ? ( diff --git a/static/js/components/tiles/bar_tile.tsx b/static/js/components/tiles/bar_tile.tsx index 487914d2e7..fe3947dcb3 100644 --- a/static/js/components/tiles/bar_tile.tsx +++ b/static/js/components/tiles/bar_tile.tsx @@ -119,11 +119,17 @@ export function BarTile(props: BarTilePropType): JSX.Element { const [barChartData, setBarChartData] = useState( null ); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (!barChartData || !_.isEqual(barChartData.props, props)) { (async () => { - const data = await fetchData(props); - setBarChartData(data); + setIsLoading(true); + try { + const data = await fetchData(props); + setBarChartData(data); + } finally { + setIsLoading(false); + } })(); } }, [props, barChartData]); @@ -138,18 +144,19 @@ export function BarTile(props: BarTilePropType): JSX.Element { useDrawOnResize(drawFn, chartContainerRef.current); return (
- { - /* 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 }