Skip to content

useChartPressState with useChartTransformState #548

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
marcoshernanz opened this issue Apr 15, 2025 · 1 comment
Open

useChartPressState with useChartTransformState #548

marcoshernanz opened this issue Apr 15, 2025 · 1 comment

Comments

@marcoshernanz
Copy link

It currently seems impossible to create a chart that's both pannable and pressable. When you use useChartPressState with useChartTransformState, the panning works like a charm but the press state position is wrong - it doesn't take into account the panning.

Surely someone has stumbled upon this problem in the past, right? If so, did you find any workaround to fix it other than creating a custom PanResponder to handle press state?

@Mirnyjj
Copy link

Mirnyjj commented Apr 21, 2025

Hi, look at the implementation, maybe it will help.

`import SpinerRendering from '@/components/SpinerRendering';
import {DataPoint, HeadData, Telemetry} from '@/models/dataType';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {ScrollView, TouchableOpacity, View} from 'react-native';
import inter from '../../../assets/fonts/inter-medium.ttf';
import {CartesianChart, Line, useChartPressState} from 'victory-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {useFont} from '@shopify/react-native-skia';
import * as Haptics from 'expo-haptics';
import {formatXLabels, generateTickData} from './lineChartFunctions';
import {ActiveValueIndicator} from './ActiveValueIndicator';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import {useDerivedValue, useSharedValue} from 'react-native-reanimated';
import {StockArea} from './StockArea';

type Props = {
fetchData: Telemetry[][];
timeRange: number;
headData: HeadData;
};
const initChartPressStateFirst = {x: 0, y: {y1: 0, y2: 0, y3: 0}};
const initChartPressStateSecond = {x: 0, y: {y1: 0, y2: 0, y3: 0}};

const LineChartRendering = ({fetchData, timeRange, headData}: Props) => {
const font = useFont(inter, 12);

const scrollOffset = useSharedValue(0);
const handleScroll = (event: any) => {
scrollOffset.value = event.nativeEvent.contentOffset.x;
};

const {state: firstTouch, isActive: isFirstPressActive} = useChartPressState(
initChartPressStateFirst,
);
const {state: secondTouch, isActive: isSecondPressActive} = useChartPressState(
initChartPressStateSecond,
);
const [currentRange, setCurrentRange] = useState({
start: null as number | null,
end: null as number | null,
});
const [pendingRange, setPendingRange] = useState({
start: null as number | null,
end: null as number | null,
});
const [shouldFilter, setShouldFilter] = useState(false);

let zoomLevel = timeRange;
if (timeRange > 2) {
if (currentRange.end && currentRange.start && zoomLevel > 2) {
zoomLevel = (currentRange.end - currentRange.start) / (60 * 60 * 1000);
}
}

const pendingRangeDerived = useDerivedValue(() => {
return {
start: Math.min(firstTouch.x.value.value, secondTouch.x.value.value),
end: Math.max(firstTouch.x.value.value, secondTouch.x.value.value),
};
});

useEffect(() => {
if (isFirstPressActive && isSecondPressActive) {
setPendingRange({
start: Math.min(pendingRangeDerived.value.start, pendingRangeDerived.value.end),
end: Math.max(pendingRangeDerived.value.start, pendingRangeDerived.value.end),
});
}
}, [isFirstPressActive, isSecondPressActive, shouldFilter]);

useEffect(() => {
if (!isFirstPressActive && !isSecondPressActive && zoomLevel > 2) {
if (pendingRange.start !== null && pendingRange.end !== null) {
setCurrentRange(pendingRange);
setShouldFilter(true);
}
}
}, [isFirstPressActive, isSecondPressActive, shouldFilter]);

useEffect(() => {
if (isFirstPressActive) Haptics.selectionAsync().catch(() => null);
}, [isFirstPressActive]);
useEffect(() => {
if (isSecondPressActive) Haptics.selectionAsync().catch(() => null);
}, [isSecondPressActive]);

const indicatorColor = {
y1: fetchData.length === 3 ? 'rgba(255, 235, 59, 1)' : 'rgba(33, 150, 243, 1)',
y2: 'rgba(100, 221, 23, 1)',
y3: 'rgba(244, 67, 54, 1)',
};

const fetchDataMap = useMemo(() => {
const result: DataPoint[] = [];
const maxLength = Math.max(...fetchData.map(arr => arr.length));

for (let i = 0; i < maxLength; i++) {
  const dataPoint: DataPoint = {} as DataPoint;

  fetchData.forEach((telemetryArray, index) => {
    if (telemetryArray[i]) {
      dataPoint['x'] = telemetryArray[i].ts;
      dataPoint[`y${index + 1}`] = Number(telemetryArray[i].value);
    }
  });
  if (Object.keys(dataPoint).length > 0) {
    if (
      !shouldFilter ||
      (currentRange.start !== null &&
        currentRange.end !== null &&
        dataPoint.x >= currentRange.start &&
        dataPoint.x <= currentRange.end)
    ) {
      result.push(dataPoint);
    }
  }
}

return result;

}, [fetchData, shouldFilter, currentRange]);

const generateTickValues = useCallback(() => {
console.log('zoomLevel', zoomLevel);
return generateTickData(fetchDataMap, zoomLevel);
}, [fetchDataMap, timeRange, shouldFilter]);

const formatYLabels = (value: number | undefined) => {
return ${value} ${headData.measurement};
};

const handleResetFilter = () => {
setShouldFilter(false);
setCurrentRange({start: null, end: null});
setPendingRange({start: null, end: null});
};

if (!fetchData.length) {
return ;
}
return (
<>


<View
style={{
flex: 1,
height: 500,
width: 2500,
backgroundColor: 'white',
}}>
<CartesianChart
data={fetchDataMap}
padding={{left: 10, right: 10, top: 10, bottom: 100}}
xKey='x'
yKeys={['y1', 'y2', 'y3']}
yAxis={[
{
enableRescaling: false,
font: font,
tickCount: 4,
formatYLabel: formatYLabels,
},
]}
domain={{
x: [
Math.min(...fetchDataMap.map(d => d.x)),
Math.max(...fetchDataMap.map(d => d.x)),
],
}}
chartPressState={[firstTouch, secondTouch]}
xAxis={{
font: font,
enableRescaling: false,
labelRotate: 25,
labelPosition: 'inset',
labelOffset: -50,
tickCount: fetchDataMap.length,
tickValues: generateTickValues(),
formatXLabel: formatXLabels,
}}
renderOutside={({chartBounds}) => (
<>
{isFirstPressActive && (
<>

</>
)}
{isSecondPressActive && (
<>

</>
)}
</>
)}>
{({points, chartBounds}) => {
return (
<>
<Line
points={points.y1}
strokeWidth={1}
curveType='linear'
color={${fetchData.length === 3 ? '#ffeb3b' : '#2196f3'}}
/>

                <Line
                  points={points.y2}
                  strokeWidth={1}
                  curveType='linear'
                  color='#64dd17'
                />
                <Line
                  points={points.y3}
                  strokeWidth={1}
                  curveType='linear'
                  color='#f44336'
                />
                <StockArea
                  isWindowActive={isFirstPressActive && isSecondPressActive}
                  startX={firstTouch.x.position}
                  endX={secondTouch.x.position}
                  {...chartBounds}
                />
              </>
            );
          }}
        </CartesianChart>
      </View>
    </SafeAreaView>
  </ScrollView>
  {shouldFilter && (
    <TouchableOpacity
      className={`bg-white h-8 items-centerjustify-center border-[1px] border-[#DADADA] rounded-md absolute bottom-2 left-4`}
      onPress={handleResetFilter}>
      <MaterialIcons name='filter-alt-off' size={24} color='black' />
    </TouchableOpacity>
  )}
</>

);
};

export default LineChartRendering;
`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants