After first load of Highcharts, how to properly swap different series in/out using React and properly render Highchart? - reactjs

Currently, I am using useState to swap out the series with a different kind of data set. The goal is to have the user click a button to show/swap the kind of data they want to see. I have attached a codesandbox for demo purposes (and it also shows one of the bugs). When I do it this way (as shown in this sandbox), 2 bugs occur.
Firstly, after swapping the data set, the legend shows series names properly (e.g. per the sandbox, will swap to a single series name as there is technically only 1 series to show along with its corresponding arearange). But after swapping back to the original, the legend only shows one name (of the two available series). However, the line series on the graph itself still shows the 2 lines properly despite the legend only have 1 of the line's names. This behavior shows with more than two series as well.
Secondly, I can't seem to reproduce it in the code sandbox. But essentially with a similar behavior as the first bug I explained earlier, the line series/arearange series will sometimes change. Meaning that when I flip between the data, the line series sometimes becomes an arearange (not intended) and vice versa.
The actual goal of my code is that the chart will get the data as a prop and then I would manipulate this data and set it to state (e.g. filtering an array for specific series sets). I'm not sure why this is not working as intended and any help is much appreciated :).
Here's an example code (same as the sandbox):
import Highcharts, { Options } from "highcharts";
import HighchartsReact from "highcharts-react-official";
import HighchartsExport from "highcharts/modules/exporting";
import HighchartsMore from "highcharts/highcharts-more";
import * as React from "react";
import { LineData, LineDataWithRange } from "./mocks/LineChartMockData";
if (typeof Highcharts === "object") {
HighchartsExport(Highcharts);
HighchartsMore(Highcharts);
}
export const Example: React.FC = () => {
const [chartData, setChartData] = React.useState(LineData);
const handleChangeToDataRange = () => {
setChartData(LineDataWithRange);
};
const handleChangeToJustData = () => {
setChartData(LineData);
};
const chartOptions: Options = {
chart: {
type: "line"
},
title: {
text: "Highcharts/React/Typescript"
},
series: chartData
};
return (
<>
<HighchartsReact highcharts={Highcharts} options={chartOptions} />
<button onClick={handleChangeToDataRange}>
Change to Line Data with Range
</button>
<button onClick={handleChangeToJustData}>Change to Just Line Data</button>
</>
);
};

That's because the wrapper uses chart.update to react to a component update. Series are updated and their options are merged. Example in pure JS: http://jsfiddle.net/gk06rxqw/
As a solution enable immutable property:
<HighchartsReact immutable={true} highcharts={Highcharts} options={chartOptions} />
Or overwrite all of the used series options:
export const LineData = [
{...},
{
...,
linkedTo: null
}
];
export const LineDataWithRange = [
{...},
{
...,
linkedTo: "series3"
}
];
Live example: https://codesandbox.io/s/sample-highchart-forke-o2byno
API Reference: https://api.highcharts.com/class-reference/Highcharts.Chart#update
Docs: https://www.npmjs.com/package/highcharts-react-official#options-details

Related

React testing library unable to detect datagrid row cells

