Is it possible to get / return Highcharts Axis values? - reactjs

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

Related

negative values missing in logarithmic mode

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

Count the duplicates in a string array using React JS

Following is a code I implemented to create a bar chart using chart js in React app. Here it creates a bar chart with all the data in an array. But, I want to change this code only to give the output in the x-axis - destination, y-axis - no. of occurrence of this destination since it has many repeated destinations.
I searched methods to this but I couldn't get a correct solution.
Can anyone help me to do this?
const dataArrayY4 = [];
res.data.map(item => {
dataArrayY4.push(item.time)
})
const dataArrayX4 = []
res.data.map(item => {
dataArrayX4.push(item.destination)
})
this.setState({
data4: dataArrayY4,
labels4: dataArrayX4,
});
This could be done as follows:
const res = {
data: [
{ time: 1, destination: 'A'},
{ time: 3, destination: 'A'},
{ time: 2, destination: 'B'}
]
};
let tmp4 = [];
res.data.map((o, i) => {
const existing = tmp4.find(e => e.destination == o.destination);
if (existing) {
existing.time += o.time;
} else {
tmp4.push({time: o.time, destination: o.destination});
}
})
this.setState({
data4: tmp.map(o => o.time);
labels4: tmp.map(o => o.destination);
});
Above code could further be optimized by using Array.reduce() instead of Array.map().
I would make the code more efficient. Instead of dataArrayY4 being an array, I would make it an object that has a key of value and the number of occurrence of each value. This way, you can count all the number of occurrences of the all items in res.data
const dataArrayY4 = {};
res.data.map(item => {
dataArrayY4[item.destination] = (dataArrayY4[item.destination] || 0) + 1
})
const dataArrayX4 = []
res.data.forEach(item => {
dataArrayX4.push(item.destination)
})
this.setState({
data4: dataArrayY4,
labels4: dataArrayX4,
});
Then if you want to look for the occurrence of a particular value you
use this eg. Sri Lanka
this.state.data4['Sri Lanka']

React : Pushing result of map() to an array

Hello I am trying to map through an array of objects and push them to a new array.
My ISSUE : only the last item of the object is being pushed to the new array
I believe this has to do with React life cycle methods but I don't know where I should I loop and push the values to the array to get the full list
//My object in an array named states
var states = [{"_id":"Virginia","name":"Virginia","abbreviation":"VN","__v":0},{"_id":"North Carolina","name":"North Carolina","abbreviation":"NC","__v":0},{"_id":"California","name":"California","abbreviation":"CA","__v":0}];
export function StateSelect()
{
**EDIT 1**
const options = [];
function getStates()
{
//This is how I am looping through it and adding to an array
{ states.length > 0 &&
states.map(item =>
(
console.log(`ITEM: ${JSON.stringify(item)}`),
options.push([{ value: `${item.name}`, label: `${item.name}`}])
))
}
}
return( {getStates()}: );
}
Thank you
It looks like your getStates() might not even be returning anything... but assuming it is, I believe you should be able to accomplish this using a forEach() fn in order to push values into your options array... Try adding the following into your map:
states.map((item) => {
console.log(`ITEM: ${JSON.stringify(item)}`);
let processed = 0;
item.forEach((i) => {
options.push([{ value: `${i.name}`, label: `${i.name}`}]);
processed++;
if(processed === item.length) {
// callback fn, or return
}
}
.map usually used to return another result, you could just use .forEach
In fact, you don't really need to declare options at all, just use .map on state to return the result would be fine.
return states.length > 0 && states.map(({ name }) => {
return { value: name, label: name };
});

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.

tfjs codelab 'making predictions from 2D data' use of values keyword in tf-vis.render.scatterplot

Why can't I replace values with another variable name like values1?
values is a js keyword, so I think there is something really fancy going on here.
In this codelab:
https://codelabs.developers.google.com/codelabs/tfjs-training-regression/index.html#2
In this code snippet:
async function run() {
// Load and plot the original input data that we are going to train on.
const data = await getData();
const values = data.map(d => ({
x: d.horsepower,
y: d.mpg,
}));
tfvis.render.scatterplot(
{name: 'Horsepower v MPG'},
{values},
{
xLabel: 'Horsepower',
yLabel: 'MPG',
height: 300
}
);
// More code will be added below
}
The tfjs documentation doesn't require the use of the values keyword:
https://js.tensorflow.org/api_vis/latest/#render.scatterplot
If I set up the HTML as instructed in the tutorial and use the same js code, I get the expected plot in my browser (firefox).
If I use the following js code, it breaks. Browser remains completely blank, console error message states: TypeError: r is undefined
In the code below, values in both places is changed to values1. That's the only change.
async function getData() {
const dataRequest = await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json');
const data = await dataRequest.json();
const cleaned = data.map(car => ({
mpg: car.Miles_per_Gallon,
hp: car.Horsepower,
}))
.filter(car => (car.mpg != null && car.hp != null));
return cleaned
}
async function run() {
// Load and plot the original input data that we are going to train on.
const data = await getData();
const values1 = data.map(d => ({
x: d.hp,
y: d.mpg,
}));
tfvis.render.scatterplot(
{name: 'Horsepower v MPG'},
{values1},
{
xLabel: 'Horsepower',
yLabel: 'MPG',
height: 300
}
);
// More code will be added below
}
document.addEventListener('DOMContentLoaded', run);
I'd expect the above code to result in a plot as with the original code, but I get no plot and a TypeError instead.
values is not a js keyword. It is a property of the object data that is a parameter of scatterplot. If you want to change the const values to values1, you have to do the following:
tfvis.render.scatterplot(
{name: 'Horsepower v MPG'},
{values: values1},
{
xLabel: 'Horsepower',
yLabel: 'MPG',
height: 300
}
)

Resources