Skip to content

Commit

Permalink
time series chart
Browse files Browse the repository at this point in the history
  • Loading branch information
nofurtherinformation committed May 16, 2024
1 parent 3d469e7 commit 91ed65b
Show file tree
Hide file tree
Showing 14 changed files with 383 additions and 182 deletions.
8 changes: 4 additions & 4 deletions app/tract/[tract]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,10 @@ const TractPage: React.FC<TractRouteProps> = async ({ params }) => {
</ul>
</div>
</div>
{/* <div className="my-8 h-[100vh] w-full bg-white p-8 shadow-xl">
<TimeseriesChart id={tract} />
</div> */}
<div className="my-8 h-[100vh] w-full bg-white p-8 shadow-xl overflow-y-auto">
<div className="my-8 w-full p-8 shadow-xl bg-white">
<TimeseriesChart id={tract} placeName={tractName}/>
</div>
<div className="my-8 w-full bg-white p-8 shadow-xl">
<StoreList id={tract} />
</div>
</div>
Expand Down
234 changes: 110 additions & 124 deletions components/LineChart/LineChart.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { withParentSize } from "@visx/responsive"
"use client"
import { scaleLinear, scaleTime } from "@visx/scale"
import { Axis, Tooltip } from "@visx/xychart"
import React, { useCallback } from "react"
import { DataRecord, ResponsiveXYChartProps } from "./types"
import { LineChartProps } from "./types"
import { timeSeriesConfig } from "utils/data/config"
import { Threshold } from "@visx/threshold"
import { LinePath } from "@visx/shape"
import { AxisBottom, AxisLeft } from "@visx/axis"
import { localPoint } from '@visx/event';
import { bisector } from '@visx/vendor/d3-array';
import { localPoint } from "@visx/event"
import { bisector } from "@visx/vendor/d3-array"
import { useParentSize } from "@cutting/use-get-parent-size"
import { curveLinear } from "@visx/curve"

const bisectDate = bisector<any, Date>((d: any) => new Date(d.date)).left;
const getMinMax = (data: DataRecord[], keys: string[]) => {
const minMax: Record<string, { min: number; max: number }> = {}
const bisectDate = bisector<any, Date>((d: any) => new Date(d.date)).left
const getMinMax = <T extends Record<string, any>>(data: Array<T>, keys: Array<keyof T>) => {
const minMax: Record<keyof T, { min: number; max: number }> = {} as any

keys.forEach((key) => {
minMax[key] = {
Expand Down Expand Up @@ -47,135 +49,119 @@ const getMinMax = (data: DataRecord[], keys: string[]) => {
}
return minMax
}
const niceNumberFormatter = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 2,
// compact
notation: "compact",
})

const yearKey = "year"
const ResponsiveXYChart = withParentSize<ResponsiveXYChartProps>(
({ parentWidth, parentHeight, data, timeseriesConfigKey, dataKey }) => {
// Define the margin for the chart
const margin = { top: 20, right: 30, bottom: 50, left: 40 }
const config = timeSeriesConfig[timeseriesConfigKey]
const xMax = parentWidth - margin.left - margin.right
const yMax = parentHeight - margin.top - margin.bottom
const parsedData = config.columns.map((c) => {
return {
year: new Date(`${c}-02-01`),
// @ts-ignore
average: data[`average_${c}`],
// @ts-ignore
median: data[`median_${c}`],
// @ts-ignore
q75: data[`q75_${c}`],
// @ts-ignore
q25: data[`q25_${c}`],
}
})

const minMax = getMinMax(parsedData, [yearKey, "average", "q25", "q75"])
const LineChart = <T extends Record<string, any>>({ data, dataKey, yearKey, parentRef, children }: LineChartProps<T>) => {
const { width, height } = useParentSize(parentRef)
const parentWidth = width ?? 100
const parentHeight = height ?? 100
// Define the margin for the chart
const margin = { top: 20, right: 30, bottom: 50, left: 40 }
const xMax = parentWidth
const yMax = parentHeight - margin.top - margin.bottom
const minMax = getMinMax(data, [dataKey, yearKey])

// Scales for the chart
const dateScale = scaleTime({
// @ts-ignore
domain: [minMax[yearKey].min, minMax[yearKey].max],
range: [margin.left, xMax + margin.left],
})
const valueScale = scaleLinear({
// @ts-ignore
domain: [minMax.q25.min, minMax.q75.max],
range: [yMax, margin.bottom],
nice: true,
})
const handleTooltip = useCallback(
(event: React.TouchEvent<SVGRectElement> | React.MouseEvent<SVGRectElement>) => {
const { x } = localPoint(event) || { x: 0 };
const x0 = dateScale.invert(x);
const index = bisectDate(parsedData, x0, 1);
const d0 = parsedData[index - 1];
const d1 = parsedData[index];
let d = d0;
if (d1 && d1.year) {
// @ts-ignore
d = x0.valueOf() - (d0.year).valueOf() > (d1.year).valueOf() - x0.valueOf() ? d1 : d0;
}
console.log(d, x)
// showTooltip({
// tooltipData: d,
// tooltipLeft: x,
// tooltipTop: stockValueScale(getStockValue(d)),
// });
},
[
// showTooltip, stockValueScale,
dateScale],
);
const modifiedChildren = React.Children.map(children||[], child =>
React.cloneElement(child, { xMax, yMax, yMin: margin.bottom, xMin: margin.left })
);
// Scales for the chart
const dateScale = scaleTime({
// @ts-ignore
domain: [minMax[yearKey].min, minMax[yearKey].max],
range: [margin.left, xMax],
})
const valueScale = scaleLinear({
// @ts-ignore
domain: [minMax[dataKey].min, minMax[dataKey].max],
range: [yMax, margin.bottom],
nice: true,
})

// const handleTooltip = useCallback(
// (event: React.TouchEvent<SVGRectElement> | React.MouseEvent<SVGRectElement>) => {
// const { x } = localPoint(event) || { x: 0 }
// const x0 = dateScale.invert(x)
// const index = bisectDate(parsedData, x0, 1)
// const d0 = parsedData[index - 1]
// const d1 = parsedData[index]
// let d = d0
// if (d1 && d1.year) {
// // @ts-ignore
// d = x0.valueOf() - d0.year.valueOf() > d1.year.valueOf() - x0.valueOf() ? d1 : d0
// }
// console.log(d, x)
// // showTooltip({
// // tooltipData: d,
// // tooltipLeft: x,
// // tooltipTop: stockValueScale(getStockValue(d)),
// // });
// },
// [
// // showTooltip, stockValueScale,
// dateScale,
// ]
// )

return (
<svg
width={parentWidth}
height={Math.max(parentHeight, 300)} // Ensure the chart has a minimum height
>
<LinePath
stroke={"black"}
strokeWidth={2}
data={parsedData}
x={(d) => dateScale(d.year) ?? 0}
y={(d) => valueScale(d.average) ?? 0}
/>
{/* @ts-ignore */}
<Threshold
return (
<svg
width={parentWidth}
height={Math.max(parentHeight, 300)} // Ensure the chart has a minimum height
>
{modifiedChildren}
<LinePath
stroke={"black"}
strokeWidth={2}
data={data}
x={(d) => dateScale(d[yearKey]) ?? 0}
y={(d) => valueScale(d[dataKey]) ?? 0}
curve={curveLinear}
shapeRendering="geometricPrecision"
/>
{/* @ts-ignore */}
{/* <Threshold
id="Q25-Q75 Range"
data={parsedData}
x={(d) => dateScale(d[yearKey])}
// y0={d => valueScale(0.2)}
// y1={d => valueScale(0.4)}
y0={d => valueScale(d["q75"])}
y1={d => valueScale(d["q25"])}
y0={(d) => valueScale(d["q75"])}
y1={(d) => valueScale(d["q25"])}
clipAboveTo={0}
// clipBelowTo={yMax}
// curve={curveBasis}
belowAreaProps={{
fill: 'violet',
fillOpacity: 0.,
}}
aboveAreaProps={{
fill: 'gray',
fillOpacity: 0.1,
}}
/>
<Axis orientation="bottom" tickFormat={(d) => d.getFullYear()} />
<AxisBottom top={yMax} scale={dateScale} numTicks={parentWidth > 520 ? 10 : 5} />
<AxisLeft scale={valueScale}
left={margin.left}
/>
<Tooltip
showVerticalCrosshair
showSeriesGlyphs
snapTooltipToDatumX
snapTooltipToDatumY
renderTooltip={({ tooltipData }) => (
<div>
{/* @ts-ignore */}
<div>{`Date: ${tooltipData?.nearestDatum?.datum?.[yearKey]?.getFullYear() + 1}`}</div>
{/* @ts-ignore */}
// clipBelowTo={yMax}
// curve={curveBasis}
belowAreaProps={{
fill: "violet",
fillOpacity: 0,
}}
aboveAreaProps={{
fill: "gray",
fillOpacity: 0.1,
}}
/> */}
<Axis orientation="bottom" tickFormat={(d) => d.getFullYear()} />
<AxisBottom top={yMax} scale={dateScale} numTicks={parentWidth > 520 ? 10 : 5} />
<AxisLeft scale={valueScale} left={margin.left} tickFormat={(num:any) => niceNumberFormatter.format(num)}/>
<Tooltip
showVerticalCrosshair
showSeriesGlyphs
snapTooltipToDatumX
snapTooltipToDatumY
renderTooltip={({ tooltipData }) => (
<div>
{JSON.stringify(tooltipData?.nearestDatum)}
{/* <div>{`Date: ${tooltipData?.nearestDatum?.datum?.[yearKey]?.getFullYear() + 1}`}</div>
<div>{`${"average"}: ${tooltipData?.nearestDatum?.datum?.["average"]}`}</div>
{/* @ts-ignore */}
<div>{`${"q25"}: ${tooltipData?.nearestDatum?.datum?.["q25"]}`}</div>
{/* @ts-ignore */}
<div>{`${"q75"}: ${tooltipData?.nearestDatum?.datum?.["q75"]}`}</div>
</div>
)}
/>
</svg>
)
}
)

const LineChart: React.FC<Omit<ResponsiveXYChartProps, "parentWidth" | "parentHeight">> = ({
data,
timeseriesConfigKey,
dataKey,
}) => {
return <ResponsiveXYChart data={data} timeseriesConfigKey={timeseriesConfigKey} dataKey={dataKey} />
<div>{`${"q75"}: ${tooltipData?.nearestDatum?.datum?.["q75"]}`}</div> */}
</div>
)}
/>
</svg>
)
}

