React loading data for d3 linechart - reactjs

I'm both new to d3 and React, and I'm trying to draw a linechart with d3 with data I get from my backend through a async rest call.
When I used static data from a .csv file, it worked. But when I try to use the backend data, neither the xScale is on the correct position nor the lines are shown.
As far I can see from my console outputs, the data is formatted and fetched correctly when I'm trying to draw the lines.
Since neither the xScale or my lines are visible, my guess is that the zoom also doesn't work anymore.
Edit: I also noticed that the xAxis is displayed on top even when I use d3.axisBottom
Hope someone can help me.
type ChartDataType = {
timestamp: Date,
value: number
}
const LineChart = ({
width, height, top, right, bottom, left, fill, url, equipmentUUID
}: ZoomBasicLineChartProps) => {
const dispatch = useDispatch<AppDispatch>()
const [data, setData] = useState<ChartDataType[]>()
const title = useRef<string>('Default title, use label')
const containerRef = useRef<HTMLDivElement>()
let container: Selection<SVGGElement, unknown, HTMLElement, unknown>;
let zoom: d3.ZoomBehavior<HTMLDivElement, unknown>;
const chartWidth = width - left - right
const chartHeight = height - top - bottom
let xAxis: Selection<SVGGElement, unknown, HTMLElement, unknown>;
let yAxis: Selection<SVGGElement, unknown, HTMLElement, unknown>;
let path: any;
const diagramDTO = useSelector(LineChartDiagramSelectors.strippedLineChartDiagramData)
const loadStrippedLineChartDiagramData = useCallback(() => {
dispatch(LineChartDiagramDataThunks.getLineChartDiagramDataArray("services/performancemanagement/api/diagram/line-chart/7f5a2e69-0a51-4131-a77f-601ae9de24c6/0/SchlagstatistikGes_VR1?since=148"))
}, [dispatch])
const containerSelection = useMemo(() => (
d3.select<HTMLDivElement, unknown>('#ZoomLineChart')
), [containerRef.current])
const xScale = useMemo(() => {
const domain = data ? d3.extent(data, (d) => d.timestamp) : [new Date(), new Date()]
console.log("domain")
console.log(domain)
return d3
.scaleTime()
.domain([domain[0] ?? new Date(), domain[1] ?? new Date()])
.range([0, width])
}, [data])
const yScale = useMemo(() => {
const domain = data ? d3.extent(data, (d) => d.value) : [0, 0]
return d3
.scaleLinear()
.domain([domain[0] ?? 0, domain[1] ?? 0])
.range([height, 0])
}, [data])
const initSvg = (): SelectionType => (
containerSelection
.append('svg')
.attr('width', chartWidth + left + right)
.attr('height', chartHeight + top + bottom + 75)
.append('g')
.attr('transform', `translate(${left},${top})`)
)
const drawAxes = (g: SelectionType): void => {
xAxis = g.append('g')
.call(d3.axisBottom(xScale))
yAxis = g.append('g')
.call(d3.axisLeft(yScale))
}
const drawLabel = (g: SelectionType): void => {
g.append('text')
.attr('text-anchor', 'start')
.attr('y', height + 40)
.attr('x', 0)
.text(title.current)
}
const drawLines = (g: SelectionType): void => {
if (data) {
const lines = d3.line<ChartDataType>()
.x(d => {
// console.log(d.timestamp)
return xScale(d.timestamp)
})
.y(d => {
// console.log(d.value)
return yScale(d.value)
})
console.log("data")
console.log(data)
path = g
.append('g')
.attr('clip-path', "url(#clip)")
.append('path')
.datum(data)
.attr('class', 'line')
.attr('fill', 'none')
.attr('stroke', fill)
.attr('stroke-width', 1.5)
.attr('d', lines)
}
}
function updateChart(event: any) {
const {transform} = event;
const newX = transform.rescaleX(xScale);
xAxis.call(d3.axisBottom(newX));
path.attr("d", d3.line<ChartDataType>()
.x(d => newX(d.timestamp))
.y(d => yScale(d.value)));
}
const clip = (g: SelectionType): void => {
g.append("defs")
.append("SVG:clipPath")
.attr("id", "clip")
.append("SVG:rect")
.attr("width", width)
.attr("height", height)
.attr("x", 0)
.attr("y", 0);
}
const initZoom = (g: SelectionType): void => {
zoom = d3.zoom<HTMLDivElement, unknown>()
.scaleExtent([0.5, 5])
.on('zoom', updateChart)
containerSelection.call(zoom)
}
// Parse into data object
useEffect(() => {
if (diagramDTO) {
const updatedData = diagramDTO.payload?.map(dataPoint => {
const value = +dataPoint.value
return {timestamp: dataPoint.timestamp, value}
})
setData(updatedData)
} else {
loadStrippedLineChartDiagramData()
}
}, [diagramDTO])
useEffect(() => {
container = initSvg();
initZoom(container)
clip(container)
drawAxes(container)
drawLines(container)
drawLabel(container)
}, [data])
return (
<>
<Box id='ZoomLineChart' ref={containerRef}/>
</>
)
}
Example data playload:
{
"unit" : "unit.schlagstatistikges_vr1",
"label" : "label.schlagstatistikges_vr1",
"payload" : [ {
"timestamp" : "2022-06-08T03:22:00Z",
"value" : "10676"
}, {
"timestamp" : "2022-06-08T03:23:00Z",
"value" : "10583"
}, {
"timestamp" : "2022-06-08T03:24:00Z",
"value" : "10647"
}, {
"timestamp" : "2022-06-08T03:25:00Z",
"value" : "10585"
}, {
"timestamp" : "2022-06-08T03:26:00Z",
"value" : "10644"
}, {
"timestamp" : "2022-06-08T03:27:00Z",
"value" : "10227"
}, {
"timestamp" : "2022-06-08T03:28:00Z",
"value" : "10620"
}, {
"timestamp" : "2022-06-08T03:29:00Z",
"value" : "10635"
}, {
"timestamp" : "2022-06-08T03:30:00Z",
"value" : "10432"
}, {
"timestamp" : "2022-06-08T03:31:00Z",
"value" : "10295"
}, {
"timestamp" : "2022-06-08T03:32:00Z",
"value" : "10674"
}, {
"timestamp" : "2022-06-08T03:33:00Z",
"value" : "10715"
}, {
"timestamp" : "2022-06-08T03:34:00Z",
"value" : "10068"
}, {
"timestamp" : "2022-06-08T03:35:00Z",
"value" : "10262"
}, {
"timestamp" : "2022-06-08T03:36:00Z",
"value" : "10926"
}, {
"timestamp" : "2022-06-08T03:37:00Z",
"value" : "10271"
}, {
"timestamp" : "2022-06-08T03:38:00Z",
"value" : "10870"
} ],
"color" : "#80BEBF"
}

