From e497212b79d914894a049a682f3b56406398b748 Mon Sep 17 00:00:00 2001 From: "J.D. House" Date: Tue, 6 Jan 2026 08:25:52 -0600 Subject: [PATCH 1/3] wip --- .../explorer/ExplorerWrapperPage.jsx | 14 +- .../visualization/treemap/ExplorerTreemap.jsx | 315 +++++++++--------- 2 files changed, 158 insertions(+), 171 deletions(-) diff --git a/src/js/components/explorer/ExplorerWrapperPage.jsx b/src/js/components/explorer/ExplorerWrapperPage.jsx index 1050d2271e..194f3131d2 100644 --- a/src/js/components/explorer/ExplorerWrapperPage.jsx +++ b/src/js/components/explorer/ExplorerWrapperPage.jsx @@ -18,16 +18,15 @@ const propTypes = { showShareIcon: PropTypes.bool }; -const defaultProps = { - showShareIcon: false -}; - require('pages/explorer/explorerPage.scss'); const slug = 'explorer'; const emailSubject = 'USAspending.gov Federal Spending Explorer'; -const ExplorerWrapperPage = (props) => { +const ExplorerWrapperPage = ({ + showShareIcon = false, + children +}) => { const dispatch = useDispatch(); const handleShareDispatch = (url) => { dispatch(showModal(url)); @@ -45,7 +44,7 @@ const ExplorerWrapperPage = (props) => { classNames="usa-da-explorer-page" title="Spending Explorer" metaTagProps={explorerPageMetaTags} - toolBarComponents={props.showShareIcon ? [ + toolBarComponents={showShareIcon ? [ @@ -53,13 +52,12 @@ const ExplorerWrapperPage = (props) => {
- {props.children} + {children}
); }; ExplorerWrapperPage.propTypes = propTypes; -ExplorerWrapperPage.defaultProps = defaultProps; export default ExplorerWrapperPage; diff --git a/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx b/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx index 143d6e4ab5..a96aa626d7 100644 --- a/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx +++ b/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx @@ -3,7 +3,7 @@ * Created by Kevin Li 8/17/17 */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { hierarchy, treemap, treemapBinary } from 'd3-hierarchy'; @@ -14,72 +14,147 @@ import { measureTreemapHeader, measureTreemapValue } from 'helpers/textMeasureme import LoadingSpinner from 'components/sharedComponents/LoadingSpinner'; import TreemapCell from 'components/sharedComponents/TreemapCell'; -import { isEqual } from 'lodash-es'; const propTypes = { isLoading: PropTypes.bool, width: PropTypes.number, height: PropTypes.number, + total: PropTypes.number, data: PropTypes.object, goDeeper: PropTypes.func, showTooltip: PropTypes.func, hideTooltip: PropTypes.func, - goToUnreported: PropTypes.func, - activeSubdivision: PropTypes.object + goToUnreported: PropTypes.func + // activeSubdivision: PropTypes.object }; -const defaultProps = { - height: 530 -}; +export const ExplorerTreemap = ({ + isLoading, + width, + height, + total, + data, + goDeeper, + showTooltip, + hideTooltip, + goToUnreported +}) => { + const [virtualChart, setVirtualChart] = useState([]); + + const truncateText = (text, type, maxWidth) => { + // calculate the text width of the full label + let label = text; + let labelWidth = 0; + if (type === 'title') { + labelWidth = measureTreemapHeader(text); + } + else if (type === 'subtitle') { + labelWidth = measureTreemapValue(text); + } -export default class ExplorerTreemap extends React.Component { - constructor(props) { - super(props); - this.state = { - chartReady: false, - virtualChart: [] - }; + // check to see if the full label will fit + if (labelWidth > maxWidth) { + // label won't fit, let's cut it down + // determine the average character pixel width + const characterWidth = Math.ceil(labelWidth / text.length); + // give an additional 30px for the ellipsis + const availableWidth = maxWidth - 30; + let availableLength = Math.floor(availableWidth / characterWidth); + if (availableLength < 1) { + // we must show at least one character + availableLength = 1; + } - this.selectedCell = this.selectedCell.bind(this); - } + // substring the label to this length + if (availableLength < text.length) { + label = `${label.substring(0, availableLength)}...`; + } + } - componentDidMount() { - this.buildVirtualChart(this.props); - } + return label; + }; + + const buildVirtualCell = (item, scale, localTotal) => { + console.log("checking item ==== > ", item); + console.log("checking scale ==== > ", scale); + console.log("checking localTotal ==== > ", localTotal); - componentDidUpdate(prevProps) { - if (!isEqual(prevProps.data, this.props.data)) { - this.buildVirtualChart(this.props); + const localHeight = item.y1 - item.y0; + const localWidth = item.x1 - item.x0; + + const amount = item.data.amount; + const percent = amount / localTotal; + const percentString = `${(Math.round(percent * 1000) / 10)}%`; + + // the available width is 40px less than the box width to account for 20px of padding on + // each side + const usableWidth = localWidth - 40; + let name = item.data.name; + const isUnreported = item.data.name === "Unreported Data"; + if (isUnreported) { + name = "Unreported Data*"; } - else if (prevProps.width !== this.props.width || prevProps.height !== this.props.height) { - this.buildVirtualChart(this.props); + const title = truncateText(name, 'title', usableWidth); + const subtitle = truncateText(percentString, 'subtitle', usableWidth); + let color = scale(amount); + + if (isUnreported) { + // use the gray color for unreported data, instead of the usual calculated + // color + color = 'rgb(103,103,103)'; } - } - buildVirtualChart(props) { - const data = props.data.toJS(); + const cell = { + width: localWidth, + height: localHeight, + x: item.x0, + y: item.y0, + data: Object.assign({}, item.data, { + percent, + percentString + }), + color, + title: { + text: title, + x: (localWidth / 2), + y: (localHeight / 2) - 5 // shift it up slightly so the full title + subtitle combo is vertically centered + }, + subtitle: { + text: subtitle, + x: (localWidth / 2), + y: (localHeight / 2) + 15 // to place the subtitle below the title + } + }; + + return cell; + }; - const total = props.total; + const selectedCell = (id, title) => { + goDeeper(id, title); + }; + const buildVirtualChart = () => { + const localData = data.toJS(); + console.log("checking local data ===== ", localData); // parse the inbound data into D3's treemap hierarchy structure - const treemapData = hierarchy({ children: data }) + const treemapData = hierarchy({ children: localData }) .sum((d) => d.amount); // tell D3 how to extract the monetary value out of the object // set up a function for generating the treemap of the specified size and style const tree = treemap() - .size([props.width, props.height]) + .size([width, height]) .tile(treemapBinary) .paddingInner(5) .round(true); // generate the treemap and calculate the individual boxes const treeItems = tree(treemapData).leaves(); + console.log("checking treeItems ===== ", treeItems); - if (treeItems.length === 0 || data.length === 0) { + if (treeItems.length === 0 || localData.length === 0) { // we have no data, so don't draw a chart - this.setState({ - virtualChart: [] - }); + setVirtualChart([]); + return; } @@ -101,145 +176,59 @@ export default class ExplorerTreemap extends React.Component { // we can now begin creating the individual treemap cells const cells = []; treeItems.forEach((item) => { - const cell = this.buildVirtualCell(item, scale, total); + const cell = buildVirtualCell(item, scale, total); cells.push(cell); }); - this.setState({ - virtualChart: cells - }); - } - - buildVirtualCell(data, scale, total) { - const height = data.y1 - data.y0; - const width = data.x1 - data.x0; - - const amount = data.data.amount; - const percent = amount / total; - const percentString = `${(Math.round(percent * 1000) / 10)}%`; - - // the available width is 40px less than the box width to account for 20px of padding on - // each side - const usableWidth = width - 40; - let name = data.data.name; - const isUnreported = data.data.name === "Unreported Data"; - if (isUnreported) { - name = "Unreported Data*"; - } - const title = this.truncateText(name, 'title', usableWidth); - const subtitle = this.truncateText(percentString, 'subtitle', usableWidth); - let color = scale(amount); + setVirtualChart(cells); + }; - if (isUnreported) { - // use the gray color for unreported data, instead of the usual calculated - // color - color = 'rgb(103,103,103)'; - } - - const cell = { - width, - height, - x: data.x0, - y: data.y0, - data: Object.assign({}, data.data, { - percent, - percentString - }), - color, - title: { - text: title, - x: (width / 2), - y: (height / 2) - 5 // shift it up slightly so the full title + subtitle combo is vertically centered - }, - subtitle: { - text: subtitle, - x: (width / 2), - y: (height / 2) + 15 // to place the subtitle below the title - } - }; + useEffect(() => { + buildVirtualChart(); + }, [width, height, data]); - return cell; + if (width <= 0) { + return null; } - truncateText(text, type, maxWidth) { - // calculate the text width of the full label - let label = text; - let labelWidth = 0; - if (type === 'title') { - labelWidth = measureTreemapHeader(text); - } - else if (type === 'subtitle') { - labelWidth = measureTreemapValue(text); - } - - // check to see if the full label will fit - if (labelWidth > maxWidth) { - // label won't fit, let's cut it down - // determine the average character pixel width - const characterWidth = Math.ceil(labelWidth / text.length); - // give an additional 30px for the ellipsis - const availableWidth = maxWidth - 30; - let availableLength = Math.floor(availableWidth / characterWidth); - if (availableLength < 1) { - // we must show at least one character - availableLength = 1; - } - - // substring the label to this length - if (availableLength < text.length) { - label = `${label.substring(0, availableLength)}...`; - } - } - - return label; - } - - selectedCell(id, title) { - this.props.goDeeper(id, title); - } - - render() { - if (this.props.width <= 0) { - return null; - } - - const cells = this.state.virtualChart.map((cell) => ( - - )); - - let loadingMessage = null; - if (this.props.isLoading) { - loadingMessage = ( -
-
- -
Gathering your data...
-
Updating Spending Explorer.
-
This should only take a few moments...
-
+ const cells = virtualChart.map((cell) => ( + + )); + + let loadingMessage = null; + if (isLoading) { + loadingMessage = ( +
+
+ +
Gathering your data...
+
Updating Spending Explorer.
+
This should only take a few moments...
- ); - } - - return ( -
- {loadingMessage} - - {cells} -
); } -} + + return ( +
+ {loadingMessage} + + {cells} + +
+ ); +}; ExplorerTreemap.propTypes = propTypes; -ExplorerTreemap.defaultProps = defaultProps; + +export default ExplorerTreemap; + From 6d463c9683c83d73c7720c8d7da9935be4ee26c7 Mon Sep 17 00:00:00 2001 From: "J.D. House" Date: Tue, 6 Jan 2026 08:33:06 -0600 Subject: [PATCH 2/3] add in default height --- .../detail/visualization/treemap/ExplorerTreemap.jsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx b/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx index a96aa626d7..7612897d0d 100644 --- a/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx +++ b/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx @@ -31,7 +31,7 @@ const propTypes = { export const ExplorerTreemap = ({ isLoading, width, - height, + height = 530, total, data, goDeeper, @@ -75,10 +75,6 @@ export const ExplorerTreemap = ({ }; const buildVirtualCell = (item, scale, localTotal) => { - console.log("checking item ==== > ", item); - console.log("checking scale ==== > ", scale); - console.log("checking localTotal ==== > ", localTotal); - const localHeight = item.y1 - item.y0; const localWidth = item.x1 - item.x0; @@ -135,7 +131,6 @@ export const ExplorerTreemap = ({ const buildVirtualChart = () => { const localData = data.toJS(); - console.log("checking local data ===== ", localData); // parse the inbound data into D3's treemap hierarchy structure const treemapData = hierarchy({ children: localData }) .sum((d) => d.amount); // tell D3 how to extract the monetary value out of the object @@ -149,7 +144,6 @@ export const ExplorerTreemap = ({ // generate the treemap and calculate the individual boxes const treeItems = tree(treemapData).leaves(); - console.log("checking treeItems ===== ", treeItems); if (treeItems.length === 0 || localData.length === 0) { // we have no data, so don't draw a chart From f5f81cc541f3c3bea05d98ea6af04e5c4ad0a191 Mon Sep 17 00:00:00 2001 From: "J.D. House" Date: Wed, 7 Jan 2026 10:11:05 -0600 Subject: [PATCH 3/3] pr feedback changes --- .../explorer/detail/ExplorerDetailPage.jsx | 10 +++++----- .../visualization/treemap/ExplorerTreemap.jsx | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/js/components/explorer/detail/ExplorerDetailPage.jsx b/src/js/components/explorer/detail/ExplorerDetailPage.jsx index 5925122798..f16f02cb50 100644 --- a/src/js/components/explorer/detail/ExplorerDetailPage.jsx +++ b/src/js/components/explorer/detail/ExplorerDetailPage.jsx @@ -3,7 +3,7 @@ * Created by Kevin Li 8/16/17 */ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import DetailContentContainer from 'containers/explorer/detail/DetailContentContainer'; import ExplorerWrapperPage from '../ExplorerWrapperPage'; import ExplorerTooltip from './visualization/ExplorerTooltip'; @@ -22,14 +22,14 @@ const ExplorerDetailPage = () => { isAward: false }); - const showTooltipFn = (position, data) => { + const showTooltipFn = useCallback((position, data) => { setShowTooltip(true); setTooltip(Object.assign({}, position, data)); - }; + }, []); - const hideTooltipFn = () => { + const hideTooltipFn = useCallback(() => { setShowTooltip(false); - }; + }, []); let tooltipUi = null; diff --git a/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx b/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx index 7612897d0d..a99fe6a943 100644 --- a/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx +++ b/src/js/components/explorer/detail/visualization/treemap/ExplorerTreemap.jsx @@ -3,7 +3,7 @@ * Created by Kevin Li 8/17/17 */ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { hierarchy, treemap, treemapBinary } from 'd3-hierarchy'; @@ -74,7 +74,7 @@ export const ExplorerTreemap = ({ return label; }; - const buildVirtualCell = (item, scale, localTotal) => { + const buildVirtualCell = useCallback((item, scale, localTotal) => { const localHeight = item.y1 - item.y0; const localWidth = item.x1 - item.x0; @@ -123,13 +123,13 @@ export const ExplorerTreemap = ({ }; return cell; - }; + }, []); - const selectedCell = (id, title) => { + const selectedCell = useCallback((id, title) => { goDeeper(id, title); - }; + }, [goDeeper]); - const buildVirtualChart = () => { + const buildVirtualChart = useCallback(() => { const localData = data.toJS(); // parse the inbound data into D3's treemap hierarchy structure const treemapData = hierarchy({ children: localData }) @@ -175,11 +175,11 @@ export const ExplorerTreemap = ({ }); setVirtualChart(cells); - }; + }, [data, width, height, buildVirtualCell, total]); useEffect(() => { buildVirtualChart(); - }, [width, height, data]); + }, [buildVirtualChart]); if (width <= 0) { return null;