Change colour of clicked item in deck.gl - reactjs

I'm trying to achieve the same behaviour that we have with the autoHighlight prop on mobile where clicking on an item 'selects' it and changes its colour. I managed to make it work with the updateTriggers prop of deck.gl but I feel like I'm missing something because the update takes 3 seconds and the layer isn't responsive in this time.
Here's what a have so far:
const App = () => {
const [selectedItem, setSelectedItem] = useState(null);
const layers = [
new H3ClusterLayer({
id: 'h3-cluster-layer',
data,
pickable: true,
stroked: true,
filled: true,
extruded: false,
getHexagons: d => d.hexIds,
getFillColor: hexagon => {
if (selectedItem && hexagon.hexIds[0] === selectedItem.hexIds[0]) return [25,116,210]
if (hexagon.mean === undefined) return [122, 122, 122];
return [255, (1 - hexagon.mean / 500) * 255, 0];
},
updateTriggers: {
getFillColor: [selectedItem]
},
getLineColor: [255, 255, 255],
lineWidthMinPixels: 1,
opacity: 0.03,
autoHighlight: true,
highlightColor: [142,223,255],
}),
];
return (
<DeckGL
initialViewState={initialViewState}
controller={true}
layers={layers}
onClick={(info, event) => {
// event bubble cancelation doesn't work between react components and deck.gl
if (event.target !== document.getElementById('view-default-view')) return;
setSelectedItem(info.object)
}}
>
<StaticMap mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN} />
{selectedItem && <Details selectedItem={selectedItem} />}
</DeckGL>
);
}

For anyone coming across this: rerendering of the layer works perfectly if you only pass the id (rather than the whole object) to updateTriggers. In my case I just needed to pass the following to updateTriggers:
updateTriggers: {
getFillColor: [selectedItem ? selectedItem.hexIds[0]: null]
},

Related

ChartJS 3 Doesn't Show Data Until A Legend Is Clicked

