Skip to content
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

Handle Invalid scale and arcDegreeScale values in segments to avoid crash #114

Open
wants to merge 10 commits into
base: development
Choose a base branch
from

Conversation

roni-castro-shipt
Copy link

@roni-castro-shipt roni-castro-shipt commented Dec 25, 2024

Description

Currently, when an invalid number (NaN) is passed as arcDegreeScale, Android crashes, and iOS generates an inconsistent graph.

Solution:

  • If the arcDegreeScale or scale is a finite number, parse it as usual. However, if either value is invalid (i.e., NaN, Infinity, or -Infinity, or any unexpected numeric value), issue a warning to the user and override the value with a default scale. This default will be calculated based on the remaining value needed to sum to 1 (current solution already). This ensures that neither arcDegreeScale nor scale will ever be NaN, preventing crashes.

  • Issue a warning every time a SegmentArc is rendered for the first time, under the following conditions:

    1. Either arcDegreeScale or scale is not a finite number (i.e., NaN, Infinity, or -Infinity).
    2. A segment’s arcDegreeScale or scale value is outside the valid range of 0 (0%) to 1 (100%).
    3. The sum of all segment scale or arcDegreeScale values exceeds 1 or is less than 0.

Obs:

  • Previously, the scale and arcDegreeScale props were being mutated, which is generally discouraged by eslint and considered difficult to debug. With this change, the same behavior will be maintained without mutating the props. Note that this will require some snapshot updates. If this mutation is wanted for some reason, let me know to undo.

    • After the PR changes, this will happen in some snapshots, no mutation of the props, and the default values will be set internally in the lib
    Screenshot 2024-12-30 at 13 00 38
  • In the current solution:

    • A scale value of zero is considered invalid, so it is overridden by _ensureDefaultSegmentScale. This behavior has been preserved.
    • A arcDegreeScale value of zero is considered valid, so it is not overridden by _ensureDefaultArcScale. This logic remains unchanged.
    • scale or arcDegreeScale values set to undefined are currently accepted as valid props. The previous functions _ensureDefaultSegmentScale and _ensureDefaultArcScale handle these values by evenly distributing the value across segments, so no warning is generated for these cases, and the same logic is preserved.

Screenshots

Android

Before After
android_before.mp4
android_after.mp4

iOS

Before After
ios_before.mp4
ios_after.mp4

No warnings for valid props

Android iOS

Some possible warnings

arcDegreeScale

arcDegreeScale-10 arcDegreeScale+10 arcDegreeScale_NaN arcDegreeScale_null

scale

scale_more_than_1 scale_allocated_smaller_than_0 scale_NaN

Copy link
Collaborator

@jkhusanov jkhusanov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I'll pull up and run the example app locally, couple of comments.

import { SegmentedArcError } from './segmentedArcErrors';
import WarningManager from './warningManager';

const isInvalidScalePercent = value => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are local functions can we _underscore these?


export const createInvalidScaleValueError = (propertyName, value) => {
return new SegmentedArcError(
`Each '${propertyName}' value should be a positive number between 0 and 1 inclusive. It was found ${value}`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should paraphrase the ending something like this or better.

Suggested change
`Each '${propertyName}' value should be a positive number between 0 and 1 inclusive. It was found ${value}`
`Each '${propertyName}' value should be a positive number between 0 and 1 inclusive. ${value} was provided instead`


if (!warnings.has(key)) {
console.warn(data);
warnings.add(key);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, did we opt into having a warning set becase we wanted to avoid showing the same warnings more than once?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was to avoid depending on the useShowSegmentedArcWarnings useRef to just show the same warning once in case rerender happens, like when an array is passed directly as props without memoization, and avoid spamming multiple warnings per render. But I'll check if that is still needed

<SegmentedArc segments={[ {scale: NaN} ]} />

@@ -0,0 +1,29 @@
export const ensureDefaultSegmentScale = segments => {
const isInvalidScale = value => !Number.isFinite(value) || value === 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this function to outside of ensureDefaultSegmentScale function?

const allocatedScale = segments.reduce((sum, { scale }) => sum + (isInvalidScale(scale) ? 0 : scale), 0);
const defaultArcScale = invalidScaleCount > 0 ? (1 - allocatedScale) / invalidScaleCount : 0;

return segments.map(segment => (isInvalidScale(segment.scale) ? { ...segment, scale: defaultArcScale } : segment));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreaciate for not manipulating props directly!

};

export const ensureDefaultSegmentArcDegreeScale = segments => {
const isInvalidArcDegreeScale = value => !Number.isFinite(value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also move this to outside of this function and make it _local?

global.__DEV__ = currentGlobalDev;
});

it('show warnings and renders the component when segments have invalid scale or arcDegreeScale data', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding s

Suggested change
it('show warnings and renders the component when segments have invalid scale or arcDegreeScale data', () => {
it('shows warnings and renders the component when segments have invalid scale or arcDegreeScale data', () => {


const segmentsResult = ensureDefaultSegmentArcDegreeScale(segments);

expect(segmentsResult).toEqual([{ arcDegreeScale: 1 / 2 }, { arcDegreeScale: 1 / 2 }]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you just use 1/2 to avoid precision issues?

Copy link
Author

@roni-castro-shipt roni-castro-shipt Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. it's 1/2 because there're two segments in here const segments = [{ arcDegreeScale: NaN }, { arcDegreeScale: NaN }]; so the default value would be 1 / numberOfSegments. I'll change to 1 / segments.length to make it clear

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to say could we do 0.5 ?

Comment on lines +148 to +154
{
arcDegreeScale: 8 / 50
},
{
arcDegreeScale: 42 / 50
}
])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick it seems like having exact values like this is more readable

Suggested change
{
arcDegreeScale: 8 / 50
},
{
arcDegreeScale: 42 / 50
}
])
{
arcDegreeScale: 0.16
},
{
arcDegreeScale: 0.84
}
])

describe('ensureDefaultSegmentScaleValues', () => {
it('ensures default values for `scale` are set', () => {
expect(
ensureDefaultSegmentScaleValues([
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could have called this outside of expect function and used a variable instead.

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

Successfully merging this pull request may close these issues.

2 participants