React Native d3方位角等积旋转不平滑

React Native d3 azimuthal equal-area rotate not smooth

我在 react-native 中做 d3 方位角等积投影,为此我使用了这个 example。它工作正常但我正在使用 panGestureHandler 更新旋转值这也有效但它不流畅并且需要时间来更新地图。

this the repo of this.

  1. 这是我更新旋转值的代码:

    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]);
    

这是我的完整代码

  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;

我解决这个问题的方法是:

  1. 我把国家json换成国家-110m.json';
  2. 删除 rotateX, rotateY 并替换为 translateX translateY
  3. 新的轮换代码是: .rotate([-translateX, translateY])

如有任何疑问,请检查我来自 Github

的完整源代码