negative values missing in logarithmic mode - reactjs

so after going all over other questions on SOF.
I'm using HighchartsReact wrapper for Highcharts.
Expected behaviour:
show negative values in stacked column chart even in logarithmic scale.
When toggling a series that has y values < 0, log scale hides them. When toggling back to linear scale the hidden points should be visible again.
Actual behaviour:
negative values disappear when going from linear to logarithmic. in addition when going back to linear the negative value that were seen in linear scale before - disappear and are not part of the chart.
It was working before several weeks and stopped working suddenly.
Live demo with steps to reproduce:
demo
official Custom Axis extension to allow emulation of
negative values on a logarithmic
Product version
9.3.2
How can I add this function with using HighchartsReact? I tried passing a callback and also inside React.useEffect to use the chart ref. but anything I do, still negative values are missing. and going back to linear just removes the original negative values from series.
(function (H) {
H.addEvent(H.Axis, 'afterInit', function () {
const logarithmic = this.logarithmic;
if (logarithmic && this.options.custom.allowNegativeLog) {
// Avoid errors on negative numbers on a log axis
this.positiveValuesOnly = false;
// Override the converter functions
logarithmic.log2lin = num => {
const isNegative = num < 0;
let adjustedNum = Math.abs(num);
if (adjustedNum < 10) {
adjustedNum += (10 - adjustedNum) / 10;
}
const result = Math.log(adjustedNum) / Math.LN10;
return isNegative ? -result : result;
};
logarithmic.lin2log = num => {
const isNegative = num < 0;
let result = Math.pow(10, Math.abs(num));
if (result < 10) {
result = (10 * (result - 1)) / (10 - 1);
}
return isNegative ? -result : result;
};
}
});
}(Highcharts));

You need to define the variable chart if you want to see how is changing.
const chart = Highcharts.chart('container', {
});
Live demo: https://jsfiddle.net/BlackLabel/x1zp347m/
Here is the official Highcharts React wrapper where you will find how to use Highcharts at the React.
https://github.com/highcharts/highcharts-react#basic-usage-example
EDIT --------------------------------------------------------------
To extending the chart custom code or edit the core you can fire function after imports.
How to extending Higcharts code:
https://www.highcharts.com/docs/extending-highcharts/extending-highcharts
import React from 'react'
import { render } from 'react-dom'
import Highcharts from 'highcharts'
import HighchartsReact from 'highcharts-react-official'
(function (H) {
H.addEvent(H.Chart, 'load', function (e) {
var chart = e.target;
H.addEvent(chart.container, 'click', function (e) {
e = chart.pointer.normalize(e);
console.log('Clicked chart at ' + e.chartX + ', ' + e.chartY);
});
H.addEvent(chart.xAxis[0], 'afterSetExtremes', function (e) {
console.log('Set extremes to ' + e.min + ', ' + e.max);
});
});
}(Highcharts));
const options = {
title: {
text: 'My chart'
},
plotOptions: {
series: {
enableMouseTracking: true
}
},
series: [{
data: [1, 2, 3]
}]
}
const App = () => <div>
<HighchartsReact
highcharts={Highcharts}
options={options}
/>
</div>
render(<App />, document.getElementById('root'))
https://stackblitz.com/edit/react-t6rh4c?file=components%2FChart.jsx

Related

HighCharts zoom with mouse wheel in react