export default LineChart
32 changes: 15 additions & 17 deletions components/LineChart/types.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { timeSeriesConfig } from "utils/data/config";
import React from "react";

export interface DataRecord {
[key: string]: any;
type DataType<T extends Record<string, any>> = Array<T>;
export interface DimensionProps {
xMax: number;
yMax: number;
xMin: number;
yMin: number;
}

export type TimeseriesConfigKey = keyof typeof timeSeriesConfig;
export type TimeseriesConfigEntry = typeof timeSeriesConfig[TimeseriesConfigKey];
export type TimeseriesConfigColumn = TimeseriesConfigEntry["columns"][number];
export type TimeseriesColumns = ('year' | 'median' | 'average'| 'q75'| 'q25')

export interface LineChartProps {
data: Record<TimeseriesColumns, number>
dataKey: TimeseriesColumns
timeseriesConfigKey: TimeseriesConfigKey;
}

export interface ResponsiveXYChartProps extends LineChartProps {
parentWidth: number;
parentHeight: number;
export interface LineChartProps<T extends Record<string, any>> {
data: DataType<T>
parentRef: React.RefObject<HTMLDivElement>
dataKey: keyof T
yearKey: keyof T
children?: React.ReactElement<DimensionProps> | React.ReactElement<DimensionProps>[];
// lowerBandKey: keyof T
// upperBandKey: keyof T
}
7 changes: 5 additions & 2 deletions components/StoreList/StoreList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ export const StoreList: React.FC<StoreListProps<string[]>> = ({
}

return (
<div className="prose max-w-full overflow-y-auto">
<div className="prose w-full max-w-full">
<h3>
Store{data?.length > 1 ? "s" : ""} in {title || "service area"}
</h3>
<table className="max-h-full w-full table-auto overflow-y-auto">
<div className="max-h-[50vh] w-full overflow-y-auto">

<table className="max-h-full w-full table-auto">
<thead>
<tr>
{columns.map((_col, i) => {
Expand All @@ -75,6 +77,7 @@ export const StoreList: React.FC<StoreListProps<string[]>> = ({
))}
</tbody>
</table>
</div>
</div>
)
}
Loading

0 comments on commit 91ed65b

Please sign in to comment.