Skip to content

Commit

Permalink
Merge pull request #2258 from cfpb/2252-add-graph-note
Browse files Browse the repository at this point in the history
[Graphs] Data Table Expandable & Data Issue Note
  • Loading branch information
Michaeldremy committed Aug 12, 2024
2 parents 667aabe + b8671e0 commit 3f0255e
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 4 deletions.
5 changes: 5 additions & 0 deletions src/common/ExpandableSection.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@
padding: 1em 1.3em;
width: 100%;
}

/* Table CSS within Graphs */
.expandable-section .bold-font {
font-family: SourceSansProBold;
}
24 changes: 20 additions & 4 deletions src/data-browser/graphs/Graph.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { hideUnselectedLines } from './utils/graphHelpers'
import { useCallback, useRef } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import Highcharts from 'highcharts'
import HighchartsExport from 'highcharts/modules/exporting'
import HighchartsExportData from 'highcharts/modules/export-data'
import HighchartsAccessibility from 'highcharts/modules/accessibility'
import HighchartsReact from 'highcharts-react-official'
import { hideUnselectedLines } from './utils/graphHelpers'
import useGraphLoading from './useGraphLoading'
import { AvoidJumpToDataTable } from './highchartsCustomModules'
import { DataTable } from './quarterly/DataTable'
import { useTableData } from './quarterly/useTableData'

HighchartsExport(Highcharts) // Enable export to image
HighchartsExportData(Highcharts) // Enable export of underlying data
Expand All @@ -22,6 +24,10 @@ Highcharts.setOptions({
export const Graph = ({ options, loading, seriesForURL }) => {
const chartRef = useRef()

// No longer using HighCharts Data Table and instead we are generating our own to allow for more custom styling
const { tableData, isSeriesVisible, generateTableData } =
useTableData(chartRef)

/**
* Note about onLoad:
*
Expand All @@ -33,17 +39,26 @@ export const Graph = ({ options, loading, seriesForURL }) => {
*
* See the following link for context on how these images are generated.
* https://github.com/highcharts/highcharts-react/issues/315
*
* Additionally we are now manually generating the data table instead of having HighCharts automatically creating it
*/
const onLoad = useCallback(
(ref) => {
(chart) => {
// Hide series based on URL query parameters
hideUnselectedLines(ref, seriesForURL)
hideUnselectedLines(chart, seriesForURL)
generateTableData()
},
[seriesForURL],
)

useGraphLoading(chartRef, loading, options)

useEffect(() => {
if (chartRef.current && chartRef.current.chart) {
generateTableData()
}
}, [options])

return (
<div className='graph-wrapper'>
<div className='export-charts'>
Expand All @@ -54,6 +69,7 @@ export const Graph = ({ options, loading, seriesForURL }) => {
callback={onLoad}
/>
</div>
{isSeriesVisible && <DataTable tableData={tableData} />}
</div>
)
}
21 changes: 21 additions & 0 deletions src/data-browser/graphs/highchartsConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@ export const baseConfig = {
sourceHeight: 595,
sourceWidth: 842,
showTable: hmda_charts.config.showDataTable, // OPTION: Show/hide underlying data (already available in export menu)
buttons: { // Removed Data Table Button as we are using our own custom DataTable component
contextButton: {
menuItems: [
'viewFullscreen',
'printChart',
'separator',
'downloadPNG',
'downloadJPEG',
'downloadPDF',
'downloadSVG',
'separator',
'downloadCSV',
'downloadXLS',
],
},
},
},
navigation: {
buttonOptions: {
enabled: true,
},
},
colors: seriesColors,
chart: {
Expand Down
36 changes: 36 additions & 0 deletions src/data-browser/graphs/quarterly/DataTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react'
import { ExpandableSection } from '../../../common/ExpandableSection'

export const DataTable = ({ tableData }) => {
if (!tableData) return null

return (
<div style={{ marginTop: '.8em' }}>
<ExpandableSection label={'Data Table'}>
<table>
<thead>
<tr>
{tableData.headers.map((header, index) => (
<th key={index}>{header}</th>
))}
</tr>
</thead>
<tbody>
{tableData.rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td
key={cellIndex}
className={cell.includes('Q') ? 'bold-font' : ''}
>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</ExpandableSection>
</div>
)
}
7 changes: 7 additions & 0 deletions src/data-browser/graphs/quarterly/SectionGraphs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,13 @@ export const SectionGraphs = ({
yAxis: [selectedGraphData?.yLabel],
})}
/>

<div className='alert' style={{ marginTop: '1.7em' }}>
<p style={{ margin: 0 }}>
Data points which would be generated based on fewer than 100 loans are
not shown. This may cause gaps in the graph and blanks in the table data.
</p>
</div>
</>
)
}
89 changes: 89 additions & 0 deletions src/data-browser/graphs/quarterly/useTableData.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useState, useCallback, useEffect } from 'react'
import { useSelector } from 'react-redux'
import Highcharts from 'highcharts'
import { graphs } from '../slice'
import { SELECTED_GRAPH_DATA } from '../slice/graphConfigs'

export const useTableData = (chartRef) => {
const [tableData, setTableData] = useState(null)
const [isSeriesVisible, setIsSeriesVisible] = useState(true)

const graphsConfigStore = useSelector(({ graphsConfig }) => graphsConfig)
const selectedGraphData = graphs.getConfig(
graphsConfigStore,
SELECTED_GRAPH_DATA,
)

const formatNumber = (value, decimalPrecision) => {
if (value === null || value === undefined || isNaN(value)) return ''
const formatted = parseFloat(value).toFixed(decimalPrecision)
const [wholePart, decimalPart] = formatted.split('.')
const withCommas = wholePart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return decimalPart ? `${withCommas}.${decimalPart}` : withCommas
}

const generateTableData = useCallback(() => {
if (chartRef.current && chartRef.current.chart) {
const chart = chartRef.current.chart
const series = chart.series.filter((s) => s.visible)

if (series.length === 0) {
setIsSeriesVisible(false)
setTableData(null)
return null
}

const categories = chart.xAxis[0].categories || []
const decimalPrecision = selectedGraphData?.decimalPrecision || 0

const tableData = {
headers: ['Year Quarter', ...series.map((s) => s.name)],
rows: categories.map((category, index) => {
return [
category,
...series.map((s) => {
const value = s.data[index] ? s.data[index].y : ''
return formatNumber(value, decimalPrecision)
}),
]
}),
}

setTableData(tableData)
setIsSeriesVisible(true)

// Hide the HighCharts data table
const highchartsDataTable = document.querySelector('.highcharts-data-table');
if (highchartsDataTable) {
highchartsDataTable.style.display = 'none';
}
}
}, [selectedGraphData, chartRef])

useEffect(() => {
if (chartRef.current && chartRef.current.chart) {
const chart = chartRef.current.chart
generateTableData()

// Use Highcharts.addEvent for proper event binding
const hideHandler = Highcharts.addEvent(
chart.series,
'hide',
generateTableData,
)
const showHandler = Highcharts.addEvent(
chart.series,
'show',
generateTableData,
)

return () => {
// Clean up event listeners
if (hideHandler) hideHandler()
if (showHandler) showHandler()
}
}
}, [chartRef])

return { tableData, isSeriesVisible, generateTableData }
}

0 comments on commit 3f0255e

Please sign in to comment.