Current behavior/issue:
Using react testing libraries queries, I am not able to detect row cells or the data inside them, even after implementing the correct queries for data not available immediately.
Expected behavior:
Using forBy queries should result in a passing testing and show those rendered rows in screen.debug.
Code/Steps to reproduce:
import { render, screen } from '#testing-library/react';
import PerformanceDataGridModal from '../../features/PerformanceDataGridModal/PerformanceDataGridModal';
import ChartMockData from '../../utils/Mocks/ChartsMockData/ChartMockData';
import '#testing-library/jest-dom';
describe('Performance datagrid modal', () => {
test.each(ChartMockData)('opens with correct information based on %s button click', async (item) => {
const CloseDataGrid = jest.fn();
const ClosedDataGridModal = (
<PerformanceDataGridModal
open={false}
onclose={CloseDataGrid}
rows={ChartMockData[item.id].rows}
columns={ChartMockData[item.id].columns}
/>
);
const OpenedDataGridModal = (
<PerformanceDataGridModal
open
onclose={CloseDataGrid}
rows={ChartMockData[item.id].rows}
columns={ChartMockData[item.id].columns}
/>
);
render(ClosedDataGridModal);
expect(screen.queryByRole('dialog')).toBeFalsy();
expect(screen.queryByRole('grid')).toBeFalsy();
expect(screen.queryByRole('cell', { name: 'Yes' })).toBeFalsy();
render(OpenedDataGridModal);
expect(screen.getByRole('dialog')).toBeTruthy();
expect(screen.getByRole('grid')).toBeTruthy();
expect(await screen.findByRole('cell', { name: 'jane' })).toBeTruthy();
screen.debug();
});
});
As you see on this line
expect(await screen.findByRole('cell', { name: 'jane' })).toBeTruthy();
I have followed the instructions of react testing library as indicated here:
https://testing-library.com/docs/guide-disappearance
What I've tried?
Await WaitFor, with getBy queries instead of forBy queries
using disableVirtualization as indicated in material mui datagrid section and the following source: https://github.com/mui/mui-x/issues/1151
using jest.requierActual datagrid and setting autoHeight
awaiting the render of component
await the entire assertion(expect....

How to animate react-native-svg Polygon element?

Is there a simple way to animate the Polygon element from the react-native-svg library?
I need to animate his shape by animating the points.
I found few examples on how to animate Path element or Circle, but couldn't find anything regarding the Polygon. Thanks in advance.
Bit late to the party, but I've found a solution if you're still interested. It's not exactly 'simple', but it works. There's a library called React Native Reanimated, and it extends the functionality of Animated components
substantially. Here's what I was able to achieve:
The reason animating Polygons isn't available out of the box is because the standard Animated API only handles simple values, namely individual numbers. The Polygon component in react-native-svg takes props of points, which is an array of each of the points, themselves array of x and y. For example:
<Polygon
strokeWidth={1}
stroke={strokeColour}
fill={fillColour}
fillOpacity={1}
points={[[firstPointX, firstPointY],[secondPointX, secondPointY]}
/>
React Native Reanimated allows you to animate even complex data types. In this case, there is useSharedValue, which functions almost identical to new Animated.value(), and a function called useAnimatedProps, where you can create your points (or whatever else you want to animate) and pass them to the component.
// import from the library
import Animated, {
useSharedValue,
useAnimatedProps,
} from 'react-native-reanimated';
// creates the animated component
const AnimatedPolygon = Animated.createAnimatedComponent(Polygon);
const animatedPointsValues = [
{x: useSharedValue(firstXValue), y: useSharedValue(firstYValue)},
{x: useSharedValue(secondXValue), y: useSharedValue(secondYValue)},
];
const animatedProps = useAnimatedProps(() => ({
points: data.map((_, i) => {
return [
animatedPointValues[i].x.value,
animatedPointValues[i].y.value,
];
}),
})),
Then in your render/return:
<AnimatedPolygon
strokeWidth={1}
stroke={strokeColour}
fill={fillColour}
fillOpacity={1}
animatedProps={animatedProps}
/>
Then whenever you update one of those shared values, the component will animate.
I'd recommend reading their docs and becoming familiar with the library, as it will open up a whole world of possibilities:
https://docs.swmansion.com/react-native-reanimated/
Also, the animations are handled in the native UI thread, and easily hit 60fps, yet you can write them in JS.
Good luck!
react-native-reanimated also supports flat arrays for the Polygon points prop, so we can simplify the animation setup even more.
Full example which will animate the react-native-svg's Polygon when the points prop changes looks like this:
import React, { useEffect } from 'react'
import Animated, { useAnimatedProps, useSharedValue, withTiming } from 'react-native-reanimated'
import { Polygon } from 'react-native-svg'
interface Props {
points: number[]
}
const AnimatedPolygonInternal = Animated.createAnimatedComponent(Polygon)
export const AnimatedPolygon: React.FC<Props> = ({ points }: Props) => {
const sharedPoints = useSharedValue(points)
useEffect(() => {
sharedPoints.value = withTiming(points)
}, [points, sharedPoints])
const animatedProps = useAnimatedProps(() => ({
points: sharedPoints.value,
}))
return <AnimatedPolygonInternal fill="lime" animatedProps={animatedProps} />
}

Recursive component rendering based on a tree menu object

I have created a component which adds an additional selection box dropdown whenever a key inside an object is another object.
For example, consider the following object:
{
a1: {
x1: 1,
x2: 2,
x3: 3,
x4: {
z1: "z1",
z2: "z2"
},
x5: [
{
x5a: {
z5a1: 1,
z5a2: 2
}
},
{
x5b: {
z5b1: 1,
z5b2: 2
}
}
]
},
a2: {
x1: 1,
x2: 2,
x3: 3
},
a3: "some values"
};
What I want to achieve is (when I select a value from the dropdown menu):
if subTree[value] is an object ({}) or an array ([]), display its keys or indices in
a new selection box drop down, directly bellow the current
else stop
Initial display
Selecting a value in the dropdown
After I select a value, the next selection will show empty, and so on and so forth...
The problem
When I update a value in a selection box, my code doesn't update/clear the selections bellow it properly.
The source code of my project is available at: https://codesandbox.io/s/frosty-grass-9jdue
When changing a value that is not the last in the path, you need to clear all subsequent selections because they were based on a different path. I'm not quite sure how we do that in your setup because I haven't quite wrapped my head around it.
What makes sense to me is to store the pieces of the path as an array. That way we can use slice to remove the tail. I am going to use lodash's get method as a helper to access the value at a path. I am expecting the prop data to be the object itself rather than Object.entries like you were doing before.
import React, { useState, useEffect } from "react";
import { MenuItem, TextField } from "#material-ui/core";
import _get from "lodash/get";
const InfiniteSelection = ({ data, onCaseCadeSelection }) => {
// an array of segments like ['a1', 'x4', 'z1']
const [path, setPath] = useState([]);
// joins to a string like `a1.x4.z1`
const cascade = path.join(".");
// call callback whenever the cascade changes
useEffect(() => {
if (onCaseCadeSelection) {
onCaseCadeSelection(cascade);
}
}, [cascade]);
// need to know the index in the paths array where the change occurred
const handleChange = (index) => (event) => {
// set this value and delete everything after it
setPath([...path.slice(0, index), event.target.value]);
};
// options for the NEXT value from a given path
const optionsForPath = (path) => {
// lodash get handles this except when path is empty array []
const value = path.length > 0 ? _get(data, path) : data;
// get the options from this path, or null if it is terminal
return typeof value === "object" ? Object.keys(value) : null;
};
// either the current path is to a terminal value, or there should be one more level of selects
const currentOptions = optionsForPath(path);
// helper function can be used as a callback to path.map
// will also be called one extra time for the next values if not on a terminal value
const renderSelect = (value, index) => {
return (
<SelectControlled
className="text form_text"
variant="outlined"
list={optionsForPath(path.slice(0, index)) ?? []}
onChange={handleChange(index)}
value={value ?? ""}
/>
);
};
// render selects for each element in the path and maybe a next select
return (
<div className="vertically_spaced">
{path.map(renderSelect)}
{currentOptions === null || renderSelect("", path.length)}
</div>
);
};
Code Sandbox Link
From #LindaPaiste's answer:
When changing a value that is not the last in the path, you need to clear all subsequent selections because they were based on a different path.
That's the key to solving your problem! You have to somehow blow away and forget everything bellow the selection box whose value you are currently changing.
React was designed around the "blow away and forget" principle. Note also that The Data Flows Down. With that in mind, your task should be fairly easy to complete and while Linda's solution seems to work, it is perhaps not as simple as it could be.
What if we could have a special component that (1) accepts a sub-tree of your data, (2) renders its 1st level children as a selection box dropdown and then (3) repeats the process recursively? Something like this:
<RecursiveComponent subTree={DATA_SAMPLE} {/*maybe some other props*/}/>
When we think of recursion, we have to think of terminal conditions. In our case, this happens when the sub-tree is a primitive type (i.e. not an object ({}) or an array ([])).
Every RecursiveComponent has to:
render the selection menu dropdown, containing all the 1st level children of the sub-tree
render the nested RecursiveComponent, based on props.subTree[selection]
handle user interaction
Something like this:
import { MenuItem, Select } from "#material-ui/core";
import { useState } from "react";
function RecursiveComponent(props) {
const [selection, setSelection] = useState(props.currentSelection);
const handleChange = (event) => {
setSelection(event.target.value);
};
return (
<>
<Select variant="outlined" value={selection} onChange={handleChange}>
{Object.keys(props.subTree).map((key) => (
<MenuItem value={key}>{key}</MenuItem>
))}
</Select>
<div /> {/* forces a line break between selection boxes */}
{props.subTree[selection] !== Object(props.subTree[selection]) ? (
<></>
) : (
<RecursiveComponent
subTree={props.subTree[selection]}
currentSelection=""
/>
)}
</>
);
}
export default RecursiveComponent;
This is how you can use RecursiveComponent in your project by editing index.js:
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { DATA_SAMPLE } from "./DataSample";
import RecursiveComponent from "./RecursiveComponent";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<RecursiveComponent subTree={DATA_SAMPLE} currentSelection="" />
</StrictMode>,
rootElement
);

Moving slider with Cypress

I've got a Slider component from rc-slider and I need Cypress to set the value of it.
<Slider
min={5000}
max={40000}
step={500}
value={this.state.input.amount}
defaultValue={this.state.input.amount}
className="sliderBorrow"
onChange={(value) => this.updateInput("amount",value)}
data-cy={"input-slider"}
/>
This is my Cypress code:
it.only("Changing slider", () => {
cy.visit("/");
cy.get(".sliderBorrow")
.invoke("val", 23000)
.trigger("change")
.click({ force: true })
});
What I've tried so far does not work.
Starting point of slider is 20000, and after test runs it goes to 22000, no matter what value I pass, any number range.
Looks like it used to work before, How do interact correctly with a range input (slider) in Cypress? but not anymore.
The answer is very and very simple. I found the solution coincidentally pressing enter key for my another test(date picker) and realized that pressing left or right arrow keys works for slider.
You can achieve the same result using props as well. The only thing you need to do is to add this dependency: cypress-react-selector and following instructions here: cypress-react-selector
Example of using {rightarrow}
it("using arrow keys", () => {
cy.visit("localhost:3000");
const currentValue = 20000;
const targetValue = 35000;
const increment = 500;
const steps = (targetValue - currentValue) / increment;
const arrows = '{rightarrow}'.repeat(steps);
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 20000)
.type(arrows)
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 35000)
})
#darkseid's answer helped guide me reach an optimal solution.
There are two steps
Click the slider's circle, to move the current focus on the slider.
Press the keyboard arrow buttons to reach your desired value.
My slider jumps between values on the sliders, therefore this method would work. (I am using Ion range slider)
This method doesn't require any additional depedency.
// Move the focus to slider, by clicking on the slider's circle element
cy.get(".irs-handle.single").click({ multiple: true, force: true });
// Press right arrow two times
cy.get(".irs-handle.single").type(
"{rightarrow}{rightarrow}"
);
You might be able to tackle this using Application actions, provided you are able to modify the app source code slightly.
Application actions give the test a hook into the app that can be used to modify the internal state of the app.
I tested it with a Function component exposing setValue from the useState() hook.
You have used a Class component, so I guess you would expose this.updateInput() instead, something like
if (window.Cypress) {
window.app = { updateInput: this.updateInput };
}
App: index.js
import React, { useState } from 'react';
import { render } from 'react-dom';
import './style.css';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
function App() {
const [value, setValue] = useState(20000);
// Expose the setValue() method so that Cypress can set the app state
if (window.Cypress) {
window.app = { setValue };
}
return (
<div className="App">
<Slider
min={5000}
max={40000}
step={500}
value={value}
defaultValue={value}
className="sliderBorrow"
onChange={val => setValue(val)}
data-cy={"input-slider"}
/>
<div style={{ marginTop: 40 }}><b>Selected Value: </b>{value}</div>
</div>
);
}
render(<App />, document.getElementById('root'));
Test: slider.spec.js
The easiest way I found assert the value in the test is to use the aria-valuenow attribute of the slider handle, but you may have another way of testing that the value has visibly changed on the page.
describe('Slider', () => {
it("Changing slider", () => {
cy.visit("localhost:3000");
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 20000)
cy.window().then(win => {
win.app.setValue(35000);
})
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 35000)
})
})
For whoever comes across this with Material UI/MUI 5+ Sliders:
First off, this github issue and comment might be useful: https://github.com/cypress-io/cypress/issues/1570#issuecomment-606445818.
I tried changing the value by accessing the input with type range that is used underneath in the slider, but for me that did not do the trick.
My solution with MUI 5+ Slider:
<Slider
disabled={false}
step={5}
marks
data-cy="control-percentage"
name="control-percentage"
defaultValue={0}
onChange={(event, newValue) =>
//Handle change
}
/>
What is important here is the enabled marks property. This allowed me to just click straight on the marks in the cypress test, which of course can also be abstracted to a support function.
cy.get('[data-cy=control-percentage]').within(() => {
// index 11 represents 55 in this case, depending on your step setting.
cy.get('span[data-index=11]').click();
});
I got this to work with the popular react-easy-swipe:
cy.get('[data-cy=week-picker-swipe-container]')
.trigger('touchstart', {
touches: [{ pageY: 0, pageX: 0 }]
})
.trigger('touchmove', {
touches: [{ pageY: 0, pageX: -30 }]
})