I get some data from back-end to show. When I inspect element with React Developer Tools, I can see that data is there but not shown in production. ChartJS version is 3.8, not react-chartjs
I was having the same problem in development, too, but solved it by setting a unique key with key={Math.random()}. In development build, it works just fine. Problem occurs in production. I deploy my app on Firebase.
I wait for data before rendering:
{isAnyFetching ? "Loading..." : <BarChart01 data={chartData} width={595} height={248} key={Math.random()} />}
I tried giving an array of zeroes until data is loaded to be sure chartData changed to trigger re-render by changing the state of the chart component. I also tried giving an extraKey prop and change it with useEffect to re-render again.
The whole chart component is:
function BarChart01({
data,
width,
height
}) {
const canvas = useRef(null);
const legend = useRef(null);
useEffect(() => {
const ctx = canvas.current;
// eslint-disable-next-line no-unused-vars
const chart = new Chart(ctx, {
type: 'bar',
data: data,
options: {
layout: {
padding: {
top: 12,
bottom: 16,
left: 20,
right: 20,
},
},
scales: {
y: {
grid: {
drawBorder: false,
},
ticks: {
maxTicksLimit: 6,
callback: (value) => formatValue(value),
},
},
x: {
type: 'time',
time: {
parser: 'MM-DD-YYYY',
unit: 'month',
displayFormats: {
month: 'MMM YY',
},
},
grid: {
display: false,
drawBorder: false,
},
},
},
plugins: {
legend: {
display: true,
},
tooltip: {
callbacks: {
title: () => false, // Disable tooltip title
label: (context) => formatValue(context.parsed.y),
},
},
},
interaction: {
intersect: false,
mode: 'nearest',
},
animation: {
duration: 500,
},
maintainAspectRatio: false,
resizeDelay: 200,
},
plugins: [{
id: 'htmlLegend',
afterUpdate(c, args, options) {
const ul = legend.current;
if (!ul) return;
// Remove old legend items
while (ul.firstChild) {
ul.firstChild.remove();
}
// Reuse the built-in legendItems generator
const items = c.options.plugins.legend.labels.generateLabels(c);
items.forEach((item) => {
const li = document.createElement('li');
li.style.marginRight = tailwindConfig().theme.margin[4];
// Button element
const button = document.createElement('button');
button.style.display = 'inline-flex';
button.style.alignItems = 'center';
button.style.opacity = item.hidden ? '.3' : '';
button.onclick = () => {
c.setDatasetVisibility(item.datasetIndex, !c.isDatasetVisible(item.datasetIndex));
c.update();
};
// Color box
const box = document.createElement('span');
box.style.display = 'block';
box.style.width = tailwindConfig().theme.width[3];
box.style.height = tailwindConfig().theme.height[3];
box.style.borderRadius = tailwindConfig().theme.borderRadius.full;
box.style.marginRight = tailwindConfig().theme.margin[2];
box.style.borderWidth = '3px';
box.style.borderColor = item.fillStyle;
box.style.pointerEvents = 'none';
// Label
const labelContainer = document.createElement('span');
labelContainer.style.display = 'flex';
labelContainer.style.alignItems = 'center';
const value = document.createElement('span');
value.style.color = tailwindConfig().theme.colors.slate[800];
value.style.fontSize = tailwindConfig().theme.fontSize['3xl'][0];
value.style.lineHeight = tailwindConfig().theme.fontSize['3xl'][1].lineHeight;
value.style.fontWeight = tailwindConfig().theme.fontWeight.bold;
value.style.marginRight = tailwindConfig().theme.margin[2];
value.style.pointerEvents = 'none';
const label = document.createElement('span');
label.style.color = tailwindConfig().theme.colors.slate[500];
label.style.fontSize = tailwindConfig().theme.fontSize.sm[0];
label.style.lineHeight = tailwindConfig().theme.fontSize.sm[1].lineHeight;
const theValue = c.data.datasets[item.datasetIndex].data.reduce((a, b) => a + b, 0);
const valueText = document.createTextNode(formatValue(theValue));
const labelText = document.createTextNode(item.text);
value.appendChild(valueText);
label.appendChild(labelText);
li.appendChild(button);
button.appendChild(box);
button.appendChild(labelContainer);
labelContainer.appendChild(value);
labelContainer.appendChild(label);
ul.appendChild(li);
});
},
}],
});
chart.update();
return () => chart.destroy();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
return (
<>
<div className="px-5 py-3">
<ul ref={legend} className="flex flex-wrap"></ul>
</div>
<div className="grow">
<canvas ref={canvas} width={width} height={height}></canvas>
</div>
</>
);
}
According to the code, it seems like
button.onclick = () => {
c.setDatasetVisibility(item.datasetIndex,!c.isDatasetVisible(item.datasetIndex));
c.update();
};
part in forEach loop is responsible of this update operation when I click on a label. So it somehow doesn't call update function in production as it should as useEffect listens to data prop.

Where to add custom column types to filter panel - MUI Datagrid

I cannot figure out where to add my custom column type inside of my DataGrid. I tried adding it to the filterPanel inside of the DataGrid, but it didn't seem to work. I can't find anything in the documentation showing WHERE to add the custom column type.
Here is the documentation for creating a column type: https://mui.com/x/react-data-grid/filtering/#:~:text=to%20start%20editing-,Custom%20column%20types,-When%20defining%20a
Here is my code:
export const MultiSelectColumnType: GridColTypeDef = {
extendType: 'singleSelect',
filterOperators: getGridSingleSelectOperators().filter(
(operator) => operator.value === 'is' || operator.value === 'not' || operator.value === 'isAnyOf',
),
};
Here is my DataGrid:
<DataGridPro
selectionModel={selectionModel()}
hideFooterSelectedRowCount
disableSelectionOnClick
keepNonExistentRowsSelected
rows={TableRows}
columns={TableColumns}
loading={tableIsLoading}
rowsPerPageOptions={[5, 10, 25, 50, 100, 500]}
pagination
paginationMode="server"
sortingMode="server"
filterMode="server"
filterModel={filterModel}
onFilterModelChange={(model, details) => {
setFilterModel(model)
}}
page={currentPagination.page}
rowCount={totalRows}
pageSize={currentPagination.page_size}
onPageChange={(PageNumber, details) => {
setCurrentPagination({ page: PageNumber, page_size: currentPagination.page_size });
}}
onPageSizeChange={(numberOfRows, details) => {
setCurrentPagination({ page: 0, page_size: numberOfRows });
}}
onSortModelChange={(cols, details) => {
//TODO: [CNG-446] Add multi sorting to server
setCurrentSorting({
sortby: _.get(cols, [0, 'field'], 'id'),
direction: _.get(cols, [0, 'sort'], 'desc'),
});
}}
density="compact"
components={{
Toolbar: CustomToolBar,
Footer: CustomFooter,
}}
componentsProps={{
toolbar: {
tableData: rows.length,
totalData: totalRows,
customToolbarButton,
tableType,
},
footer: {
totalRows: !isAllSelected ? selectedRows.length : totalRows - excludedRows.length,
},
filterPanel: {
columnTypes: MultiSelectColumnType, // I thought to add it here
linkOperators: ['and']
}
}}
onCellClick={(data, event, details) => {
}}
localeText={{
toolbarDensity: translate('Density'),
toolbarDensityCompact: translate('Compact'),
toolbarDensityStandard: translate('Standard'),
toolbarDensityComfortable: translate('Comfortable'),
toolbarExport: translate('Export'),
toolbarColumns: translate('Columns'),
toolbarColumnsLabel: 'Select columns',
columnsPanelTextFieldLabel: translate('Find column'),
columnsPanelTextFieldPlaceholder: translate('Column title'),
columnsPanelDragIconLabel: translate('Reorder column'),
columnsPanelShowAllButton: translate('Show all'),
columnsPanelHideAllButton: translate('Hide all'),
noRowsLabel: translate('No rows'),
MuiTablePagination: {
labelRowsPerPage: translate('Rows Per Page:'),
labelDisplayedRows: ({ from, to, count }) => (
<span id="rtl-label-display-rows-container">
<span> {`${from} - ${to}`} </span> <span>{`${translate('Of')} ${count}`}</span>
</span>
),
},
}}
autoHeight
sx={{
border: 'none',
}}
/>
```

Swiper JS How zoom-in or zoom-out image several times on click?

I want to make two buttons "-" and "+" in my gallery on swiper js. How can I increment/decrement scale on every click? I need to keep the ability to move around the image!
Now I have this code in my React App:
const maxScale = 5;
const [scale, setScale] = useState(1);
const handleMagnifier = (type) => {
if (scale < maxScale && type === 'in') {
setScale(scale + 1);
swiperRef.current?.zoom.in();
} else if (scale <= maxScale && type === 'out') {
setScale(scale - 1);
scale === 1 ? swiperRef.current?.zoom.out() : swiperRef.current?.zoom.in();
}
};
render
<IconButton onClick={handleMagnifier('out')}>
<Icon name='zoom-out' />
</IconButton>
<IconButton onClick={handleMagnifier('in')}>
<Icon name='zoom-in' />
</IconButton>
<Swiper
slides={slides}
SwiperOptions={{
mousewheel: true,
freeMode: true,
onSwiper,
slidesPerView: 1,
keyboard: {
enabled: true,
},
preloadImages: false,
lazy: {
checkInView: true,
loadPrevNext: true,
},
observer: true,
onSlideChange,
onAfterInit: (): void => setInited(true), // Prevent flickering
thumbs: {
swiper: thumbsSwiper,
autoScrollOffset: 0,
},
initialSlide: initialIndex,
zoom: {
maxRatio: scale,
minRatio: 1,
},
}}
{...(items.length > 1 && navigationProps)}
/>
But it doesn't work.
I tried to search examples, read documentation, but there is no decision.

Data not displayed after rows updated in Material Ui Datagrid

This component is used to display the users. Once a new user is added from another component usersUpdated gets toggled and a call is made to backend to fetch all the users again which contains the newly added user and display in the Datagrid. But the datagrid does not display any record and distorts the datagrid UI. If the page is refreshed or some other action is performed in Datagrid like changing the pageSize displays all the records properly.
const UsersDisplayTable = (props) => {
const usersUpdated = props.usersUpdated;
const [columns, setColumns] = useState(
[
{
field: 'email',
headerName: 'Email',
align: "left",
headerAlign: "left",
flex: 1,
filterable: true
},
{
field: 'dateOfBirth',
headerName: 'Date Of Birth',
align: "center",
headerAlign: "center",
flex: 0.75,
filterable: false,
sortable: false,
valueFormatter: (params) => {
const valueFormatted = moment(
new Date(params.row.dateOfBirth)).format(
'DD MMM YYYY');
return `${valueFormatted}`;
}
},
{
field: "actions",
headerName: "Actions",
sortable: false,
filterable: false,
align: "center",
headerAlign: "center",
flex: 0.75,
renderCell: (params) => {
return (
<>
<EditUserIcon
onClick={(e) => props.editUser(
e, params.row)}
title='Edit'/>
</>
);
}
}
]
);
const [allUsers, setAllUsers] = useState([]);
useEffect(() => {
axios
.get("/get-all-users")
.then(data => {
setAllUsers(data.data.data)
}).catch(error => {})
}, [usersUpdated])
return (
<>
<DataGrid
sortingOrder={["desc", "asc"]}
rows={allUsers} columns={columns}
disableSelectionOnClick
disableColumnSelector />
</>
);
}
export default UsersDisplayTable;
Initial load of datagrid
after adding dynamic row or user
Is this a limitation of Material UI Datagrid?
I was experiencing the same issue using #mui/x-data-grid version 5.0.1.
I was able to get around this issue by setting up a useEffect with a dependency on my rows. Within this use effect I just toggle a state variable which I use for the row height in my grid.
const [rowHeight, setRowHeight] = useState(28);
useEffect(() => {
if (rowHeight === 28) {
setRowHeight(29);
}else {
setRowHeight(28);
}
}, [rows]);
...
<DataGrid
rows={rows}
columns={columns}
pageSize={pgSize}
rowHeight={rowHeight}
...otherProps
/>
I think by changing the height it's triggering a re-render of the grid and its contents.
This solution is a hack to work-around a bug in the code.
I found the same issue on #mui/x-data-grid v5.17.14 (and Next.js 13 if that has anything to do with it)
In my case, the bug was when changing to a new page. I was pushing that change of page to the query params of the URL instead of using state, and then reading from there like this:
export default function usePagination(initialValues?:PaginationOptions) {
const {query, push} = useRouter();
const pageSize = Number(query.pageSize) > 0 ? Number(query.pageSize) : initialValues?.initialPageSize ?? GLOBAL_DEFAULT_PAGE_SIZE;
const page = Number(query.page) > 0 ? Number(query.page) : initialValues?.initialPage ?? 1;
const setPage = (input:number) => push({query:{...query, page:input+1}});
const setPageSize = (input:number) => push({query:{...query, pageSize:input}});
return {page, pageSize, setPage, setPageSize};
}
That doesn't work because datagrid must somehow be checking which parameter caused the re-render, and since this change was coming from a change in URL, it didn't react properly.
Forcing to use a useState for the page fixes the issue:
export default function usePagination(initialValues?:PaginationOptions) {
const {query, push} = useRouter();
const pageSize = Number(query.pageSize) > 0 ? Number(query.pageSize) : initialValues?.initialPageSize ?? GLOBAL_DEFAULT_PAGE_SIZE;
const [page,setPageState] = useState(Number(query.page) > 0 ? Number(query.page) : initialValues?.initialPage ?? 1);
const setPage = (input:number) => {setPageState(input+1); push({query:{...query, page:input+1}})};
const setPageSize = (input:number) => push({query:{...query, pageSize:input}});
return {page, pageSize, setPage, setPageSize};
}

React useEffect doesn't change data displayed on map

I'm playing with uber's react-map-gl and deck-gl libraries to try to display data dynamically.I've got 2 components in my react little app.A navigation bar and a Mapbox map.So here is my pipeline.When the page first loads,only the map is displayed without any marker or visual data.But when i click on one of navigation bar link,an action creator gets called,when i make an ajax call to fetch some data,and from the action that is dispatched a pass my response data as payload and the reducer is reached so that i have a new version of the store.the data that i would like to visualize in they stor key : mainProjects that contains an array of geojson.My click on a navbar link succefully updates that value,and in the actual Map component,i can load the new values but useEffect does not update my localstate.Here is my map code:
import React, { useState, useEffect, useContext } from "react";
import { StaticMap } from "react-map-gl";
import { MapContext } from "./contexts/MapProvider";
import DeckGL, { GeoJsonLayer } from "deck.gl";
import chroma from "chroma-js";
import { connect } from "react-redux";
const MAPBOX_TOKEN =
"pk.mykey";
const mapStyle = "mapbox://mymapstyle";
function getEnergyFillColor(regime) {
const ENERGY_COLORS = {
naturalGaz: "#FF8500",
coal: "#99979A",
nuclear: "#E0399E",
hydroelectric: "#0082CB",
Wind: "#00B53B",
solar: "#DBCA00",
oil: "#FF0009",
other: "#FFEFD3"
};
let color;
switch (regime) {
case "Hydraulique":
color = ENERGY_COLORS.hydroelectric;
break;
case "Thermique":
color = ENERGY_COLORS.nuclear;
break;
default:
color = ENERGY_COLORS.other;
break;
}
color = chroma(color)
.alpha(0.667)
.rgba();
color[3] *= 255;
return color;
}
function _onClick(info) {
if (info.object) {
// eslint-disable-next-line
alert(
`${info.object.properties.NOM} (${info.object.properties.PROVINCE}) - ${
info.object.properties.PUISSANCE
}MW`
);
}
}
function Map({ mainProjects }) {
const { viewport, setViewport, onLoad } = useContext(MapContext);
const [airports, setAireports] = useState();
const [electricalEnergy, setElectricalEnergy] = useState();
const [hospitals, setHospitals] = useState();
const [roads, setRoads] = useState();
useEffect(() => {
if (mainProjects.length) {
setHospitals(mainProjects[0].hospitals);
setAireports(mainProjects[1].aeroports);
setElectricalEnergy(mainProjects[2].electricite);
setRoads(mainProjects[2].routes);
}
}, [airports, electricalEnergy, hospitals, roads]);
const layers = [
//ENERGIE ELECTRIQUE
new GeoJsonLayer({
id: "energy",
data: electricalEnergy,
// Styles
filled: true,
pointRadiusMinPixels: 20,
opacity: 1,
pointRadiusScale: 2000,
getRadius: energyItem => energyItem.properties.puissance * 3.14,
getFillColor: energyItem =>
getEnergyFillColor(energyItem.properties.regime),
// Interactive props
pickable: true,
autoHighlight: true,
onClick: _onClick
}),
//AEROPORTS
new GeoJsonLayer({
id: "airports",
data: airports,
// Styles
filled: true,
pointRadiusMinPixels: 20,
opacity: 1,
pointRadiusScale: 2000,
getRadius: energyItem => energyItem.properties.PUISSANCE * 3.14,
getFillColor: energyItem =>
getEnergyFillColor(energyItem.properties.REGIME),
// Interactive props
pickable: true,
autoHighlight: true,
onClick: _onClick
}),
//HOSPITALS
new GeoJsonLayer({
id: "hospitals",
data: hospitals,
// Styles
filled: true,
pointRadiusMinPixels: 20,
opacity: 1,
pointRadiusScale: 2000,
getRadius: energyItem => energyItem.properties.PUISSANCE * 3.14,
getFillColor: energyItem =>
getEnergyFillColor(energyItem.properties.REGIME),
// Interactive props
pickable: true,
autoHighlight: true,
onClick: _onClick
}),
//ROUTES
new GeoJsonLayer({
id: "roads",
data: roads,
pickable: true,
stroked: false,
filled: true,
extruded: true,
lineWidthScale: 20,
lineWidthMinPixels: 2,
getFillColor: [160, 160, 180, 200],
getLineColor: d => [255, 160, 20, 200],
// getLineColor: d => colorToRGBArray(d.properties.color),
getRadius: 100,
getLineWidth: 1,
getElevation: 30,
onHover: ({ object, x, y }) => {
// const tooltip = object.properties.name || object.properties.station;
/* Update tooltip
http://deck.gl/#/documentation/developer-guide/adding-interactivity?section=example-display-a-tooltip-for-hovered-object
*/
},
onClick: _onClick
})
];
return (
<>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.0/mapbox-gl.css"
rel="stylesheet"
/>
<DeckGL
initialViewState={viewport}
viewState={viewport}
controller={true}
layers={layers}
onLoad={onLoad}
onViewportChange={nextViewport => setViewport(nextViewport)}
>
<StaticMap mapboxApiAccessToken={MAPBOX_TOKEN} mapStyle={mapStyle} />
</DeckGL>
</>
);
}
const mapStateToProps = ({
selectedLinks: { sectorSelected, provinceSelected, subSectorSelected },
mainProjects
}) => {
if (sectorSelected || provinceSelected || subSectorSelected) {
return {
mainProjects
};
} else {
return {
mainProjects: []
};
}
};
export default connect(mapStateToProps)(Map);
In the above code,i try to update my local state values by is setters,but useEffect doesn't seem to work.And it looks like it's only called once,at when the component renders for the first time.How can i solve this problem?
Thank you!!
Your useEffect has a set of dependencies that donˋt match those, which are actually used.
You are setting your local state with elements of mainProjects, so useEffect will only do something when mainProjects changes.
You donˋt seem to be doing anything with your useState-Variables, so you donˋt change state, so react doesnˋt rerender.
Update: it is really important to check, that the dependency-array (2nd argument to useEffect) and the used variables inside the function (1st argument) correspond, else bad things will happen ;-)
There is an eslint-rule for that: https://www.npmjs.com/package/eslint-plugin-react-hooks

Resources