1

I am doing the d3 azimuthal equal-area projection in react-native, i used this example for this. its working fine but im updating rotate values using panGestureHandler this is also working but it's not smooth and it's take time to update map.

this the repo of this.

  1. this is the code where i update rotate values:

    const countryPaths = useMemo(() => {
    const clipAngle = 150;
    
    const projection = d3
      .geoAzimuthalEqualArea()
      // .rotate([0, -90])
      .rotate([rotateX, rotateY])
      .fitSize([mapExtent, mapExtent], {
        type: 'FeatureCollection',
        features: COUNTRIES,
      })
      .clipAngle(clipAngle)
      .translate([dimensions.width / 2, mapExtent / 2]);
    
    const geoPath = d3.geoPath().projection(projection);
    
    const windowPaths = COUNTRIES.map(geoPath);
    
    return windowPaths;
    }, [dimensions, rotateX, rotateY]);
    

enter image description here

here is my complete code

  1. App.js
import React, {useState, useMemo, useEffect, useRef} from 'react';
import {
  StyleSheet,
  View,
  Dimensions,
  Animated,
  PanResponder,
  Text,
  SafeAreaView,
} from 'react-native';

import Map from './components/Map';

import COLORS from './constants/Colors';
import movingAverage from './functions/movingAverage';
import * as d3 from 'd3';
import covidData_raw from './assets/data/who_data.json';

export default function App(props) {
  const dimensions = Dimensions.get('window');
  const [stat, setStat] = useState('avg_confirmed');
  const [date, setDate] = useState('2020-04-24');

  //Data Manipulation
  const covidData = useMemo(() => {
    const countriesAsArray = Object.keys(covidData_raw).map((key) => ({
      name: key,
      data: covidData_raw[key],
    }));

    const windowSize = 7;

    const countriesWithAvg = countriesAsArray.map((country) => ({
      name: country.name,
      data: [...movingAverage(country.data, windowSize)],
    }));

    const onlyCountriesWithData = countriesWithAvg.filter(
      (country) => country.data.findIndex((d, _) => d[stat] >= 10) != -1,
    );

    return onlyCountriesWithData;
  }, []);

  const maxY = useMemo(() => {
    return d3.max(covidData, (country) => d3.max(country.data, (d) => d[stat]));
  }, [stat]);

  const colorize = useMemo(() => {
    const colorScale = d3
      .scaleSequentialSymlog(d3.interpolateReds)
      .domain([0, maxY]);

    return colorScale;
  });

  return (
    <SafeAreaView>
      <View>
        <Map
          dimensions={dimensions}
          data={covidData}
          date={date}
          colorize={colorize}
          stat={stat}
        />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: COLORS.primary,
    alignItems: 'center',
    justifyContent: 'center',
  },
  rotateView: {
    width: 300,
    height: 300,
    backgroundColor: 'black',
    shadowOpacity: 0.2,
  },
});

  1. map.js
import React, {useMemo, useState, useEffect} from 'react';
import {StyleSheet, View, Animated, PanResponder} from 'react-native';

//LIBRARIES
import Svg, {G, Path, Circle} from 'react-native-svg';
import * as d3 from 'd3';
import {
  PanGestureHandler,
  PinchGestureHandler,
  State,
} from 'react-native-gesture-handler';

//CONSTANTS
import {COUNTRIES} from '../constants/CountryShapes';
import COLORS from '../constants/Colors';

//COMPONENTS
import Button from './Button';