Intro
Hi I'm new to react and new to Highcharts but have worked with js before. I have a zoom function, I'd like to convert this to react code. I'm having hard time figuring out how to use Highcharts.Chart.prototype.callbacks.push in the context of react.
What I'm trying to do
I'm trying to create a zoom function that is smooth with the mouse wheel on a stock chart. The popular platform trading view has a very smooth zoom and I'm trying to do something similar to that.
The code:
I have the code working decently in a js fiddle https://jsfiddle.net/drewscatterday/84shran6/ but I'm having hard time converting this to react.
I have tried doing this in my react code but had no luck:
const handleScroll= (e) =>{
e.preventDefault();
var chart = Highcharts.chart;
var xAxis = chart.xAxis[0],
extremes = xAxis.getExtremes(),
newMin = extremes.min,
output = [];
if (e.deltaY < 0) {
xAxis.setExtremes(extremes.min - (extremes.min * 0.001), extremes.max, true);
}
else {
xAxis.setExtremes(extremes.min + (extremes.min * 0.001), extremes.max, true);
}
}
<div className="Chartdisplay__chart" id="chart" onScroll={handleScroll}>
<StockChart options={stockOptions} highcharts={Highcharts} />
</div>
I have also just tried adding the function straight up like this but also had no luck:
let ar = [];
(function(H) {
Highcharts.Chart.prototype.callbacks.push(function(chart) {
H.addEvent(chart.container, 'mousewheel', function(e) {
var xAxis = chart.xAxis[0],
extremes = xAxis.getExtremes();
if (e.deltaY < 0) {
xAxis.setExtremes(extremes.min - (extremes.min * 0.001), extremes.max, true);
}
else {
xAxis.setExtremes(extremes.min + (extremes.min * 0.001), extremes.max, true);
}
});
});
}(Highcharts));
Thank you for anyone who takes the time to read this and help me :)
To extend Higstock in React.js put the code that you want to wrap before the configuration object (options).
import React from 'react'
import { render } from 'react-dom'
import Highcharts from 'highcharts/highstock'
import HighchartsReact from 'highcharts-react-official'
(function(H) {
Highcharts.Chart.prototype.callbacks.push(function(chart) {
H.addEvent(chart.container, 'wheel', function(e) {
var xAxis = chart.xAxis[0],
extremes = xAxis.getExtremes(),
newMin = extremes.min;
console.log(extremes);
console.log(newMin);
if (e.deltaY < 0) {
xAxis.setExtremes(extremes.min - (extremes.min * 0.001), extremes.max, true);
}
else {
xAxis.setExtremes(extremes.min + (extremes.min * 0.001), extremes.max, true);
}
});
});
}(Highcharts));
const options = {
title: {
text: 'My chart'
},
plotOptions: {
series: {
enableMouseTracking: true
}
}
https://www.highcharts.com/docs/extending-highcharts/extending-highcharts
Live demo: https://stackblitz.com/edit/react-hmvz2z?file=index.js

Test scrolling in a react-window list with react-testing-library

I am stuck here with a test where I want to verify that after scrolling through a list component, imported by react-window, different items are being rendered. The list is inside a table component that saves the scrolling position in React context, which is why I need to test the whole table component.
Unfortunately, the scrolling event seems to have no effect and the list still shows the same items.
The test looks something like this:
render(
<SomeProvider>
<Table />
</SomeProvider>
)
describe('Table', () => {
it('scrolls and renders different items', () => {
const table = screen.getByTestId('table')
expect(table.textContent?.includes('item_A')).toBeTruthy() // --> true
expect(table.textContent?.includes('item_Z')).toBeFalsy() // --> true
// getting the list which is a child component of table
const list = table.children[0]
fireEvent.scroll(list, {target: {scrollY: 100}})
expect(table.textContent?.includes('item_A')).toBeFalsy() // --> false
expect(table.textContent?.includes('item_Z')).toBeTruthy() // --> false
})
})
Any help would be much appreciated.
react-testing-library by default renders your components in a jsdom environment, not in a browser. Basically, it just generates the html markup, but doesn't know where components are positioned, what are their scroll offsets, etc.
See for example this issue.
Possible solutions are :
use Cypress
or override whatever native attribute react-window is using to measure scroll offset in your container (hacky). For example, let's say react-window is using container.scrollHeight :
// artificially setting container scroll height to 200
Object.defineProprerty(containerRef, 'scrollHeight', { value: 200 })
I had a scenario where certain presentation aspects depended on the scroll position. To make tests clearer, I defined the following mocks in test setup:
1. Mocks that ensure programmatic scrolls trigger the appropriate events:
const scrollMock = (leftOrOptions, top) => {
let left;
if (typeof (leftOrOptions) === 'function') {
// eslint-disable-next-line no-param-reassign
({ top, left } = leftOrOptions);
} else {
left = leftOrOptions;
}
Object.assign(document.body, {
scrollLeft: left,
scrollTop: top,
});
Object.assign(window, {
scrollX: left,
scrollY: top,
scrollLeft: left,
scrollTop: top,
}).dispatchEvent(new window.Event('scroll'));
};
const scrollByMock = function scrollByMock(left, top) { scrollMock(window.screenX + left, window.screenY + top); };
const resizeMock = (width, height) => {
Object.defineProperties(document.body, {
scrollHeight: { value: 1000, writable: false },
scrollWidth: { value: 1000, writable: false },
});
Object.assign(window, {
innerWidth: width,
innerHeight: height,
outerWidth: width,
outerHeight: height,
}).dispatchEvent(new window.Event('resize'));
};
const scrollIntoViewMock = function scrollIntoViewMock() {
const [left, top] = this.getBoundingClientRect();
window.scrollTo(left, top);
};
const getBoundingClientRectMock = function getBoundingClientRectMock() {
let offsetParent = this;
const result = new DOMRect(0, 0, this.offsetWidth, this.offsetHeight);
while (offsetParent) {
result.x += offsetParent.offsetX;
result.y += offsetParent.offsetY;
offsetParent = offsetParent.offsetParent;
}
return result;
};
function mockGlobal(key, value) {
mockedGlobals[key] = global[key]; // this is just to be able to reset the mocks after the tests
global[key] = value;
}
beforeAll(async () => {
mockGlobal('scroll', scrollMock);
mockGlobal('scrollTo', scrollMock);
mockGlobal('scrollBy', scrollByMock);
mockGlobal('resizeTo', resizeMock);
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { value: scrollIntoViewMock, writable: false });
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', { value: getBoundingClientRectMock, writable: false });
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { value: 250, writable: false });
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { value: 250, writable: false });
});
The above ensures that, after a programmatic scroll takes place, the appropriate ScrollEvent will be published, and the window properties are updated accordingly.
2. Mocks that setup a basic layout for a collection of siblings
export function getPosition(element) {
return element?.getClientRects()[0];
}
export function scrollToElement(element, [extraX = 0, extraY = 0]) {
const { x, y } = getPosition(element);
window.scrollTo(x + extraX, y + extraY);
}
export const layoutTypes = {
column: 'column',
row: 'row',
};
function* getLayoutBoxIterator(type, { defaultElementSize }) {
const [width, height] = defaultElementSize;
let offset = 0;
while (true) {
let left = 0;
let top = 0;
if (type === layoutTypes.column) {
top += offset;
offset += height;
} else if (type === layoutTypes.row) {
left += offset;
offset += width;
}
yield new DOMRect(left, top, width, height);
}
}
function getLayoutProps(element, layoutBox) {
return {
offsetX: layoutBox.x,
offsetY: layoutBox.y,
offsetWidth: layoutBox.width,
offsetHeight: layoutBox.height,
scrollWidth: layoutBox.width,
scrollHeight: layoutBox.height,
};
}
function defineReadonlyProperties(child, props) {
let readonlyProps = Object.entries(props).reduce((accumulator, [key, value]) => {
accumulator[key] = {
value,
writable: false,
}; return accumulator;
}, {});
Object.defineProperties(child, readonlyProps);
}
export function mockLayout(parent, type, options = { defaultElementSize: [250, 250] }) {
const layoutBoxIterator = getLayoutBoxIterator(type, options);
const parentLayoutBox = new DOMRect(parent.offsetX, parent.offsetY, parent.offsetWidth, parent.offsetHeight);
let maxBottom = 0;
let maxRight = 0;
Array.prototype.slice.call(parent.children).forEach((child) => {
let layoutBox = layoutBoxIterator.next().value;
// eslint-disable-next-line no-return-assign
defineReadonlyProperties(child, getLayoutProps(child, layoutBox));
maxBottom = Math.max(maxBottom, layoutBox.bottom);
maxRight = Math.max(maxRight, layoutBox.right);
});
parentLayoutBox.width = Math.max(parentLayoutBox.width, maxRight);
parentLayoutBox.height = Math.max(parentLayoutBox.height, maxBottom);
defineReadonlyProperties(parent, getLayoutProps(parent, parentLayoutBox));
}
With those two in place, I would write my tests like this:
// given
mockLayout(/* put the common, direct parent of the siblings here */, layoutTypes.column);
// when
Simulate.click(document.querySelector('#nextStepButton')); // trigger the event that causes programmatic scroll
const scrolledElementPosition = ...; // get offsetX of the component that was scrolled programmatically
// then
expect(window.scrollX).toEqual(scrolledElementPosition.x); // verify that the programmatically scrolled element is now at the top of the page, or some other desired outcome
The idea here is that you give all siblings at a given level sensible, uniform widths and heights, as if they were rendered as a column / row, thus imposing a simple layout structure that the table component will 'see' when calculating which children to show / hide.
Note that in your scenario, the common parent of the sibling elements might not be the root HTML element rendered by table, but some element nested inside. Check the generated HTML to see how to best obtain a handle.
Your use case is a little different, in that you're triggering the event yourself, rather than having it bound to a specific action (a button click, for instance). Therefore, you might not need the first part in its entirety.

