Source

src/components/Panels/ReportBuilder/PageComponents/MapReport.jsx

import React, { useRef, useMemo } from 'react'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import CircularProgress from '@mui/material/CircularProgress'
import useMapData from '../../../../hooks/useMapData'
import useGetReportViewport from '../../../../hooks/useGetReportViewport'
import { ControlPopover, LegendInner, MapSection } from '../../..'
import {
    PanelItemContainer,
    GrabTarget,
    DeleteBlock,
    // widthOptions,
    // heightOptions,
} from './PageComponentsLayout'
import { findIn } from '../../../../utils'
import colors from '../../../../config/colors'
import countyNames from '../../../../meta/countyNames'
import { colorScales } from '../../../../config/scales'
import { defaultData } from '../../../../config/defaults'
import { Box } from '@mui/material'
import { HoverButtonsContainer } from '../InterfaceComponents/HoverButtonsContainer'
import { paramsSelectors } from '../../../../stores/paramsStore'
import { reportSelectors } from '../../../../stores/reportStore'
const { selectPrintStatus } = reportSelectors
const { selectDates, selectVariableTree, selectVariables } = paramsSelectors

const defaultMapParams = {
    mapType: 'natural_breaks',
    bins: {
        bins: [],
        breaks: [],
    },
    binMode: '',
    fixedScale: null,
    nBins: 8,
    vizType: '2D',
    activeGeoid: '',
    overlay: '',
    resource: '',
    dotDensityParams: {
        raceCodes: {
            1: true,
            2: true,
            3: true,
            4: true,
            5: false,
            6: false,
            7: false,
            8: true,
        },
        colorCOVID: false,
        backgroundTransparency: 0.01,
    },
}

const MapTitle = styled.div`
    position: absolute;
    left: 0;
    top: 0;
    z-index: 500;
    width: 100%;
    background: rgba(255, 255, 255, 0.85);
    padding: 0.25rem 0.5rem;
    font-size: 1rem;
`
const MapAttribution = styled(MapTitle)`
    left: initial;
    top: initial;
    right: 0;
    bottom: 0;
    width: auto;
    font-size: 0.65rem;
`
export const NoInteractionGate = ({ children, style }) => (
    <div
        style={{
            pointerEvents: 'none !important',
            userSelect: 'none !importabnt',
            width: '100%',
            height: '100%',
            ...style,
        }}
    >
        {children}
    </div>
)

const getColorScale = (mapType, dataParams) => {
    switch (mapType) {
        case 'natural_breaks':
            return (
                colorScales[dataParams.colorScale] || colorScales.natural_breaks
            )
        case 'hinge15_breaks':
            return colorScales.hinge15_breaks
        case 'lisa':
            return colorScales.lisa
        default:
            return []
    }
}

/**
 * Memoized wrapper report item for a map in the report builder. This component
 * shouldn't be called directly, but through the report spec and builder.
 *
 * @category Components/ReportBuilder
 * @param {Object} props
 * @param {number} props.geoid GEOID of county to display center map on
 * @param {number} props.pageIdx Index of the page this map is on
 * @param {string} props.itemId ID of the report item
 * @param {function} props.handleChange Function to partially change a report
 *   item (props: Partial<ReportItem>) => void See Report Slice for more
 * @param {function} props.handleRemove Function to remove this report item from
 *   the report () => void
 * @param {number} props.dateIndex Date index to display, days since January
 *   21st, 2020
 * @param {string} props.variable Name of variable to display
 * @param {string} props.mapType Type of map to display 2D | 3D | dotDensity
 * @param {string} props.scale Scale to display - county | neighbors | region |
 *   state | national
 * @param {function} props.loadedCallback Function after chart is loaded
 *   (isLoaded: boolean) => void
 */
