@@ -113,20 +113,20 @@ const MODE_DEFAULTS: Record<string, ModeConfig> = {
113113 'lot' : { max : 300 , maxHeight : 4500 } ,
114114 'unit' : { max : 300 , maxHeight : 4500 } ,
115115 'census-block:per_sqft' : { max : 20 , maxHeight : 2000 , stops : {
116- dark : [ { value : 0 , color : [ 96 , 96 , 96 ] } , { value : 1.5 , color : [ 255 , 0 , 0 ] } , { value : 12 , color : [ 0 , 255 , 0 ] } ] ,
117- light : [ { value : 0 , color : [ 255 , 255 , 255 ] } , { value : 1.5 , color : [ 255 , 71 , 71 ] } , { value : 12 , color : [ 0 , 214 , 0 ] } ] ,
116+ dark : [ { value : 0 , color : [ 96 , 96 , 96 ] } , { value : 1.5 , color : [ 255 , 0 , 0 ] } , { value : 4 , color : [ 255 , 217 , 26 ] } , { value : 12 , color : [ 0 , 255 , 0 ] } ] ,
117+ light : [ { value : 0 , color : [ 255 , 255 , 255 ] } , { value : 1.5 , color : [ 255 , 71 , 71 ] } , { value : 4 , color : [ 230 , 190 , 0 ] } , { value : 12 , color : [ 0 , 214 , 0 ] } ] ,
118118 } } ,
119119 'census-block:per_capita' :{ max : 15000 , maxHeight : 4500 , scale : 'sqrt' , stops : {
120- dark : [ { value : 0 , color : [ 96 , 96 , 96 ] } , { value : 3000 , color : [ 255 , 0 , 0 ] } , { value : 10000 , color : [ 0 , 255 , 0 ] } ] ,
121- light : [ { value : 0 , color : [ 255 , 255 , 255 ] } , { value : 3000 , color : [ 255 , 71 , 71 ] } , { value : 10000 , color : [ 0 , 214 , 0 ] } ] ,
120+ dark : [ { value : 0 , color : [ 96 , 96 , 96 ] } , { value : 3000 , color : [ 255 , 0 , 0 ] } , { value : 5500 , color : [ 255 , 217 , 26 ] } , { value : 10000 , color : [ 0 , 255 , 0 ] } ] ,
121+ light : [ { value : 0 , color : [ 255 , 255 , 255 ] } , { value : 3000 , color : [ 255 , 71 , 71 ] } , { value : 5500 , color : [ 230 , 190 , 0 ] } , { value : 10000 , color : [ 0 , 214 , 0 ] } ] ,
122122 } } ,
123123 'ward:per_sqft' : { max : 10 , maxHeight : 5500 , scale : 'linear' , stops : {
124- dark : [ { value : 0 , color : [ 96 , 96 , 96 ] } , { value : 4.9 , color : [ 255 , 0 , 0 ] } , { value : 8.6 , color : [ 0 , 255 , 0 ] } ] ,
125- light : [ { value : 0 , color : [ 255 , 255 , 255 ] } , { value : 4.9 , color : [ 255 , 71 , 71 ] } , { value : 8.6 , color : [ 0 , 214 , 0 ] } ] ,
124+ dark : [ { value : 0 , color : [ 96 , 96 , 96 ] } , { value : 4.9 , color : [ 255 , 0 , 0 ] } , { value : 6.5 , color : [ 255 , 217 , 26 ] } , { value : 8.6 , color : [ 0 , 255 , 0 ] } ] ,
125+ light : [ { value : 0 , color : [ 255 , 255 , 255 ] } , { value : 4.9 , color : [ 255 , 71 , 71 ] } , { value : 6.5 , color : [ 230 , 190 , 0 ] } , { value : 8.6 , color : [ 0 , 214 , 0 ] } ] ,
126126 } } ,
127127 'ward:per_capita' : { max : 9000 , maxHeight : 5400 , scale : 'sqrt' , stops : {
128- dark : [ { value : 0 , color : [ 96 , 96 , 96 ] } , { value : 1273.7 , color : [ 255 , 0 , 0 ] } , { value : 6834.5 , color : [ 0 , 255 , 0 ] } ] ,
129- light : [ { value : 0 , color : [ 255 , 255 , 255 ] } , { value : 1273.7 , color : [ 255 , 71 , 71 ] } , { value : 6834.5 , color : [ 0 , 214 , 0 ] } ] ,
128+ dark : [ { value : 0 , color : [ 96 , 96 , 96 ] } , { value : 1273.7 , color : [ 255 , 0 , 0 ] } , { value : 3500 , color : [ 255 , 217 , 26 ] } , { value : 6834.5 , color : [ 0 , 255 , 0 ] } ] ,
129+ light : [ { value : 0 , color : [ 255 , 255 , 255 ] } , { value : 1273.7 , color : [ 255 , 71 , 71 ] } , { value : 3500 , color : [ 230 , 190 , 0 ] } , { value : 6834.5 , color : [ 0 , 214 , 0 ] } ] ,
130130 } } ,
131131}
132132const SS_PREFIX = 'jc-taxes:'
@@ -149,8 +149,8 @@ const optScaleParam: Param<ScaleType | undefined> = {
149149}
150150
151151const boolParam : Param < boolean > = {
152- decode : ( s : string | undefined ) => s === '1 ',
153- encode : ( v : boolean ) => v ? '1' : undefined as unknown as string ,
152+ decode : ( s : string | undefined ) => s !== '0 ',
153+ encode : ( v : boolean ) => v ? undefined as unknown as string : '0' ,
154154}
155155
156156const optNumParam : Param < number | undefined > = {
@@ -183,6 +183,7 @@ export default function App() {
183183 const [ metricMode , setMetricModeRaw ] = useUrlState ( 'metric' , stringParam ( 'per_sqft' ) )
184184 const [ wardGeom , setWardGeom ] = useUrlState ( 'wg' , stringParam ( 'merged' ) )
185185 const [ wardLabels , setWardLabels ] = useUrlState ( 'wl' , boolParam )
186+ const [ extruded , setExtruded ] = useUrlState ( '3d' , boolParam )
186187
187188 const hasPopulation = aggregateMode === 'census-block' || aggregateMode === 'ward'
188189 const modeKey = getModeKey ( aggregateMode , metricMode )
@@ -192,7 +193,15 @@ export default function App() {
192193 const maxVal = modeConf . max
193194 // Effective max height: URL value if explicitly set, else mode default
194195 const maxHeight = maxHeightRaw ?? modeConf . maxHeight
195- const heightScale = maxHeight / modeConf . max
196+ // Height scale: tallest bar = maxHeight. Derived from actual data max.
197+ const dataMax = useMemo ( ( ) => {
198+ if ( ! data || data . length === 0 ) return modeConf . max
199+ const field = metricMode === 'per_capita' ? 'paid_per_capita' : 'paid_per_sqft'
200+ let mx = 0
201+ for ( const f of data ) mx = Math . max ( mx , f . properties ?. [ field ] ?? 0 )
202+ return mx || modeConf . max
203+ } , [ data , metricMode , modeConf . max ] )
204+ const heightScale = maxHeight / dataMax
196205 // Freeze height scale while loading to prevent stale data rendered with new-mode elevation
197206 const stableHeightScaleRef = useRef ( heightScale )
198207 if ( ! loading ) stableHeightScaleRef . current = heightScale
@@ -234,14 +243,15 @@ export default function App() {
234243 } , [ aggregateMode , metricMode , maxHeight , maxHeightRaw , colorScaleRaw , hasCustomStops , resetColorStopsRaw ] )
235244
236245 const setAggregateMode = useCallback ( ( newAgg : string ) => {
246+ if ( newAgg === aggregateMode ) return
237247 setLoading ( true )
238248 const newMetric = ( newAgg === 'census-block' || newAgg === 'ward' ) ? metricMode : 'per_sqft'
239249 switchToMode ( newAgg , newMetric )
240250 setAggregateModeRaw ( newAgg )
241251 if ( ! ( newAgg === 'census-block' || newAgg === 'ward' ) && metricMode === 'per_capita' ) {
242252 setMetricModeRaw ( 'per_sqft' )
243253 }
244- } , [ switchToMode , metricMode ] )
254+ } , [ aggregateMode , switchToMode , metricMode ] )
245255
246256 const setMetricMode = useCallback ( ( newMetric : string ) => {
247257 switchToMode ( aggregateMode , newMetric )
@@ -372,7 +382,7 @@ export default function App() {
372382
373383 for ( let wi = 0 ; wi < wardLabelInfo . length ; wi ++ ) {
374384 const { metricVal, rings } = wardLabelInfo [ wi ]
375- const elev = Math . min ( metricVal * heightScale , maxHeight )
385+ const elev = metricVal * heightScale
376386
377387 for ( const ring of rings ) {
378388 const n = ring . length
@@ -587,10 +597,10 @@ export default function App() {
587597 id : 'parcels' ,
588598 data : effectiveData ?? [ ] ,
589599 filled : true ,
590- extruded : true ,
591- wireframe : true ,
600+ extruded,
601+ wireframe : extruded ,
592602 getFillColor,
593- getElevation : ( f ) => Math . min ( getMetricValue ( f ) * stableHeightScaleRef . current , maxHeight ) ,
603+ getElevation : extruded ? ( f ) => getMetricValue ( f ) * stableHeightScaleRef . current : 0 ,
594604 getLineColor : lineColor ,
595605 lineWidthMinPixels : 1 ,
596606 pickable : true ,
@@ -617,7 +627,7 @@ export default function App() {
617627 } ,
618628 updateTriggers : {
619629 getFillColor : [ year , maxVal , colorStops , colorScale , hoveredId , selectedId , aggregateMode , actualTheme , metricMode , staleData ] ,
620- getElevation : [ year , stableHeightScaleRef . current , aggregateMode , metricMode , maxHeight ] ,
630+ getElevation : [ year , stableHeightScaleRef . current , aggregateMode , metricMode ] ,
621631 getLineColor : [ actualTheme ] ,
622632 } ,
623633 } ) ,
@@ -770,7 +780,15 @@ export default function App() {
770780 Ward labels
771781 </ label >
772782 </ > ) }
773- < label style = { { display : 'flex' , alignItems : 'center' , gap : 6 } } >
783+ < label style = { { display : 'flex' , alignItems : 'center' , gap : 8 } } >
784+ < input
785+ type = "checkbox"
786+ checked = { extruded }
787+ onChange = { ( e ) => setExtruded ( e . target . checked ) }
788+ />
789+ 3D
790+ </ label >
791+ { extruded && < label style = { { display : 'flex' , alignItems : 'center' , gap : 6 } } >
774792 Max height:{ ' ' }
775793 < input
776794 type = "number"
@@ -795,7 +813,7 @@ export default function App() {
795813 ↺
796814 </ button >
797815 ) }
798- </ label >
816+ </ label > }
799817 < label style = { { display : 'flex' , alignItems : 'center' , gap : 8 } } >
800818 Pitch: { Math . round ( viewState . pitch ) } °
801819 < input
@@ -822,6 +840,7 @@ export default function App() {
822840 position : 'absolute' ,
823841 top : 10 ,
824842 left : 10 ,
843+ zIndex : 1 ,
825844 background : 'var(--panel-bg)' ,
826845 color : 'var(--text-primary)' ,
827846 padding : '10px 15px' ,
@@ -901,7 +920,6 @@ export default function App() {
901920 ? '0 0 4px rgba(0,0,0,0.9), 0 0 8px rgba(0,0,0,0.6)'
902921 : '0 0 4px rgba(255,255,255,0.9), 0 0 8px rgba(255,255,255,0.6)' ,
903922 pointerEvents : 'none' ,
904- zIndex : 2 ,
905923 } }
906924 >
907925 { label . text }
0 commit comments