const Map = (props) => {
  const [countryList, setCountryList] = useState([]);
  const [translateX, setTranslateX] = useState(0);
  const [translateY, setTranslateY] = useState(0);
  const [lastTranslateX, setLastTranslateX] = useState(0);
  const [lastTranslateY, setLastTranslateY] = useState(0);
  const [buttonOpacity, _] = useState(new Animated.Value(0));
  const [scale, setScale] = useState(1);
  const [prevScale, setPrevScale] = useState(1);
  const [lastScaleOffset, setLastScaleOffset] = useState(0);

  const [rotateX, setrotateX] = useState();
  const [rotateY, setrotateY] = useState();

  const {dimensions, data, date, colorize, stat} = props;

  //Gesture Handlers
  const panStateHandler = (event) => {
    if (event.nativeEvent.oldState === State.UNDETERMINED) {
      setLastTranslateX(translateX);
      setLastTranslateY(translateY);
    }

    if (event.nativeEvent.oldState === State.ACTIVE) {
      Animated.timing(buttonOpacity, {
        toValue: 1,
        duration: 1000,
        useNativeDriver: true,
      }).start();
    }
  };

  const panGestureHandler = (event) => {
    console.log('event', event.nativeEvent);
    setrotateX(event.nativeEvent.x);
    setrotateX(event.nativeEvent.y);
    setTranslateX(-event.nativeEvent.translationX / scale + lastTranslateX);
    setTranslateY(-event.nativeEvent.translationY / scale + lastTranslateY);
  };

  const pinchStateHandler = (event) => {
    if (event.nativeEvent.oldState === State.UNDETERMINED) {
      setLastScaleOffset(-1 + scale);
    }

    if (event.nativeEvent.oldState === State.ACTIVE) {
      Animated.timing(buttonOpacity, {
        toValue: 1,
        duration: 1000,
        useNativeDriver: true,
      }).start();
    }
  };

  const pinchGestureHandler = (event) => {
    if (
      event.nativeEvent.scale + lastScaleOffset >= 1 &&
      event.nativeEvent.scale + lastScaleOffset <= 5
    ) {
      setPrevScale(scale);
      setScale(event.nativeEvent.scale + lastScaleOffset);
      setTranslateX(
        translateX -
          (event.nativeEvent.focalX / scale -
            event.nativeEvent.focalX / prevScale),
      );
      setTranslateY(
        translateY -
          (event.nativeEvent.focalY / scale -
            event.nativeEvent.focalY / prevScale),
      );
    }
  };

  //Initialize Map Transforms
  const initializeMap = () => {
    setTranslateX(0);
    setTranslateY(0);
    setScale(1);
    setPrevScale(1);
    setLastScaleOffset(0);
    Animated.timing(buttonOpacity, {
      toValue: 0,
      duration: 1000,
      useNativeDriver: true,
    }).start();
  };

  //Create Map Paths
  const mapExtent = useMemo(() => {
    return dimensions.width > dimensions.height / 2
      ? dimensions.height / 2
      : dimensions.width;
  }, [dimensions]);

    const countryPaths = useMemo(() => {
    const clipAngle = 150;

    const projection = d3
      .geoAzimuthalEqualArea()
      // .rotate([0, -90])
      .rotate([rotateX, rotateY])
      .fitSize([mapExtent, mapExtent], {
        type: 'FeatureCollection',
        features: COUNTRIES,
      })
      .clipAngle(clipAngle)
      .translate([dimensions.width / 2, mapExtent / 2]);

    const geoPath = d3.geoPath().projection(projection);

    const windowPaths = COUNTRIES.map(geoPath);

    return windowPaths;
    }, [dimensions, rotateX, rotateY]);



  useEffect(() => {
    setCountryList(
      countryPaths.map((path, i) => {
        const curCountry = COUNTRIES[i].properties.name;

        const isCountryNameInData = data.some(
          (country) => country.name === curCountry,
        );

        const curCountryData = isCountryNameInData
          ? data.find((country) => country.name === curCountry)['data']
          : null;

        const isDataAvailable = isCountryNameInData
          ? curCountryData.some((data) => data.date === date)
          : false;

        const dateIndex = isDataAvailable
          ? curCountryData.findIndex((x) => x.date === date)
          : null;

        return (
          <Path
            key={COUNTRIES[i].properties.name}
            d={path}
            stroke={COLORS.greyLight}
            strokeOpacity={0.3}
            strokeWidth={0.6}
            fill={
              isDataAvailable
                ? colorize(curCountryData[dateIndex][stat])
                : COLORS.greyLight
            }
            opacity={isDataAvailable ? 1 : 0.4}
          />
        );
      }),
    );
  }, [rotateX, rotateY]);

  return (
    <View>
      <PanGestureHandler
        onGestureEvent={(e) => panGestureHandler(e)}
        onHandlerStateChange={(e) => panStateHandler(e)}>
        <PinchGestureHandler
          onGestureEvent={(e) => pinchGestureHandler(e)}
          onHandlerStateChange={(e) => pinchStateHandler(e)}>
          <Svg
            width={dimensions.width}
            height={dimensions.height / 2}
            style={styles.svg}>
            <G
            // transform={`scale(${scale}) translate(${-translateX},${-translateY})`}
            >
              <Circle
                cx={dimensions.width / 2}
                cy={mapExtent / 2}
                r={mapExtent / 2}
                fill={COLORS.lightPrimary}
              />
              {countryList.map((x) => x)}
            </G>
          </Svg>
        </PinchGestureHandler>
      </PanGestureHandler>
    </View>
  );
};

const styles = StyleSheet.create({
  svg: {},
  rotateView: {
    width: 100,
    height: 400,
    backgroundColor: 'black',
    shadowOffset: {height: 1, width: 1},
    shadowOpacity: 0.2,
  },
});

export default Map;

yathavan
  • 2,051
  • 2
  • 18
  • 25
  • Do you need that level of detail in your topojson? It has the Pitcairn in it - won't show up on the map and with population of a couple dozen, probably doesn't need to. Simplifying your geometry, even at the expense of larger areas (eg Island of Rhodes) or areas of detail (eg Alaskan Panhandle) won't make the map look any different, but will likely be the easiest way to avoid delays in reprojecting every point in your geometry. Try [this file](https://gist.githubusercontent.com/Andrew-Reid/970ac3c522b1546fb7cede4b52111726/raw/edd5bb93b220f918f7a4b40d4dfb4baf120ede27/countries-simplified.json) – Andrew Reid Jan 03 '21 at 20:19
  • @yathavan can you create a mininum reproducible example on stackblitz or elsewhere? – Joe - GMapsBook.com Jan 08 '21 at 19:09
  • 1
    @JoeSorocin i fixed it, i wil post my answer soon, thank you for your response – yathavan Jan 09 '21 at 06:18

1 Answers1

0

how i fixed this issue is :

  1. I cange countries json to countries-110m.json';
  2. delete the rotateX, rotateY and replace by translateX translateY
  3. new rotate code is: .rotate([-translateX, translateY])

if any doubts please check my complete source code from Github

yathavan
  • 2,051
  • 2
  • 18
  • 25