Is it possible to get / return Highcharts Axis values?

I would like to be able to use the values printed on Highcharts x and y axes when the chart renders in another part of my app, so for example an array of number from 0 to n.
I am using highcharts-react-official - I can't find any documentation on methods that return the values as they are printed exactly on the screen.
Is this possible?
Update
With this code I can return an array of numbers that represent the tick values on the y axis:
const options: Highcharts.Options = {
yAxis: {
labels: {
formatter(): string {
console.log('YAXIS', Object.keys(this.axis.ticks)
}
}
}
}
Although the array includes a negative number which I cannot see in the chart axis itself.
The same trick does not work for the x axis.
Is there a cleaner approach do getting the correct result?
Update 2
Following the solution below I finally got what I was looking for:
This does not work as you would expect:
const res = this.yAxis
.map((axis) => axis.ticks)
.map((axisTicks) => Highcharts.objectEach(axisTicks, (tick) => tick.label.textStr));
console.log(res); // undefined;
However this does:
const yAxisVals: string[][] = [];
const axisTicks = this.yAxis.map((axis) => axis.ticks);
axisTicks.forEach((item, idx) => {
yAxisVals[idx] = [];
Highcharts.objectEach(item, (tick) => yAxisVals[idx].push(tick.label.textStr));
});
console.log(yAxisVals);
You can use render event and get the values by Axis.tickPositions property:
chart: {
events: {
render: function() {
var xAxisTickPositions = this.xAxis[0].tickPositions,
yAxisTickPositions = this.yAxis[0].tickPositions;
console.log(xAxisTickPositions, yAxisTickPositions);
}
}
}
Live demo: http://jsfiddle.net/BlackLabel/6m4e8x0y/4960/
API Reference:
https://api.highcharts.com/highmaps/chart.events.render
https://api.highcharts.com/class-reference/Highcharts.Axis#tickPositions

React JS — how do I add child components and set calculated transformations?

In a standard React JS app, how does one create objects from within, say, a componentDidMount method? These objects can be sub-components of the component I'm in, and I want to append them into my existing component which already is rendering a div with id core-categories (see below)
You'll notice my end goal is to be able to programmatically load the array (please consider that beyond the scope of this SO question--for now I am just hard-coding it), having it be of variable length, and then plot the X and Y for each item around a circle using the math you see above.
To do this, I am trying to use CSS transformations (beyond scope of this)
class XYZClass extends Component {
constructor() {
super();
this.state = {
transformed: false
}
this.nodes = [];
}
createNodes (nodes_array, radius) {
var numNodes = nodes_array.length;
var nodes = [],
width = (radius * 2) + 50,
height = (radius * 2) + 50,
angle,
x,
y,
i;
for (i=0; i<numNodes; i++) {
angle = (i / (numNodes/2)) * Math.PI; // Calculate the angle at which the element will be placed.
x = (radius * Math.cos(angle)) + (width/2); // Calculate the x position of the element.
y = (radius * Math.sin(angle)) + (width/2); // Calculate the y position of the element.
this.nodes.push({'id': i, 'name': nodes_array[i], 'x': x, 'y': y});
}
return this.nodes;
}
componentDidMount() {
setTimeout(() => {
this.setState({transformed: 'true'});
}, 0);
this.createNodes(['Apple','Bananna','Cherry','Date','Elderberry'], 250);
// create new node component for each of these 5 nodes
// append into the #core-categories div below programatically based on
// x and y values calcualted above
// ???????????????????????????????????????????????????
}
render() {
const { transformed } = this.state;
return (
<div>
<div id={'core-categories'} width={'500px'} height={'500px'}>
</div>
</div>
)
}
}
export default XYZClass;
Child components are created during render(). In componentDidMount(), you should call setState() to save the data for these components. Then in render() you can create the components from the data by, for instance, calling map() on an array. For example:
componentDidMount() {
setTimeout(() => {
this.setState({transformed: 'true'});
}, 0);
let nodes = this.createNodes(['Apple','Bananna','Cherry','Date','Elderberry'], 250);
this.setState({nodes});
}
render() {
const { transformed, nodes } = this.state;
return (
<div>
<div id={'core-categories'} width={'500px'} height={'500px'}>
{nodes.map((nodeProps) => <Node {...nodeProps}>)}
</div>
</div>
)
}
Note: I don't think you need the setTimeout(). You can call this.setState() directly. I left it anyway because it isn't critical to the question.

Use renderer for ExtJS form field

I'm trying to create a PercentField component that extends textfield for use on ExtJS forms. The behavior I'm looking for is for the field to display percent values, e.g. 25% or 400%, but have the underlying value when the user is editing the field or the form is being submitted be a decimal, e.g. .25 or 4.
I've succeeded in getting this working by using a renderer in a grid column, (Here's a fiddle) but it doesn't look like textfield has a renderer property for using the field in basic forms. I've looked at the rawToValue and valueToRaw methods, but they don't seem to be quite what I'm looking for. Any advice?
As far as I know, there's no possibility of template for form fields. That would require to flip the input element and display a div or something, on focus and blur. That would be doable, but that implies some fine tuned CSS.
A simpler option is to implement custom valueToRaw and rawToValue methods, and let Ext handles the value lifecycle (which is really the complicated part). You'll still have to change the raw value on focus and blur, but that remains pretty straightforward.
Here's an example you can build upon (see fiddle):
Ext.define('My.PercentTextField', {
extend: 'Ext.form.field.Text',
onFocus: function() {
this.callParent(arguments);
var v = this.getValue();
if (Ext.isNumeric(v)) {
this.setRawValue(this.rawToValue(v));
}
},
onBlur: function() {
this.callParent(arguments);
var v = this.getValue();
if (Ext.isNumeric(v)) {
this.setRawValue(this.valueToRaw(v));
}
},
valueToRaw: function(v) {
return Ext.isEmpty(v)
? ''
: v * 100 + ' %';
},
rawToValue: function(v) {
// cast to float
if (!Ext.isEmpty(v)) {
var pcRe = /^(\d*(?:\.\d*)?)\s*%$/,
dcRe = /^\d*(?:\.\d*)?$/,
precision = 2,
floatValue,
match;
if (match = dcRe.test(v)) { // decimal input, eg. .33
floatValue = v * 1;
} else if (match = pcRe.exec(v)) { // % input, eg. 33 %
floatValue = match[1] / 100;
} else {
// invalid input
return undefined;
}
floatValue = Number.parseFloat(floatValue);
if (isNaN(floatValue)) {
return undefined;
} else {
return floatValue.toFixed(precision);
}
} else {
return undefined;
}
}
});

Resources