function ReportMap({
    geoid = 17031,
    pageIdx = 0,
    itemId = '',
    handleChange,
    handleRemove,
    // date,
    dateIndex,
    // reportName = '',
    variable = 'Percent Fully Vaccinated',
    mapType = 'natural_breaks',
    scale = 'county',
    loadedCallback = () => {},
}) {
    const dates = useSelector(selectDates)
    const isPrinting = useSelector(selectPrintStatus)
    const variableTree = useSelector(selectVariableTree)
    const variables = useSelector(selectVariables)

    const variableList = Object.keys(variableTree)
        .filter((f) => !f.includes('HEADER'))
        .map((f) => ({ label: f, value: f }))

    const dataParams = {
        ...(findIn(variables, 'variableName', variable) || {}),
        nIndex: dateIndex,
    }

    const mapParams = {
        ...defaultMapParams,
        mapType,
        binMode: 'dynamic',
        colorScale: getColorScale(mapType, dataParams),
    }

    const mapContainerRef = useRef(null)
    const { width: mapWidth, height: mapHeight } =
        mapContainerRef?.current?.getBoundingClientRect() || {}

    const currentData = defaultData
    const mapIdCol = 'GEOID'

    const [
        currentMapGeography,
        currentMapData, // { cartogramData, cartogramCenter, cartogramDataSnapshot },
        ,
        currentMapID,
        currentBins,
        currentHeightScale,
        isLoading,
        geojsonData,
        currIndex,
        isBackgroundLoading,
    ] = useMapData({
        dataParams,
        mapParams,
        currentData,
    })

    const onLoad = isPrinting
        ? () => {
              setTimeout(() => {
                  loadedCallback(!isLoading)
              }, 2500)
          }
        : () => {
              loadedCallback(!isLoading)
          }

    const [
        countyViewport,
        neighborsViewport,
        secondOrderNeighborsViewport,
        stateViewport,
        nationalViewport,
        // neighbors,
        // secondOrderNeighbors,
        // stateNeighbors,
    ] = useGetReportViewport({
        geoid,
        currentData,
        geojsonData,
        mapIdCol,
        mapWidth,
        mapHeight,
    })

    const currViewport = {
        county: countyViewport,
        neighbors: neighborsViewport,
        region: secondOrderNeighborsViewport,
        state: stateViewport,
        national: nationalViewport,
    }[scale]

    const mapInner = useMemo(() => {
        if (isLoading || isBackgroundLoading) {
            return null
        } else {
            return (
                <NoInteractionGate>
                    <MapSection
                        currentMapGeography={currentMapGeography}
                        currentMapData={currentMapData}
                        currentMapID={currentMapID}
                        currentHeightScale={currentHeightScale}
                        isLoading={isLoading}
                        mapParams={mapParams}
                        currentData={currentData}
                        currIdCol={mapIdCol}
                        theme={'light'}
                        manualViewport={currViewport}
                        hoverGeoid={geoid}
                        highlightGeoids={[geoid]}
                        onLoad={onLoad}
                    />
                </NoInteractionGate>
            )
        }
    }, [
        isLoading,
        isBackgroundLoading,
        JSON.stringify(currViewport),
        currentMapID,
    ])

    return (
        <PanelItemContainer ref={mapContainerRef}>
            {!!(isLoading || isBackgroundLoading) ? (
                <Box
                    sx={{
                        position: 'absolute',
                        left: '50%',
                        top: '50%',
                        transform: 'translate(-50%, -50%)',
                        width: '3em',
                        height: '3em',
                    }}
                >
                    <CircularProgress />
                </Box>
            ) : (
                <>
                    <MapTitle>
                        <h4>{dataParams.variableName}</h4>
                        <LegendInner
                            colorScale={mapParams.colorScale}
                            currentBins={currentBins?.bins || []}
                            fixedScale={dataParams.fixedScale}
                        />
                    </MapTitle>
                    <MapAttribution>
                        Source: New York Times via US Covid Atlas :: Date:{' '}
                        {dates[currIndex]}
                    </MapAttribution>
                    {mapInner}
                </>
            )}
            <HoverButtonsContainer>
                <ControlPopover
                    className="hover-buttons"
                    inline
                    size={4}
                    iconColor={colors.strongOrange}
                    controlElements={[
                        {
                            type: 'header',
                            content: 'Controls for Text Report Block',
                        },
                        {
                            type: 'helperText',
                            content: 'Select the data to display on the chart.',
                        },
                        {
                            type: 'comboBox',
                            content: {
                                label: 'Search County',
                                items: countyNames,
                            },
                            action: ({ value }) =>
                                handleChange({ geoid: value }),
                            value: geoid,
                        },
                        {
                            type: 'select',
                            content: {
                                label: 'Change Variable',
                                items: variableList,
                            },
                            action: (e) =>
                                handleChange({ variable: e.target.value }),
                        },
                        {
                            type: 'select',
                            content: {
                                label: 'Change Map Type',
                                items: [
                                    {
                                        label: 'Natural Breaks',
                                        value: 'natural_breaks',
                                    },
                                    {
                                        label: 'Box Map',
                                        value: 'hinge15_breaks',
                                    },
                                    {
                                        label: 'Hotspot',
                                        value: 'lisa',
                                    },
                                ],
                            },
                            action: (e) =>
                                handleChange({ mapType: e.target.value }),
                        },
                        {
                            type: 'select',
                            content: {
                                label: 'Change View Scale',
                                items: [
                                    {
                                        value: 'county',
                                        label: 'County',
                                    },
                                    {
                                        value: 'neighbors',
                                        label: 'Neighboring Counties',
                                    },
                                    {
                                        value: 'region',
                                        label: 'Region',
                                    },
                                    {
                                        value: 'state',
                                        label: 'State',
                                    },
                                    {
                                        value: 'national',
                                        label: 'National (Lower 48)',
                                    },
                                ],
                            },
                            action: (e) =>
                                handleChange({ scale: e.target.value }),
                        },
                    ]}
                />
                <GrabTarget
                    iconColor={colors.strongOrange}
                    className="hover-buttons"
                />

                <DeleteBlock
                    iconColor={colors.strongOrange}
                    className="hover-buttons"
                    onClick={() => handleRemove(pageIdx, itemId)}
                />
            </HoverButtonsContainer>
        </PanelItemContainer>
    )
}

export default React.memo(ReportMap)