Odd behaviour from custom react hook

https://jsfiddle.net/dqx7kb91/1/
In this fiddle, I extracted a part of a project (I tried to simplify it as much as I could without breaking anything), used to manage search filters.
We have many different types of filters defined as an object like this:
filter = {
name: "",
initVal: 1, //default value for the filter,
controlChip: ... // filter's chip component
...
}
the chip component are used to list all filters activated and to edit already activated filters (in this case remove the filter from the list of activated filters).
The filters are contained in an object handled by an unique container containing a custom hook.
The problem is, let's say I set the 1st filter, then set the second and I decide to finally remove the first filter, both filters are removed. Everything is working fine apart from this.
What might be causing this ? It seems to me that in the custom hook useMap that I use, when I try to remove the filter, it doesn't take account of the actual current state but uses the state it was when I added the first filter (an empty object), so it tries to remove something from nothing and set the state to the result, an empty object.
How can I fix this ? Thank you
What's happening is when you set your first filter (let's say #1) you're setting map that contains just filter 1. When you set filter #2 map contains filters #1 & #2. BUT... and here's the thing... your remove callback for filter #1 has map only containing #1, not #2. That's because your callback was set before you set filter #2. This would normally be solved because you're using hooks like useCallback, but the way you're implementing them (createContainer(useFilters)), you're creating separate hooks for each filter object.
I would simplify this into only one component and once it is working start extracting pieces one by one if you really need to.
I know this is a complete rewrite from what you had, but this works:
import React from "react";
import ReactDOM from "react-dom";
const App = () => {
const [map, setMap] = React.useState({});
// const get = React.useCallback(key => map[key], [map])
const set = (key, entry) => {
setMap({ ...map, [key]: entry });
};
const remove = key => {
const {[key]: omit, ...rest} = map;
setMap(rest);
};
// const reset = () => setMap({});
const filters = [
{ name: 'filter1', value: 1 },
{ name: 'filter2', value: 2 },
];
return (
<>
{Object.keys(map).map(key => (
<button onClick={() => remove(key)}>
REMOVE {key}
</button>
))}
<hr />
{filters.map(({ name, value }) => (
<button onClick={() => set(name, { value })}>
SET {name}
</button>
))}
</>
)
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Resources