I am trying to get the chart data from graphql and render it to the DOM.
All is fine as the data is showing, but is not synchronized with it's real values.
As I had to extract the data into a unique set of arrays, the data is not synchronized with it's underlying values.
How can I improve my component, so the data is in sync with it's labels?
import React, { useEffect, useState, useMemo } from "react";
import { useQuery, gql } from "#apollo/client";
import { Line, Pie, Radar, Bar } from "react-chartjs-2";
import { Chart, registerables } from "chart.js";
Chart.register(...registerables);
const total = gql`
query GetIntell($myLimit: Int!) {
webs(
pagination: { start: 0, limit: $myLimit }
filters: { site: { eq: "nist" } }
) {
data {
id
attributes {
dateAdded
severity
}
}
}
}
`;
export default function Graph({limit}) {
const { loading, error, data } = useQuery(total, {
variables: { myLimit: limit },
});
const [chartData, setChartData] = useState({});
const myLabels = useMemo(
() => (data ? data.webs.data.map((t) => t.attributes.dateAdded) : null),
[data]
);
const myData = useMemo(
() => (data ? data.webs.data.map((t) => t.attributes.severity) : null),
[data]
);
useMemo(() => {
setChartData({
labels: [...new Set(myLabels)], <---I am altering the data with this set. How can I recover the data with it's correct values and still get the unique array of values?
datasets: [
{
label: "Nist",
data: myData,
fill: true,
},
],
});
}, [data]);
let delayed;
if (loading) return <div className="loader center"></div>;
if (error) return <p>`Error! ${error}`</p>;
return (
<div className="container">
<div className="chart">
<Bar
data={chartData}
options={{
responsive: true,
plugins: {
title: {
display: true,
text: "Severity Accumulation",
},
legend: {
display: true,
position: "top",
},
},
}}
/>
</div>
</div>
);
}
Please see my pic bellow. The chart is fine, but still incorrect as it displayes values maybe for other days.
Any feedback is well received! Also if you can see an easier way of improving my code, I would owe you a lot!
Related
I'm using a custom CategoryFilter for my categories AgGrid:
const columnDefs = [
{
field: 'categoryId',
headerName: t('category', { ns: 'common' }),
cellRendererSelector: (params: ICellRendererParams<IProduct>) => ({
component: CategoryColumn,
params: {
...params,
defaultLanguage
}
}),
filter: CategoryFilter,
floatingFilterComponentParams: {
suppressFilterButton: false,
},
}
]
When I filter by category, I can enter the value in the input field, but it's still not showing up under the column's name:
How do I display the selected category under the "Category" column title?
You can pass the <CategoryColumn/> reference to the parent component via forwardRef and access the parent model's value via function params of useImperativeHandle inside the <CategoryColumn/>.
Example
CategoryColumn
import React, {
Fragment,
forwardRef,
useImperativeHandle,
useRef,
} from 'react';
import { IFloatingFilterParams } from 'ag-grid-community';
export default forwardRef((props: IFloatingFilterParams<any>, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
// expose AG Grid Filter Lifecycle callbacks
useImperativeHandle(ref, () => {
return {
onParentModelChanged(parentModel: number | null) {
console.log(parentModel)
// When the filter is empty we will receive a null value here
if (parentModel == null) {
inputRef.current!.value = '';
} else {
inputRef.current!.value = parentModel + '';
}
},
};
});
return (
<Fragment>
>{' '}
<input
ref={inputRef}
style={{ width: '30px' }}
type="number"
min="0"
/>
</Fragment>
);
});
index.tsx
...
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: 'athlete', filter: 'agTextColumnFilter' },
{
field: 'gold',
floatingFilterComponent: NumberFloatingFilterComponent,
floatingFilterComponentParams: {
suppressFilterButton: true,
},
filter: NumberFilterComponent,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
filter: true,
floatingFilter: true,
};
}, []);
...
Have read of Ag-grid Floating Filter!
So I have a component which uses a CellRenderer which gets some data:
import { useEffect, useMemo, useState } from "react";
import "ag-grid-community/dist/styles/ag-grid.min.css";
import "ag-grid-community/dist/styles/ag-theme-material.min.css";
import Grid from "../Common/Grid";
import axios from "axios";
import SelectJudetCellRenderer from "./SelectJudetCellRenderer";
function GetJudete() {
return axios
.get("http://localhost:5266/api/judete")
.then((response) => {
let data = response.data;
return data;
})
.catch((err) => {
console.log("Eroare la aducerea datelor.");
});
}
function Localitati() {
let [judete, setJudete] = useState([]);
useEffect(() => {
async function GetJudeteAsync() {
const result = await GetJudete();
setJudete(result);
}
GetJudeteAsync();
}, []);
const [columnDefs] = useState([
{ field: "nume", filter: "agTextColumnFilter", editable: true },
{ field: "judet", filter: "agTextColumnFilter", editable: true, cellRenderer: SelectJudetCellRenderer, cellRendererParams: {judete: judete} },
]);
return (
<Grid
baseLink="http://localhost:5266/api/localitati"
columnDefs={columnDefs}
/>
);
}
export default Localitati;
Here's my Cell renderer:
import { ICellRendererParams } from 'ag-grid-community';
export interface JudeteCellRendererParams extends ICellRendererParams {
judete: any[];
}
function SelectJudetCellRenderer(props: JudeteCellRendererParams) {
console.log(props.judete)
return (
<select name="judete">
{
props.judete.map((judet) =>
<option value={judet.id}>{judet.name}</option>
)
}
</select>
)
}
export default SelectJudetCellRenderer;
The problem is that after the Async call Judete is getting new data but my cell renderer does not get the new data.
The console.log() from the CellRenderer returns an empty array.
Why is this happening and how can I fix it?
Thanks.
You need to tell AG Grid to refresh the rendered cell, this is not very well documented, see https://www.ag-grid.com/javascript-data-grid/component-cell-renderer/#cell-renderer-component
Here is a simple example using Angular (should be similar for class based React)
Notice the refresh() method:
// gets called whenever the user gets the cell to refresh
refresh(params: ICellRendererParams) {
// set value into cell again
this.cellValue = this.getValueToDisplay(params);
}
https://plnkr.co/edit/yFqQHfNjxMLrPb9f.
For functional components you should explicitly call the api.refreshCells() when the data is available.
See here for more details: https://www.ag-grid.com/react-data-grid/component-cell-renderer/#component-refresh
A possible solution (although I think it would be more simple to switch to a class component renderer)
function Localitati() {
let [judete, setJudete] = useState([]);
// get hold of AG Grid gridApi
const gridApiRef = React.useRef<GridApi>();
// update the 'judete' column when new data is available (this will re-invoke the cell renderers)
useEffect(() => {
gridApiRef.current.refreshCells({columns: 'judet'});
}, [judete]);
useEffect(() => {
async function GetJudeteAsync() {
const result = await GetJudete();
setJudete(result);
}
GetJudeteAsync();
}, []);
const [columnDefs] = useState([
{ field: "nume", filter: "agTextColumnFilter", editable: true },
{ field: "judet", filter: "agTextColumnFilter", editable: true, cellRenderer: SelectJudetCellRenderer, cellRendererParams: {judete: judete} },
]);
return (
<Grid
baseLink="http://localhost:5266/api/localitati"
columnDefs={columnDefs}
onGridReady={({ api }) => {
gridApiRef.current = api;
}}
/>
);
}
I'm using REACT-PLOTLY.JS to create a scatter graph. I've got everything working apart from the graph redrawing it self every-time I change a data point when using a list of objects in the array data prop. BUT... when I manually write the array and it's containing objects, my graph does not re-draw when I change a data point, which is how it should work. The 2nd solution is not dynamic and is unusable. Can anyone help please?
re-draws graph when data point changes.
<Plot
data={plotlyData}
does not re-draw graph when data point is changed but is not dynamic and therefore unusable.
<Plot
data={[plotlyData[0],plotlyData[1]]}
I'm using functional components.
How plotData is generated. I'm using an API to get the coordinates for the X and Y axis.
import { React, useState } from "react";
import Plot from "react-plotly.js";
import { useQuery } from "react-query";
import axios from "axios";
const PlotlyGraph = (props) => {
const [plot, setPlot] = useState([]);///not in use
const { datasets } = props;
const QueryUuid = datasets.map((d) => {
// console.log("Individual Dataset:", d);
return `${d.age}-${d.kmax}-${d.frontK}-${d.pachymetry}`;
});
const { error, isLoading, data } = useQuery(
`data-${QueryUuid.join("-")}`,
() =>
axios.post("/api/calculate_cxl", {
age_baseline: datasets.map((d) => d.age),
kmax: datasets.map((d) => d.kmax),
front_k1: datasets.map((d) => d.frontK),
tpt2: datasets.map((d) => d.pachymetry),
color: datasets.map((d) => d.color),
})
);
let plotlyData;
if (error !== null || isLoading === true) {
plotlyData = [];
} else {
plotlyData = data.data.map((d, i) => {
return {
x: d.x,
y: d.y,
type: "scatter",
mode: "lines",
marker: { color: datasets[i].color },
line: {
width: 3,
},
name: `Patient ${i + 1}`,
showlegend: true,
};
});
}
console.log("plot:", plotlyData);
//- Graph Configuration
const config = {
editable: false,
scrollZoom: true,
displayModeBar: true,
displaylogo: false,
};
return (
<>
<Plot
data={plotlyData}
layout={{
yaxis: { range: [0, 1] },
xaxis: { range: [0, 5] },
autoSize: "true",
title: "Patient Comparison",
}}
style={{ width: "100%", height: " 700px" }}
useResizeHandler={true}
config={config}
revision={0}
// onInitialized={plot}
// onUpdate={(plot) => setPlot(plotlyData)}
/>
</>
);
};
export default PlotlyGraph;
I had a similar issue and was able to figure out how to dynamically update my plot with new data after every API fetch (see State Management). Since I don't have access to the API you are using, I've included my own example.
From my API, I fetch an object that looks like this:
{
28AD7D49F6E13C0A: [69.36, 64.11, 68.69, 62.1, ...],
28DDC649F6003C1C: [69.59, 63.18, 60.63, 63.08, ...],
Time: ['20:50:15', '20:50:17', '20:50:19', '20:50:21', ...]
}
Every two seconds, that objects gets updated with a new item in each array. When the state data gets updated with setData, the state object gets updated, which then causes the plot to render with new data.
Full example:
import React, { useState, useEffect } from "react";
import Plot from "react-plotly.js";
function TemperatureGraph() {
const [data, setData] = useState({});
const state = {
data: [
{
x: data["Time"],
y: data["28AD7D49F6E13C0A"],
name: "28AD7D49F6E13C0A",
type: "scatter",
mode: "lines+markers",
marker: {
color: "red",
},
},
{
x: data["Time"],
y: data["28DDC649F6003C1C"],
name: "28DDC649F6003C1C",
type: "scatter",
mode: "lines+markers",
marker: {
color: "black",
},
},
],
layout: {
width: 800,
height: 500,
title: "",
xaxis: {
title: "Time",
},
yaxis: {
title: "Temperature (F)",
},
},
frames: [],
config: {},
};
useEffect(() => {
const timer = setInterval(() => {
fetch("/get-temperatures")
.then((response) => response.json())
.then((data) => setData(data));
}, 2000);
return () => clearInterval(timer);
}, []);
return (
<div>
<Plot data={state.data} layout={state.layout} />
</div>
);
}
export default TemperatureGraph;
So I have been trying to accomplish the idea that whenever the user clicks any row in the DetailsList the Button gets enabled and when the user clicks outside the selectionzone the Button gets disabled.
This is my code
import { DetailsList, SelectionMode, Selection, ISelection, initializeIcons, PrimaryButton } from '#fluentui/react'
import {useMemo } from 'react'
import { useBoolean } from '#uifabric/react-hooks'
interface ICurrency {
type: string,
amount: number
}
function App() {
initializeIcons()
const [isBtn, { setTrue: disableBtn, setFalse: enableBtn }] = useBoolean(true)
const items: ICurrency[] = [
{
type: 'INR',
amount: 20
},
{
type: 'USD',
amount: 50
},
{
type: 'GBP',
amount: 70
}
]
const selection: ISelection = useMemo(() => new Selection(
{
onSelectionChanged: ()=>{
if(selection.getSelectedCount() > 0){
enableBtn()
}else{
disableBtn()
}
}
}
), [items])
return (
<div className="App">
<PrimaryButton text="Button" disabled={isBtn}/>
<DetailsList
items={items} selectionMode={SelectionMode.single}
selection={selection}
/>
</div>
);
}
export default App;
I even used useMemo and kept on banging my head but the problem persists where clicking any row the state is lost and the button is not enabled. I have already tried storing the state of selection also, count, everything but it seems I'm missing out on something essential or fundamental for the implementation
You need to memoize items because on each render it's being re-assigned and considered a new array which causes your selection to change because it relies on items as a useMemo dependency. On each state update the selection will reset.
So one way you can fix this is by moving the items out of the function so that it holds reference instead of creating a new items array on each render.
const items = [
{
type: "INR",
amount: 20
},
{
type: "USD",
amount: 50
},
{
type: "GBP",
amount: 70
}
];
function App() {
// code
}
or by using useMemo on those items:
const items = useMemo(() => [
{
type: "INR",
amount: 20
},
{
type: "USD",
amount: 50
},
{
type: "GBP",
amount: 70
}
],[]);
Also I see you have an error, the initializeIcons should only be called once. So that should probably be placed in useEffect:
useEffect(() => {
initializeIcons();
},[])
The final code sample should look like this:
import {
DetailsList,
SelectionMode,
Selection,
ISelection,
initializeIcons,
PrimaryButton
} from "#fluentui/react";
import { useMemo, useEffect } from "react";
import { useBoolean } from "#uifabric/react-hooks";
const items = [
{
type: "INR",
amount: 20
},
{
type: "USD",
amount: 50
},
{
type: "GBP",
amount: 70
}
];
function App() {
useEffect(() => {
initializeIcons();
}, []);
const [isBtn, { setTrue: disableBtn, setFalse: enableBtn }] = useBoolean(
true
);
const selection = useMemo(
() =>
new Selection({
onSelectionChanged: () => {
if (selection.getSelectedCount() > 0) {
enableBtn();
} else {
disableBtn();
}
}
}),
[items]
);
return (
<div className="App">
<PrimaryButton text="Button" disabled={isBtn} />
<DetailsList
items={items}
selectionMode={SelectionMode.single}
selection={selection}
/>
</div>
);
}
export default App;
The Accepted answer has the proper reasoning for the issue. I just wanted to post my solution too. I just had to store the state of the items
import { DetailsList, SelectionMode, Selection, initializeIcons, PrimaryButton } from '#fluentui/react'
import { useEffect, useState } from 'react'
import { useBoolean } from '#uifabric/react-hooks'
interface ICurrency {
type: string,
amount: number
}
function App() {
useEffect(() => {
initializeIcons();
}, []);
const [isBtn, { setTrue: disableBtn, setFalse: enableBtn }] = useBoolean(true)
let _selection = new Selection({
onSelectionChanged: () => {
if (_selection.getSelectedCount() > 0) {
enableBtn()
} else {
disableBtn()
}
}
});
let _initialItems: ICurrency[] = [
{
type: 'INR',
amount: 20
},
{
type: 'USD',
amount: 50
},
{
type: 'GBP',
amount: 70
}
]
const [items, setItems] = useState(_initialItems)
return (
<>
<PrimaryButton text="Button" disabled={isBtn} />
<DetailsList
items={items}
selection={_selection}
selectionMode={SelectionMode.single}
/>
</>
);
}
export default App;
Now say if the items are coming from some props or some state management, then just use setItems inside a useEffect and set the dependency as that source
I am working with Material-UI and getting data from the backend. There is no issue with the backend, but I don't know how to loop data and print it in a table format using Material-UI.
Can anyone guide me on how to print data in a table format?
Here is my code so far:
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { getProducts } from "../../services/products";
import MaterialTable, { MTableToolbar } from "material-table";
const productsList = props => {
const [data, setData] = useState([]);
const [state] = React.useState({
columns: [
{ title: "Brand", field: "brand" }, //assume here my backend schema is brand
{ title: "Price", field: "price" }, //here price
{ title: "Model no", field: "model" } //here model
]
});
const getProducts = async () => {
try {
const res = await getProducts();
setData(res.data);
console.log(res.data);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getProducts();
}, []);
return (
<MaterialTable
components={{
Toolbar: props => {
return (
<div>
<MTableToolbar {...props} />
</div>
);
}
}}
options={{
actionsColumnIndex: 5,
selection: true
}}
/>
);
};
export default function Company() {
return <productsList />;
}
You have to set the data and columns value. So try it like this:
import React, { useState, useEffect } from "react";
import MaterialTable, { MTableToolbar } from "material-table";
const fakeFetch = () => {
return new Promise(resolve => {
resolve({
data: [
{ brand: "brand 1", price: 1, model: "123" },
{ brand: "brand 2", price: 1, model: "456" },
{ brand: "brand 3", price: 1, model: "789" }
]
});
});
};
export default function App() {
const [data, setData] = useState([]);
// When the columns don't change you don't need to hold it in state
const columns = [
{ title: "Brand", field: "brand" }, //assume here my backend schema is brand
{ title: "Price", field: "price" }, //here price
{ title: "Model no", field: "model" } //here model
];
const getProducts = async () => {
try {
const res = await fakeFetch();
setData(res.data);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getProducts();
}, []);
return (
<MaterialTable
columns={columns} // <-- Set the columns on the table
data={data} // <-- Set the data on the table
components={{
Toolbar: props => {
return (
<div>
<MTableToolbar {...props} />
</div>
);
}
}}
options={{
actionsColumnIndex: 5,
selection: true
}}
/>
);
}
To make it even easier you could also provide your fetch function (fakeFetch in this case) as the data value;
data={fakeFetch} // <-- Using this you wouldn't need the [data, setData], getProducts and useEffect code.
Working sandbox link
As per the material-table approach, you have to put your whole fetched data on the data prop inside the MaterialTable component. So as far as I can understand, there is no looping made in this case by using the material-table library.
Assuming the attributes in your data object match the field names specified in your columns prop (if not, create an array of objects from your fetched data that matches the column fields or vice-versa).
And the code would be just the addition of the data prop in your table:
<MaterialTable
// ... existing props
data={data}
/>
Keep in mind that you could also use the remote data approach as described in the documentation which gives you the means to immediately query your data and fetch it inside the data prop of the table.