1- import { useEffect , useMemo , useRef , useState } from 'react'
1+ import { useEffect , useRef , useState } from 'react'
22import './App.css'
33/* Imports */
4- import { Button , Container , Graphics , HeatLegend , Root , color as am5color , p50 as am5p50 , p100 as am5p100 , percent as am5percent , Slider , Label } from "@amcharts/amcharts5/index" ;
4+ import { Button , Container , Graphics , HeatLegend , Root , color as am5color , p50 as am5p50 , p100 as am5p100 , percent as am5percent , Slider , Label , Circle } from "@amcharts/amcharts5/index" ;
55import * as am5map from "@amcharts/amcharts5/map" ;
66import am5geodataWorldLow from "@amcharts/amcharts5-geodata/worldLow" ;
77// import am5geodataUSALow from "@amcharts/amcharts5-geodata/usaLow";
88import am5themes_Animated from "@amcharts/amcharts5/themes/Animated" ;
9- import dayjs , { Dayjs } from 'dayjs' ;
9+ import dayjs from 'dayjs' ;
1010import { DataItem , IComponentDataItem } from '@amcharts/amcharts5/.internal/core/render/Component' ;
11- import { FLATTENED_TARIFF_DATA , FlattenedTariffDataEntry , TariffType } from './data' ;
11+ import { FLATTENED_TARIFF_DATA , FlattenedTariffDataEntry , TariffType , Valid2DigitCountryCodesWithoutUSA } from './data' ;
1212
1313
14- interface EffectiveTariff extends IComponentDataItem , FlattenedTariffDataEntry {
15- }
16-
1714const dates = [ ...FLATTENED_TARIFF_DATA . keys ( ) ] ;
1815
1916function formatLargeMoney ( value : number ) {
@@ -37,34 +34,40 @@ function formatLargeMoney(value: number) {
3734 return `$${ valueTo100ths } ${ specifier } ` ;
3835}
3936
37+ type AmChartsData = ( { id : Valid2DigitCountryCodesWithoutUSA , approxValueFormatted ?: string } & Partial < Pick < FlattenedTariffDataEntry , Exclude < keyof FlattenedTariffDataEntry , "id" | "date" | "type" > > > ) & IComponentDataItem ;
4038
41- function AmChartsMap ( {
42- field = "percentValue" , type = "announced"
43- } : {
44- field : Exclude < keyof FlattenedTariffDataEntry , "id" | "date" | "type" > ,
45- type : TariffType
46- } ) {
47- const [ dateIndex , setDateIndex ] = useState < number > ( 0 ) ;
48- const [ chart , setChart ] = useState < am5map . MapChart | null > ( null ) ;
39+ function AmChartsMap ( ) {
40+ const [ field , setField ] = useState < Exclude < keyof FlattenedTariffDataEntry , "id" | "date" | "type" > > ( "percentValue" ) ;
41+ const [ type , setType ] = useState < TariffType > ( "announced" ) ;
42+ const [ dateIndex , setDateIndex ] = useState < number > ( - 1 ) ;
4943 const [ polygonSeries , setPolygonSeries ] = useState < am5map . MapPolygonSeries | null > ( null ) ;
5044 const dateIndexRef = useRef ( dateIndex ) ; // Need to pass this value into a callback in the use effect
45+ const oldData = useRef < AmChartsData [ ] > ( [ ] ) ;
5146
5247 useEffect ( ( ) => {
5348 if ( polygonSeries ) {
54- const mappedData = FLATTENED_TARIFF_DATA . get ( dates [ dateIndex ] ) ! . filter ( ( d ) => d . type === type ) . map ( d => {
49+ const mappedData : AmChartsData [ ] = dateIndex === - 1 ? [ ] : FLATTENED_TARIFF_DATA . get ( dates [ dateIndex ] ) ! . filter ( ( d ) => d . type === type ) . map ( d => {
5550 return {
5651 id : d . id ,
5752 [ field ] : d [ field ] ,
5853 approxValueFormatted : field === "approximateValueOfImportsImpacted" ? formatLargeMoney ( d . approximateValueOfImportsImpacted ) : undefined ,
5954 } ;
6055 } ) ;
61- console . log ( mappedData ) ;
6256 polygonSeries . data . setAll ( mappedData ) ;
63- if ( chart ) {
64- polygonSeries . mapPolygons . values . forEach ( polygon => polygon . appear ( 1000 ) ) ;
65- }
57+ const diffItems = mappedData . filter ( ( d ) => {
58+ const oldItem = oldData . current . find ( ( old ) => old . id === d . id ) ;
59+ if ( ! oldItem ) {
60+ return true ;
61+ }
62+ const isDifferent = oldItem [ field ] !== d [ field ] ;
63+ return isDifferent ;
64+ } ) . map ( d => d . id ) ;
65+ oldData . current = mappedData ;
66+ polygonSeries . mapPolygons . values
67+ . filter ( polygon => polygon . dataItem && diffItems . includes ( ( polygon . dataItem as DataItem < AmChartsData > ) . get ( "id" ) ) )
68+ . forEach ( polygon => polygon . appear ( 1000 ) ) ;
6669 }
67- } , [ polygonSeries , chart , dateIndex , field , type ] ) ;
70+ } , [ polygonSeries , dateIndex , field , type ] ) ;
6871
6972 useEffect ( ( ) => {
7073 if ( polygonSeries ) {
@@ -94,7 +97,6 @@ function AmChartsMap({
9497 layout : root . horizontalLayout
9598 } ) ) ;
9699
97- setChart ( chart ) ;
98100 // Create polygon series
99101 const polygonSeries = chart . series . push ( am5map . MapPolygonSeries . new ( root , {
100102 // geoJSON: am5geodataUSALow,
@@ -133,7 +135,7 @@ function AmChartsMap({
133135 } ) ) ;
134136
135137 polygonSeries . mapPolygons . template . events . on ( "pointerover" , function ( ev ) {
136- heatLegend . showValue ( Number ( ( ev . target . dataItem as DataItem < EffectiveTariff > ) . get ( field ) ) ) ;
138+ heatLegend . showValue ( Number ( ( ev . target . dataItem as DataItem < AmChartsData > ) . get ( field ) ) ) ;
137139 } ) ;
138140
139141 heatLegend . startLabel . setAll ( {
@@ -150,13 +152,14 @@ function AmChartsMap({
150152 polygonSeries . events . on ( "datavalidated" , function ( ) {
151153 // console.log({ low: polygonSeries.getPrivate("valueLow"), high: polygonSeries.getPrivate("valueHigh") });
152154 // const low = polygonSeries.getPrivate("valueLow")!;
153- const high = polygonSeries . getPrivate ( "valueHigh" ) ! ;
155+ const high = polygonSeries . getPrivate ( "valueHigh" ) ?? 0 ;
154156 // heatLegend.set("startValue", polygonSeries.getPrivate("valueLow"));
155157 // heatLegend.set("endValue", Math.max(polygonSeries.getPrivate("valueHigh") ?? 0, 100));
156158 if ( field !== "percentValue" ) {
159+ const endValue = Math . max ( high , 1_000_000 ) ;
157160 heatLegend . setAll ( {
158- endValue : Math . max ( high , 100 ) ,
159- endText : formatLargeMoney ( high ) ,
161+ endValue,
162+ endText : formatLargeMoney ( endValue ) ,
160163 } ) ;
161164 }
162165 } )
@@ -173,8 +176,8 @@ function AmChartsMap({
173176 y : am5p100 ,
174177 centerX : am5p50 ,
175178 centerY : am5p100 ,
176- x : am5p50 ,
177- width : am5percent ( 90 ) ,
179+ x : am5percent ( 50 ) ,
180+ width : am5percent ( 135 ) ,
178181 layout : root . horizontalLayout ,
179182 paddingBottom : 10
180183 } ) ) ;
@@ -189,7 +192,6 @@ function AmChartsMap({
189192 } ) ) ;
190193
191194 const slider = container . children . push ( Slider . new ( root , {
192- //width: am5.percent(80),
193195 orientation : "horizontal" ,
194196 start : 0 ,
195197 centerY : am5p50
@@ -219,7 +221,7 @@ function AmChartsMap({
219221
220222 slider . events . on ( "rangechanged" , function ( ) {
221223 const lastDateMillis = dayjs ( dates [ dates . length - 1 ] ) . valueOf ( ) ;
222- const firstDateMillis = dayjs ( dates [ 0 ] ) . valueOf ( ) ;
224+ const firstDateMillis = dayjs ( "2025-01-20" ) . valueOf ( ) ; // Inauguration date
223225 // var year = firstYear + Math.round(slider.get("start", 0) * (lastYear - firstYear));
224226 const nextDateMillis = firstDateMillis + Math . round ( slider . get ( "start" , 0 ) * ( lastDateMillis - firstDateMillis ) ) ;
225227 const nextDate = dayjs ( nextDateMillis ) ;
@@ -229,10 +231,86 @@ function AmChartsMap({
229231 if ( potentialNextDateIndex === - 1 ) {
230232 setDateIndex ( dates . length - 1 ) ;
231233 } else {
232- setDateIndex ( potentialNextDateIndex === 0 ? 0 : ( potentialNextDateIndex - 1 ) ) ;
234+ setDateIndex ( potentialNextDateIndex - 1 ) ;
235+ }
236+ } ) ;
237+
238+ const typeContainer = chart . children . push ( Container . new ( root , {
239+ layout : root . horizontalLayout ,
240+ x : am5percent ( 85 ) ,
241+ centerX : am5p100 ,
242+ y : am5percent ( 100 ) ,
243+ dy : - 40
244+ } ) ) ;
245+
246+ // Add labels and controls
247+ typeContainer . children . push ( Label . new ( root , {
248+ centerY : am5p50 ,
249+ text : "Announced"
250+ } ) ) ;
251+
252+ const typeSwitchButton = typeContainer . children . push ( Button . new ( root , {
253+ themeTags : [ "switch" ] ,
254+ centerY : am5p50 ,
255+ icon : Circle . new ( root , {
256+ themeTags : [ "icon" ]
257+ } ) ,
258+ active : type === "effective" ,
259+ } ) ) ;
260+
261+ typeSwitchButton . on ( "active" , function ( ) {
262+ if ( ! typeSwitchButton . get ( "active" ) ) {
263+ setType ( "announced" ) ;
264+ } else {
265+ setType ( "effective" ) ;
233266 }
234267 } ) ;
235268
269+ typeContainer . children . push (
270+ Label . new ( root , {
271+ centerY : am5p50 ,
272+ text : "Effective"
273+ } )
274+ ) ;
275+
276+ const fieldContainer = chart . children . push ( Container . new ( root , {
277+ layout : root . horizontalLayout ,
278+ x : am5percent ( 15 ) ,
279+ centerX : am5p100 ,
280+ y : am5percent ( 100 ) ,
281+ dy : - 40
282+ } ) ) ;
283+
284+ // Add labels and controls
285+ fieldContainer . children . push ( Label . new ( root , {
286+ centerY : am5p50 ,
287+ text : "Percent"
288+ } ) ) ;
289+
290+ const fieldSwitchButton = fieldContainer . children . push ( Button . new ( root , {
291+ themeTags : [ "switch" ] ,
292+ centerY : am5p50 ,
293+ icon : Circle . new ( root , {
294+ themeTags : [ "icon" ]
295+ } ) ,
296+ active : field === "approximateValueOfImportsImpacted" ,
297+ } ) ) ;
298+
299+ fieldSwitchButton . on ( "active" , function ( ) {
300+ if ( ! fieldSwitchButton . get ( "active" ) ) {
301+ setField ( "percentValue" )
302+ } else {
303+ setField ( "approximateValueOfImportsImpacted" )
304+ }
305+ } ) ;
306+
307+ fieldContainer . children . push (
308+ Label . new ( root , {
309+ centerY : am5p50 ,
310+ text : "Approximate Value of Imports"
311+ } )
312+ ) ;
313+
236314 return ( ) => {
237315 root . dispose ( ) ;
238316 }
@@ -244,8 +322,8 @@ function AmChartsMap({
244322
245323function App ( ) {
246324 return (
247- < AmChartsMap field = 'approximateValueOfImportsImpacted' type = 'announced' />
325+ < AmChartsMap />
248326 )
249327}
250328
251- export default App
329+ export default App ;
0 commit comments