I found the problem: my IDE recognized the date object, but d3 required an explicit timeparse:
const updatedData = diagramDTO.payload?.map(dataPoint => {
const parser = d3.timeParse('%Y-%m-%dT%H:%M:%S.%LZ')
const timestamp = parser(String(dataPoint.timestamp))!
const value = +dataPoint.value
return {timestamp, value}
})

Related

How to get slate indentation with increasing depth

how to get numbered list when Tab key press on slate editor currently i get only one numbered list ?
export const toggleBlock = (editor, format) => {
const isActive = isBlockActive(editor, format);
const isList = LIST_TYPES.includes(format);
Transforms.unwrapNodes(editor, {
match: (n) =>
LIST_TYPES.includes(
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type
),
split: true,
});
const newProperties = {
type: isActive ? 'paragraph' : isList ? 'list-item' : format,
};
Transforms.setNodes(editor, newProperties);
if (!isActive && isList) {
const block = { type: format, children: [] };
Transforms.wrapNodes(editor, block);
}
};

REACT- Displaying and filtering specific data

I want to display by default only data where the status are Pending and Not started. For now, all data are displayed in my Table with
these status: Good,Pending, Not started (see the picture).
But I also want to have the possibility to see the Good status either by creating next to the Apply button a toggle switch : Show good menus, ( I've made a function Toggle.jsx), which will offer the possibility to see all status included Good.
I really don't know how to do that, here what I have now :
export default function MenuDisplay() {
const { menuId } = useParams();
const [selected, setSelected] = useState({});
const [hidden, setHidden] = useState({});
const [menus, setMenus] = useState([]);
useEffect(() => {
axios.post(url,{menuId:parseInt(menuId)})
.then(res => {
console.log(res)
setMenus(res.data.menus)
})
.catch(err => {
console.log(err)
})
}, [menuId]);
// If any row is selected, the button should be in the Apply state
// else it should be in the Cancel state
const buttonMode = Object.values(selected).some((isSelected) => isSelected)
? "apply"
: "cancel";
const rowSelectHandler = (id) => (checked) => {
setSelected((selected) => ({
...selected,
[id]: checked
}));
};
const handleClick = () => {
if (buttonMode === "apply") {
// Hide currently selected items
const currentlySelected = {};
Object.entries(selected).forEach(([id, isSelected]) => {
if (isSelected) {
currentlySelected[id] = isSelected;
}
});
setHidden({ ...hidden, ...currentlySelected });
// Clear all selection
const newSelected = {};
Object.keys(selected).forEach((id) => {
newSelected[id] = false;
});
setSelected(newSelected);
} else {
// Select all currently hidden items
const currentlyHidden = {};
Object.entries(hidden).forEach(([id, isHidden]) => {
if (isHidden) {
currentlyHidden[id] = isHidden;
}
});
setSelected({ ...selected, ...currentlyHidden });
// Clear all hidden items
const newHidden = {};
Object.keys(hidden).forEach((id) => {
newHidden[id] = false;
});
setHidden(newHidden);
}
};
const matchData = (
menus.filter(({ _id }) => {
return !hidden[_id];
});
const getRowProps = (row) => {
return {
style: {
backgroundColor: selected[row.values.id] ? "lightgrey" : "white"
}
};
};
const data = [
{
Header: "id",
accessor: (row) => row._id
},
{
Header: "Name",
accessor: (row) => (
<Link to={{ pathname: `/menu/${menuId}/${row._id}` }}>{row.name}</Link>
)
},
{
Header: "Description",
//check current row is in hidden rows or not
accessor: (row) => row.description
},
{
Header: "Status",
accessor: (row) => row.status
},
{
Header: "Dishes",
//check current row is in hidden rows or not
accessor: (row) => row.dishes,
id: "dishes",
Cell: ({ value }) => value && Object.values(value[0]).join(", ")
},
{
Header: "Show",
accessor: (row) => (
<Toggle
value={selected[row._id]}
onChange={rowSelectHandler(row._id)}
/>
)
}
];
const initialState = {
sortBy: [
{ desc: false, id: "id" },
{ desc: false, id: "description" }
],
hiddenColumns: ["dishes", "id"]
};
return (
<div>
<button type="button" onClick={handleClick}>
{buttonMode === "cancel" ? "Cancel" : "Apply"}
</button>
<Table
data={matchData}
columns={data}
initialState={initialState}
withCellBorder
withRowBorder
withSorting
withPagination
rowProps={getRowProps}
/>
</div>
);
}
Here my json from my api for menuId:1:
[
{
"menuId": 1,
"_id": "123ml66",
"name": "Pea Soup",
"description": "Creamy pea soup topped with melted cheese and sourdough croutons.",
"dishes": [
{
"meat": "N/A",
"vegetables": "pea"
}
],
"taste": "Good",
"comments": "3/4",
"price": "Low",
"availability": 0,
"trust": 1,
"status": "Pending",
"apply": 1
},
//...other data
]
Here my CodeSandbox
Here a picture to get the idea:
Here's the second solution I proposed in the comment:
// Setting up toggle button state
const [showGood, setShowGood] = useState(false);
const [menus, setMenus] = useState([]);
// Simulate fetch data from API
useEffect(() => {
async function fetchData() {
// After fetching data with axios or fetch api
// We process the data
const goodMenus = dataFromAPI.filter((i) => i.taste === "Good");
const restOfMenus = dataFromAPI.filter((i) => i.taste !== "Good");
// Combine two arrays into one using spread operator
// Put the good ones to the front of the array
setMenus([...goodMenus, ...restOfMenus]);
}
fetchData();
}, []);
return (
<div>
// Create a checkbox (you can change it to a toggle button)
<input type="checkbox" onChange={() => setShowGood(!showGood)} />
// Conditionally pass in menu data based on the value of toggle button "showGood"
<Table
data={showGood ? menus : menus.filter((i) => i.taste !== "Good")}
/>
</div>
);
On ternary operator and filter function:
showGood ? menus : menus.filter((i) => i.taste !== "Good")
If button is checked, then showGood's value is true, and all data is passed down to the table, but the good ones will be displayed first, since we have processed it right after the data is fetched, otherwise, the menus that doesn't have good status is shown to the UI.
See sandbox for the simple demo.

Nivo line chart custom mesh layer

I have nivo line chart with gaps like this:
Gaps are covered by passing y/value: null in november and december in data series
Tooltip displays only on data points and this is correct, but I want add tooltip at November and December with explanation why there is no data.
The solution is to add custom layer 'mesh' which is responsible for displaying tooltips on line chart.
You have to declare custom layers in <ResponsiveLine component:
layers={[
'grid',
'markers',
'axes',
'areas',
'crosshair',
'lines',
'slices',
'points',
CustomMesh,
'legends',
]}
Create CustomMesh component:
const CustomMesh = (layerData: any) => {
const { showTooltipAt, hideTooltip } = useTooltip();
const handleMouseEnter = (point: any) => {
showTooltipAt(
layerData.tooltip({ point }),
[point.x + layerData.margin.left, point.y + layerData.margin.top],
'top'
);
};
const handleMouseMove = (point: any) => {
showTooltipAt(
layerData.tooltip({ point }),
[point.x + layerData.margin.left, point.y + layerData.margin.top],
'top'
);
};
const handleMouseLeave = (point: any) => {
hideTooltip();
};
const nullValuePoints = layerData.series.reduce((acc: any[], cur: any) => {
cur.data.forEach(({ data, position }: any) => {
if (data.y === null) {
const point = {
x: position.x,
y: 100, //whatever you want
data: {
x: data.x,
},
};
acc.push(point);
}
});
return acc;
}, []);
return (
<Mesh
nodes={[...layerData.points, ...nullValuePoints]}
width={layerData.width}
height={layerData.height}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
debug={layerData.debugMesh}
/>
);
};
When nullValuePoints are my custom points with no data
Import required packages:
import { Mesh } from '#nivo/voronoi';
import { useTooltip } from '#nivo/tooltip';
result:

REACT UPDATE A DATA

I have a problem trying to update an Array of Objects that lives in a Themecontext, my problem is with mutation, I'm using Update from Immutability helpers. the thing is that when I update my array in my specific element, This appears at the end of my object.
This is my code:
function changeValueOfReference(id, ref, newValue) {
const namevalue = ref === 'colors.primary' ? newValue : '#';
console.warn(id);
const data = editor;
const commentIndex = data.findIndex(function(c) {
return c.id === id;
});
const updatedComment = update(data[commentIndex], {styles: { value: {$set: namevalue} } })
var newData = update(data, {
$splice: [[commentIndex, 1, updatedComment]]
});
setEditor(newData);
this is my result:
NOTE: before I tried to implement the following code, but this mutates the final array and break down my test:
setEditor( prevState => (
prevState.map( propStyle => propStyle.styles.map( eachItem => eachItem.ref === ref ? {...eachItem, value: namevalue}: eachItem ))
))
Well, I finally understood the issue:
1 - commentIndex always referenced to 0
The solution that worked fine for me:
1 - Find the index for the Parent
2 - Find the index for the child
3 - Add an array []
styles : { value: {$set: namevalue} } => styles :[ { value: [{$set: namevalue}] } ]
Any other approach is Wellcome
Complete Code :
function changeValueOfReference(id, referenceName, newValue) {
const data = [...editor];
const elemIndex = data.findIndex((res) => res.id === id);
const indexItems = data
.filter((res) => res.id === id)
.map((re) => re.styles.findIndex((fil) => fil.ref === referenceName));
const updateItem = update(data[elemIndex], {
styles: {
[indexItems]: {
value: { $set: namevalue },
variableref: { $set: [''] },
},
},
});
const newData = update(data, {
$splice: [[elemIndex, 1, updateItem]],
});
setEditor(newData);
}

useState does not update the state

I'm trying to update a state using useState hook, however the state won't update. I've checked how to fix it but really have no idea about it what cause this point. This is the whole code I didnt include the urls and import files...
When onchange method trigger ilceZoom function event has value so ı can get it evt.value example values is "1234" but I can not set it using useState future
const ilceUrl = 'URL';
const AddressSearchMaks = (props) => {
useEffect(() => {
ilceLoad();
}, []);
const [ ilceler, setIlceler ] = useState([]);
const [ selectedIlce, setSelectedIlce ] = useState(null);
let queryTask;
let query;
let sfs;
let lineSymbol;
let polygon;
let polyline;
let graphic;
let extent;
let point;
let wMercatorUtils;
let rfConverter;
loadModules([
'esri/tasks/query',
'esri/tasks/QueryTask',
'esri/symbols/SimpleFillSymbol',
'esri/symbols/SimpleLineSymbol',
'esri/geometry/Polygon',
'esri/geometry/Polyline',
'esri/geometry/webMercatorUtils',
'esri/geometry/Extent',
'esri/geometry/Point',
'esri/graphic',
'esri/Color',
'libs/ReferenceConverter'
]).then(
(
[
Query,
QueryTask,
SimpleFillSymbol,
SimpleLineSymbol,
Polygon,
Polyline,
webMercatorUtils,
Extent,
Point,
Graphic,
Color,
referenceConverter
]
) => {
queryTask = QueryTask;
query = Query;
polygon = Polygon;
polyline = Polyline;
graphic = Graphic;
extent = Extent;
point = Point;
wMercatorUtils = webMercatorUtils;
rfConverter = referenceConverter;
sfs = new SimpleFillSymbol(
SimpleFillSymbol.STYLE_SOLID,
new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([ 0, 255, 255 ]), 4),
new Color([ 140, 140, 140, 0.25 ])
);
lineSymbol = new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([ 0, 255, 255 ]), 4).setWidth(4);
}
);
const getAdres = async (url) => {
let response = await fetch(url);
let data = await response.json();
let list = [];
data.AdresList.Adresler.Adres.forEach((item) => {
list.push({
label: item.ADI,
value: item.ID,
lat: item.LAT,
lon: item.LON
});
});
return list;
};
async function ilceLoad() {
let ilceList = await getAdres(ilceUrl);
setIlceler(ilceList);
}
const convertExtent = (lat, lon) => {
let p;
let ext;
const sr = props.map.spatialReference;
if (sr.wkid == 102100) {
const _p = wMercatorUtils.lngLatToXY(lon, lat);
ext = extent({
xmin: _p[0],
ymin: _p[1],
xmax: _p[0],
ymax: _p[1],
spatialReference: props.map.spatialReference
});
} else {
const res = rfConverter.WgsToItrf(lat, lon);
ext = extent({
xmin: res.x,
ymin: res.y,
xmax: res.x,
ymax: res.y,
spatialReference: props.map.spatialReference
});
p = point(res.x, res.y);
}
p.spatialReference = sr;
return ext;
};
const ilceZoom = (evt) => {
setSelectedIlce(evt.value);
console.log('selectedIlce', selectedIlce);
setError(false);
console.log('error', error);
const qTask = queryTask(maksIlce);
const q = query();
q.returnGeometry = true;
q.outFields = [ '*' ];
q.outSpatialReference = { wkid: 5254 };
q.where = `KIMLIKNO=${evt.value}`;
qTask.execute(q, (evt) => {
const polyGon = polygon({
rings: evt.features[0].geometry.rings
});
props.map.graphics.add(graphic(polyGon, sfs));
});
const extent = convertExtent(evt.lat, evt.lon);
props.map.setExtent(extent);
mahalleLoad();
};
return (
<Select name='adresSelect' options={ilceler} onChange={(e) => ilceZoom(e)} placeholder='İlçe Seçiniz' />
);
};
const mapStateToProps = (state) => ({
map: state.map.map
});
export default connect(mapStateToProps, null)(AddressSearchMaks);
It can be related for some environment binding issue. Try to use the the setState as function:
useEffect(() => {
ilceLoad();
}, []);
const [ ilceler, setIlceler ] = useState([]);
const [ selectedIlce, setSelectedIlce ] = useState(null);
async function ilceLoad() {
let ilceList = await getAdres(ilceUrl);
setIlceler(ilceList); // update the state, it works here
}
const ilceZoom = (evt) => {
setSelectedIlce(prev => {
console.log("prev: ", prev);
console.log("evt.value: ", evt.value);
return evt.value;
});
const qTask = queryTask(url);
const q = query();
q.returnGeometry = true;
q.outFields = [ '*' ];
q.outSpatialReference = { wkid: 5254 };
q.where = `VARIABLE NAME=${evt.value}`;
qTask.execute(q, (evt) => {
const polyGon = polygon({
rings: evt.features[0].geometry.rings
});
props.map.graphics.add(graphic(polyGon, sfs));
});
const extent = convertExtent(evt.lat, evt.lon);
props.map.setExtent(extent);
};
Can you try like this. Because, in your code, you setting the data in selectedIlce, but before it re-render, your trying to checking the value in the console, so better use your console outside the event function, so that when it get updated, it will reflect in the console.
console.log('selectedIlce', selectedIlce);
const ilceZoom = (evt) => {
setSelectedIlce(evt.value);
....
}

Resources