Add travel times
This commit is contained in:
parent
275e5afac6
commit
75950f0b1b
4 changed files with 122 additions and 38 deletions
|
|
@ -5,12 +5,13 @@ import { H3HexagonLayer } from '@deck.gl/geo-layers';
|
|||
import { IconLayer } from '@deck.gl/layers';
|
||||
import type { PickingInfo } from '@deck.gl/core';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import type { HexagonData, ViewState, ViewChangeParams, Bounds, POI } from '../types';
|
||||
import type { HexagonData, ViewState, ViewChangeParams, Bounds, POI, ColorMode } from '../types';
|
||||
|
||||
interface MapProps {
|
||||
data: HexagonData[];
|
||||
pois: POI[];
|
||||
onViewChange: (params: ViewChangeParams) => void;
|
||||
colorMode: ColorMode;
|
||||
}
|
||||
|
||||
// Twemoji CDN base URL
|
||||
|
|
@ -119,26 +120,41 @@ function interpolateColor(
|
|||
];
|
||||
}
|
||||
|
||||
function priceToColor(price: number | null | undefined): [number, number, number] {
|
||||
if (price == null || isNaN(price)) return [128, 128, 128]; // Gray for missing data
|
||||
function scaleToColor(
|
||||
value: number | null | undefined,
|
||||
scale: ColorStop[]
|
||||
): [number, number, number] {
|
||||
if (value == null || isNaN(value)) return [128, 128, 128];
|
||||
|
||||
// Clamp to scale range
|
||||
if (price <= COLOR_SCALE[0].price) return COLOR_SCALE[0].color;
|
||||
if (price >= COLOR_SCALE[COLOR_SCALE.length - 1].price) {
|
||||
return COLOR_SCALE[COLOR_SCALE.length - 1].color;
|
||||
}
|
||||
if (value <= scale[0].price) return scale[0].color;
|
||||
if (value >= scale[scale.length - 1].price) return scale[scale.length - 1].color;
|
||||
|
||||
// Find the two colors to interpolate between
|
||||
for (let i = 0; i < COLOR_SCALE.length - 1; i++) {
|
||||
const lower = COLOR_SCALE[i];
|
||||
const upper = COLOR_SCALE[i + 1];
|
||||
if (price >= lower.price && price <= upper.price) {
|
||||
const t = (price - lower.price) / (upper.price - lower.price);
|
||||
for (let i = 0; i < scale.length - 1; i++) {
|
||||
const lower = scale[i];
|
||||
const upper = scale[i + 1];
|
||||
if (value >= lower.price && value <= upper.price) {
|
||||
const t = (value - lower.price) / (upper.price - lower.price);
|
||||
return interpolateColor(lower.color, upper.color, t);
|
||||
}
|
||||
}
|
||||
|
||||
return COLOR_SCALE[COLOR_SCALE.length - 1].color;
|
||||
return scale[scale.length - 1].color;
|
||||
}
|
||||
|
||||
function priceToColor(price: number | null | undefined): [number, number, number] {
|
||||
return scaleToColor(price, COLOR_SCALE);
|
||||
}
|
||||
|
||||
// Journey time color scale: green (short) -> yellow -> orange -> red (long)
|
||||
const JOURNEY_COLOR_SCALE: ColorStop[] = [
|
||||
{ price: 0, color: [46, 204, 113] }, // Green
|
||||
{ price: 30, color: [241, 196, 15] }, // Yellow
|
||||
{ price: 60, color: [231, 76, 60] }, // Red
|
||||
{ price: 120, color: [142, 68, 173] }, // Purple
|
||||
];
|
||||
|
||||
function journeyTimeToColor(minutes: number | null | undefined): [number, number, number] {
|
||||
return scaleToColor(minutes, JOURNEY_COLOR_SCALE);
|
||||
}
|
||||
|
||||
function zoomToResolution(zoom: number): number {
|
||||
|
|
@ -193,7 +209,7 @@ interface Dimensions {
|
|||
height: number;
|
||||
}
|
||||
|
||||
export default function Map({ data, pois, onViewChange }: MapProps) {
|
||||
export default function Map({ data, pois, onViewChange, colorMode }: MapProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [viewState, setViewState] = useState<ViewState>(INITIAL_VIEW);
|
||||
const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
|
||||
|
|
@ -256,7 +272,13 @@ export default function Map({ data, pois, onViewChange }: MapProps) {
|
|||
id: 'h3-hexagons',
|
||||
data,
|
||||
getHexagon: (d) => d.h3,
|
||||
getFillColor: (d) => priceToColor(d.avg_price),
|
||||
getFillColor: (d) =>
|
||||
colorMode === 'journey_time'
|
||||
? journeyTimeToColor(d.median_journey_minutes)
|
||||
: priceToColor(d.avg_price),
|
||||
updateTriggers: {
|
||||
getFillColor: colorMode,
|
||||
},
|
||||
extruded: false,
|
||||
pickable: true,
|
||||
opacity: 0.5,
|
||||
|
|
@ -278,15 +300,26 @@ export default function Map({ data, pois, onViewChange }: MapProps) {
|
|||
onHover: handlePoiHover,
|
||||
}),
|
||||
],
|
||||
[data, pois, handlePoiHover]
|
||||
[data, pois, handlePoiHover, colorMode]
|
||||
);
|
||||
|
||||
|
||||
// Tooltip for hexagons only (POIs use MapLibre popup)
|
||||
const getTooltip = useCallback(({ object }: { object?: HexagonData }) => {
|
||||
if (!object || !('h3' in object)) return null;
|
||||
|
||||
const hex = object as HexagonData;
|
||||
const journeyLines: string[] = [];
|
||||
if (hex.median_pt_quick_minutes != null)
|
||||
journeyLines.push(`🚇 Quick PT: ${hex.median_pt_quick_minutes} min`);
|
||||
if (hex.median_pt_easy_minutes != null)
|
||||
journeyLines.push(`🚌 Easy PT: ${hex.median_pt_easy_minutes} min`);
|
||||
if (hex.median_cycling_minutes != null)
|
||||
journeyLines.push(`🚲 Cycling: ${hex.median_cycling_minutes} min`);
|
||||
const journeyTimeHtml =
|
||||
journeyLines.length > 0
|
||||
? `<div style="color: #0066cc; margin-top: 4px; font-size: 12px;">${journeyLines.join('<br/>')}</div>`
|
||||
: '';
|
||||
|
||||
return {
|
||||
html: `<div style="padding: 8px; font-size: 14px;">
|
||||
<strong>Avg: £${hex.avg_price?.toLocaleString() || 'N/A'}</strong>
|
||||
|
|
@ -294,6 +327,7 @@ export default function Map({ data, pois, onViewChange }: MapProps) {
|
|||
${hex.count} sales<br/>
|
||||
Range: £${hex.min_price?.toLocaleString()} - £${hex.max_price?.toLocaleString()}
|
||||
</div>
|
||||
${journeyTimeHtml}
|
||||
</div>`,
|
||||
style: {
|
||||
backgroundColor: 'white',
|
||||
|
|
@ -327,9 +361,7 @@ export default function Map({ data, pois, onViewChange }: MapProps) {
|
|||
<strong>
|
||||
{getTooltipEmoji(popupInfo.category)} {popupInfo.name}
|
||||
</strong>
|
||||
<div className="text-gray-500 text-xs">
|
||||
{popupInfo.category.replace(/_/g, ' ')}
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs">{popupInfo.category.replace(/_/g, ' ')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue