Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ import {Bar} from 'victory-native';
type VictoryChartBarProps = {tnode: TNode};

function VictoryChartBar({tnode}: VictoryChartBarProps) {
const {points, chartBounds} = useVictoryChartRenderArgs();
const {points, chartBounds, pixelScale} = useVictoryChartRenderArgs();
const yKey = getYKey(tnode);
const {nodeStyles} = parseStyles(tnode);
const barWidth = parseAttributeAsNumber(tnode.attributes.barwidth);
return (
<Bar
points={points[yKey]}
chartBounds={chartBounds}
color={nodeStyles.fill ?? VictoryTheme.colors.default}
innerPadding={BAR_INNER_PADDING}
roundedCorners={parseCornerRadius(tnode.attributes.cornerradius)}
barWidth={parseAttributeAsNumber(tnode.attributes.barwidth)}
roundedCorners={parseCornerRadius(tnode.attributes.cornerradius, pixelScale)}
barWidth={barWidth === undefined ? undefined : barWidth * pixelScale}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@ type VictoryChartBarGroupProps = {
};

function VictoryChartBarGroup({tnode, isHorizontal}: VictoryChartBarGroupProps) {
const {points, chartBounds} = useVictoryChartRenderArgs();
const {points, chartBounds, pixelScale} = useVictoryChartRenderArgs();
const barChildren = tnode.children.filter((child) => child.tagName === 'victorybar');
const firstBarChild = barChildren.at(0);

if (!firstBarChild) {
return null;
}

const roundedCorners = parseCornerRadius(firstBarChild?.attributes?.cornerradius ?? '');
const barWidth = parseAttributeAsNumber(firstBarChild.attributes.barwidth);
const roundedCorners = parseCornerRadius(firstBarChild?.attributes?.cornerradius ?? '', pixelScale);
const rawBarWidth = parseAttributeAsNumber(firstBarChild.attributes.barwidth);
const barWidth = rawBarWidth === undefined ? undefined : rawBarWidth * pixelScale;
const betweenGroupPadding = barWidth
? parseOffset(tnode.attributes.offset, chartBounds, barChildren.length, barWidth, points[getYKey(firstBarChild)].length, isHorizontal ?? false)
? parseOffset(tnode.attributes.offset, chartBounds, barChildren.length, barWidth, points[getYKey(firstBarChild)].length, isHorizontal ?? false, pixelScale)
: undefined;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type VictoryChartCartesianProps = {
* Labels and legend overlays are handled internally via `renderOutside`.
*/
function VictoryChartCartesian({explicitSize, headless, onRenderArgs}: VictoryChartCartesianProps) {
const {tnode, data, xKey, yKeys, xAxis, yAxis, domain, domainPadding, padding, isHorizontal, labelItems, legendItems, chartContentStyles} = useVictoryChartContext();
const {tnode, data, xKey, yKeys, xAxis, yAxis, domain, domainPadding, padding, isHorizontal, labelItems, legendItems, chartContentStyles, pixelScale} = useVictoryChartContext();
const theme = useTheme();
const timezone = useCurrentTimezone();
const designWidth = getChartDesignWidth(explicitSize, chartContentStyles.width);
Expand Down Expand Up @@ -68,7 +68,7 @@ function VictoryChartCartesian({explicitSize, headless, onRenderArgs}: VictoryCh
{...getChartLayoutModeProps(explicitSize, headless)}
renderOutside={(renderArgs) => {
const overlayContent = (
<VictoryChartRenderArgsProvider value={renderArgs}>
<VictoryChartRenderArgsProvider value={{...renderArgs, pixelScale}}>
{labelItems.map((labelItem) => (
<VictoryChartLabel
key={`label-${labelItem.x}-${labelItem.y}-${timezone}`}
Expand Down Expand Up @@ -102,7 +102,7 @@ function VictoryChartCartesian({explicitSize, headless, onRenderArgs}: VictoryCh
onRenderArgs?.(renderArgs);

return (
<VictoryChartRenderArgsProvider value={renderArgs}>
<VictoryChartRenderArgsProvider value={{...renderArgs, pixelScale}}>
{tnode.children.map((child) => (
<VictoryChartSeries
key={`${child.tagName ?? 'node'}-${getHierarchyID(child)}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import {CHART_TYPE, POLAR_CONTAINER_HEIGHT_RATIO} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants';
import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext';
import {useVictoryChartContext, VictoryChartScaledProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext';
import {resolveChartContainerBgColor} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/resolveChartThemeColor';
import Modal from '@components/Modal';
import MultiGestureCanvas from '@components/MultiGestureCanvas';

import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand All @@ -16,6 +17,7 @@ import type {LayoutChangeEvent} from 'react-native';

import React, {useState} from 'react';
import {View} from 'react-native';
import {useSharedValue} from 'react-native-reanimated';

import VictoryChartContent from './VictoryChartContent';

Expand All @@ -28,15 +30,16 @@ type VictoryChartExpandModalProps = {
};

/**
* Centered full-screen modal that re-renders the current chart scaled up to the viewport.
* Centered full-screen modal that presents the current chart scaled up to the viewport, with the
* same pinch/double-tap zoom and pan gestures as the image attachment viewer.
* Must be rendered inside a VictoryChartProvider so VictoryChartContent can read the parsed chart context.
*
* The chart is rendered at its design size and uniformly transform-scaled to fit the modal —
* the same technique the inline scaled container uses to shrink charts. This keeps the canvas
* and the absolutely-positioned label/legend overlays (whose coordinates are design-based)
* perfectly aligned, so the expanded chart looks identical to the inline one, only larger.
* Rendering fluidly instead would resize only the canvas and leave labels at design coordinates,
* misplacing them (and potentially overlaying the header, blocking the back button).
* This mirrors the Lightbox pattern exactly: the chart is rendered ONCE at a fixed high resolution
* (like a high-res image asset — via VictoryChartScaledProvider, which scales every pixel-space
* value uniformly) and handed to MultiGestureCanvas at that intrinsic size. The canvas computes the
* fit scale itself and owns the single transform for fitting, centering, and zooming — no manual
* transforms of our own, since nested transforms rasterize the inner layer and blur it on native.
* Zooming in reveals the native resolution, so the chart stays sharp up to the headroom factor.
*/
function VictoryChartExpandModal({isVisible, onClose}: VictoryChartExpandModalProps) {
const styles = useThemeStyles();
Expand All @@ -46,8 +49,12 @@ function VictoryChartExpandModal({isVisible, onClose}: VictoryChartExpandModalPr
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {chartContentStyles, chartContainerStyles, type} = useVictoryChartContext();
const [availableSize, setAvailableSize] = useState({width: 0, height: 0});
// No pager wraps this canvas, so scrolling never needs to be handed back to one.
const isPagerScrollEnabled = useSharedValue(false);

const onContainerLayout = (event: LayoutChangeEvent) => {
// Ignore layout changes while the modal is closing — re-measuring mid-animation
// would rescale the chart and cause a visible flicker.
if (!isVisible) {
return;
}
Expand All @@ -69,6 +76,22 @@ function VictoryChartExpandModal({isVisible, onClose}: VictoryChartExpandModalPr
// Uniform scale that fits the chart's (clipped) design box inside the available modal area (may be > 1).
const scale = hasDesignDimensions && effectiveDesignHeight !== undefined && isMeasured ? Math.min(availableSize.width / designWidth, availableSize.height / effectiveDesignHeight) : 1;

// The fitted (displayed) size of the chart inside the modal.
const targetWidth = (designWidth ?? 0) * scale;
const targetHeight = (designHeight ?? 0) * scale;
const clippedTargetHeight = (effectiveDesignHeight ?? 0) * scale;

// The chart's intrinsic render size: drawn larger than the fitted size (like a 2x image asset)
// so that pinch-zooming reveals native resolution instead of magnified raster pixels.
// Capped so the canvas never exceeds a safe texture size.
const MAX_CANVAS_DIMENSION = 2048;
// 2x headroom covers typical pinch-zoom depth without paying for a larger render surface.
const MAX_ZOOM_HEADROOM = 2;
const zoomHeadroom = Math.max(1, Math.min(MAX_ZOOM_HEADROOM, MAX_CANVAS_DIMENSION / Math.max(targetWidth, targetHeight, 1)));
const renderWidth = targetWidth * zoomHeadroom;
const renderHeight = targetHeight * zoomHeadroom;
const clippedRenderHeight = clippedTargetHeight * zoomHeadroom;

// Visual styles parsed from the chart HTML — resolved and applied the same way
// VictoryChartContainerFixed does inline, so the expanded chart keeps the same
// (theme-aware) background and rounding.
Expand All @@ -94,7 +117,7 @@ function VictoryChartExpandModal({isVisible, onClose}: VictoryChartExpandModalPr
onBackButtonPress={onClose}
onCloseButtonPress={onClose}
/>
{/* Padding lives on the outer view; the inner view is measured so the scale never
{/* Padding lives on the outer view; the inner view is measured so the fit scale never
exceeds the actual content area and the side gutters are preserved. */}
<View style={[styles.flex1, styles.ph5]}>
<View
Expand All @@ -103,32 +126,45 @@ function VictoryChartExpandModal({isVisible, onClose}: VictoryChartExpandModalPr
>
{isMeasured &&
(hasDesignDimensions && effectiveDesignHeight !== undefined ? (
// Clip the container (not the content) so polar dead space is hidden while the chart renders at full fidelity.
<View
style={[
StyleUtils.getWidthAndHeightStyle(designWidth * scale, effectiveDesignHeight * scale),
typeof borderRadius === 'number' && isPolar && StyleUtils.getBorderRadiusStyle(borderRadius),
styles.overflowHidden,
]}
// Pinch/double-tap zoom and pan, matching the image attachment viewer. The canvas
// receives the chart at its intrinsic (high-res) size and fits it itself.
<MultiGestureCanvas
isActive={isVisible}
canvasSize={availableSize}
contentSize={{width: renderWidth, height: clippedRenderHeight}}
isUsedInCarousel={false}
isPagerScrollEnabled={isPagerScrollEnabled}
>
{/* Fixed design-size box so the fluid chart renders at design size, then scaled uniformly. */}
{/* Clip the container (not the content) so polar dead space is hidden while the chart renders at full fidelity. */}
<View
style={[
chartContentStyles,
StyleUtils.getWidthAndHeightStyle(designWidth, designHeight),
backgroundColor !== undefined && StyleUtils.getBackgroundColorStyle(backgroundColor),
typeof borderRadius === 'number' && StyleUtils.getBorderRadiusStyle(borderRadius),
StyleUtils.getWidthAndHeightStyle(renderWidth, clippedRenderHeight),
typeof borderRadius === 'number' && isPolar && StyleUtils.getBorderRadiusStyle(borderRadius),
styles.overflowHidden,
styles.chartExpandedContent,
StyleUtils.getTransformScaleStyle(scale),
]}
>
{/* The Skia canvas is removed as soon as closing starts: WebGL canvases can
flash white when re-composited during the close animation (visible on dark
themes). The card box stays so the modal animates out looking intact. */}
{isVisible && <VictoryChartContent />}
<View
style={[
StyleUtils.getWidthAndHeightStyle(renderWidth, renderHeight),
backgroundColor !== undefined && StyleUtils.getBackgroundColorStyle(backgroundColor),
typeof borderRadius === 'number' && StyleUtils.getBorderRadiusStyle(borderRadius),
styles.overflowHidden,
]}
>
{/* The Skia canvas is removed as soon as closing starts: WebGL canvases can
flash white when re-composited during the close animation (visible on dark
themes). The card box stays so the modal animates out looking intact. */}
{isVisible && (
<VictoryChartScaledProvider scale={scale * zoomHeadroom}>
<VictoryChartContent
explicitSize={{width: renderWidth, height: renderHeight}}
headless={false}
/>
</VictoryChartScaledProvider>
)}
</View>
</View>
</View>
</MultiGestureCanvas>
) : (
// Charts without design dimensions have no design-based label coordinates, so fluid
// rendering is safe. Background/rounding are still applied so the expanded chart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import {Line} from 'victory-native';
type VictoryChartLineProps = {tnode: TNode};

function VictoryChartLine({tnode}: VictoryChartLineProps) {
const {points} = useVictoryChartRenderArgs();
const {points, pixelScale} = useVictoryChartRenderArgs();
const yKey = getYKey(tnode);
const {nodeStyles} = parseStyles(tnode);
return (
<Line
points={points[yKey]}
color={nodeStyles.stroke ?? VictoryTheme.colors.default}
strokeWidth={nodeStyles.strokeWidth !== undefined ? Number(nodeStyles.strokeWidth) : undefined}
strokeWidth={nodeStyles.strokeWidth !== undefined ? Number(nodeStyles.strokeWidth) * pixelScale : undefined}
curveType={parseCurveType(tnode.attributes.interpolation)}
/>
);
Expand Down
Loading
Loading