Is there a way to modify an individual column in a HexagonLayer? - javascript

So I created a flat HexagonLayer and have various interactions tied to the onClick prop as shown below.
My goal now is to modify the individual column that was clicked by adding an outline, changing the color for that column, or something else along those lines. I could probably come up with a brute force resolution by adding another layer beneath this one, but the documentation doesn't go into much detail on this and I wanted to see if there was possibly another route to take that would modify the existing layer.
I would also like to obtain info such as the number of points within a given column, but I would imagine that's related to the first part.
Here's what I currently have:
// Here's where I'm creating the settings for the HexagonLayer
const hexagonLayer = {
id: 'hexagon-layer',
data: props.G.cells,
pickable: true,
extruded: true,
radius: 50,
elevationScale: 0,
autoHighlight: true,
opacity: 0.3,
getPosition: d => d.COORDINATES,
// Here's the onClick interactions
onClick: (layer, $event) => {
// I can obtain some information from the layer variable here,
// but not nearly enough for what I'm trying to accomplish
switch (props.ctx.phase) {
case 'setup':
props.moves.placeUnit(layer.object.position)
break
case 'play':
// Replace with active unit functionality
const activeUnit = Object.keys(props.G.players[props.ctx.currentPlayer].pieces)[0]
props.moves.moveUnit(activeUnit, layer.index, layer.object.position)
break
default:
console.error('Unknown phase', props.ctx.phase)
}
return true
}
};
// Check some incoming data to add the ducks shown in the screenshot
// Not necessarily related to the question, but they were in the screenshot so I included it
useEffect(() => {
const tempPieces = []
props.G.players.forEach(player => {
Object.values(player.pieces).forEach(piece => {
tempPieces.push(
createScenegraphLayer(
piece.id,
UNIT_MODEL_MAP[piece.type],
piece.coordinates, 30
)
)
})
})
setPieces(tempPieces)
}, [props.G.cells, props.G.players])
// Rendering the layers
return (
<div className="Map">
<DeckGL
initialViewState={INITIAL_VIEW_STATE}
controller={true}
debug={true}>
<ReactMapGL mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN}>
<Layer {...threeDLayer} />
</ReactMapGL>
<HexagonLayer {...hexagonLayer} />
{pieces.map(piece => (
<ScenegraphLayer key={piece.id} {...piece} coordinates={piece.data[0].coordinates} />
))}
</DeckGL>
<ToastContainer />
</div>
);

My goal now is to modify the individual column that was clicked by adding an outline, changing the color for that column, or something else along those lines.
To change the color of the clicked column you could utilize highlightColor and highlightedObjectIndex in the following way:
const App = () => {
const [highlightedObjectIndex, setHighlightedObjectIndex] = useState(-1);
const onClick = info => {
if (info.object) {
setHighlightedObjectIndex(info.object.index);
}
};
const layers = [
new HexagonLayer({
id: "hexagon-layer",
data,
pickable: true,
radius: 200,
getPosition: d => d.COORDINATES,
highlightColor: [0, 0, 255],
highlightedObjectIndex,
updateTriggers: {
highlightedObjectIndex
},
onClick
})
];
return (
<DeckGL
initialViewState={INITIAL_VIEW_STATE}
controller={true}
layers={layers}
>
</DeckGL>
);
};

Related

React Leaflet with LayersControl handling object datas

I'd like to have a map using OpenStreetMap showing location of flowers. My point is to have LayersControl with colours and type and be able to check for example Orange and Tulip and see only orange tulips on the map but it seems hard from what i read on React LeafLet documentation.
To be easier to understand i will add some code :
Flower datas example:
const flowers = [
{
"type": "Tulip",
"colour": "Orange",
"latlng": [52.081222, 5.235965],
},
{
"type": "Crocus",
"colour": "Red",
"latlng": [52.081421, 5.235534],
},
]
LeafletMap.jsx (Partial):
const LeafletMap: React.FC = () => {
return (
<MapContainer id="mapId"
center={averagePos}
zoom={zoom}>
{flowerItems.map((flower, index) => (
//Maybe do something here like sorting and creating
//multiple layers
//or having already a layer for every type of search
//but if i want to add more colours ou type it can be very hard to update
))}
</LayersControl>
</MapContainer>
)
}
You could maintain the selected layers using state variables - one array to track the type (Crocus or Tulip) and another to track the colour (red or orange). Initially, the states are setup to show everything:
const [type, setType] = useState(['Tulip', 'Crocus']);
const [colour, setColour] = useState(['Orange', 'Red']);
For each type and colour you could create a LayersControl where you hook into the add and remove event handlers and update the corresponding state (type or colour) when the layer is checked or unchecked by the user:
<LayersControl.Overlay name="Crocus" checked="true">
<LayerGroup
eventHandlers={{
add: (e) => {
updateTypeState('Crocus', true);
},
remove: (e) => {
updateTypeState('Crocus', false);
},
}}
></LayerGroup>
</LayersControl.Overlay>
The functions to update the state look like this (there is one function for updating type and one for updating colour):
const updateTypeState = (key: string, value: boolean) => {
setType((prevState) => {
if (value) {
prevState = [...prevState, key];
} else {
prevState = prevState.filter((e) => e !== key);
}
console.log(prevState);
return prevState;
});
};
const updateColourState = (key: string, value: boolean) => {
setColour((prevState) => {
if (value) {
prevState = [...prevState, key];
} else {
prevState = prevState.filter((e) => e !== key);
}
return prevState;
});
};
You could then render the markers using the map function as you suggested:
{flowers
.filter((flower) => {
if (type.length == 0 && colour.length == 0) {
// No layers selected, so show all
return true;
}
if (type.length > 0 && colour.length > 0) {
// Colours and types selected
return (
type.indexOf(flower.type) >= 0 &&
colour.indexOf(flower.colour) >= 0
);
}
if (type.length > 0) {
// Type selected, no colour selected
return type.indexOf(flower.type) >= 0;
}
// Colour selected, no type selected
return colour.indexOf(flower.colour) >= 0;
})
.map((flower, index) => (
<Marker position={flower.latlng}>
<Popup>
{flower.type}, {flower.colour}
</Popup>
</Marker>
))}
There's a working StackBlitz here, but please note the marker images do not display properly due to this issue, but you should be able to see a broken image icon and click on it to view details of the flower.

sort multiple arrays of different objects by one shared field (integer) in ReactJS?

I have an object (Page) with multiple different nested objects (Question, Image, Text, Video, etc.) that all have the same property (number) served up as JSON from Ruby on Rails
render :json => #page, :include => [:videos, :images, :rtfs, questions: { :include => :answers }]
How can I display all this content sorted by the number property? I can easily map through page.images, page.videos, etc. and display those in the correct respective order as they are ordered by number field in RoR already, but that only gets me so far.
Let’s say the page has 2 questions (with numbers 2 and 4), 1 video (number 3), 1 image (number 1) and no rtf’s. For now I am simply mapping through videos first and rendering them, then images, then questions, then rtf’s. But what I need is to do in this particular case is to have the image displayed first (since it’s number property is 1), then one of the questions (with number property 2), then video (since it’s number 3) and finally, the other question.
I feel like I need to create separate arrays for all the nested object types (videos, images, questions, etc.), then merge them (using a splat operator, most likely) into a single array called page_data, sort it by number field and then render it. As I am still quite new to coding, I am having a hard time figuring it out and would appreciate any help!
Here are the relevant parts of the React Component
class LessonCreatorPage extends Component {constructor(props) {
super(props);
this.state = {
page_preview: {}, //this is the main object that has different types of nested objects
}}
showPage(id){
//returns the response from backend and passes it to page_preview object
}
};
render(){
{this.state.page_preview && (
React.Fragment>
the snippet below is repeated for this.state.page_preview.images, questions, rtf's. They all are displayed in a different manner
{this.state.page_preview.videos && (
<React.Fragment>
{this.state.page_preview.videos.map((video) =>{
const video_id = `${video.vimeo_id}`;
return (
<React.Fragment key={video.id}>
<Label pointing="below">{video.title}</Label>
<Embed id={video_id} source="vimeo" />
<Label pointing>{video.description}</Label>
</React.Fragment>
);
})}
</React.Fragment>
)}
</React.Fragment>
)}
I feel like I need to create separate arrays for all the nested object types (videos, images, questions, etc.), then merge them (using a splat operator, most likely) into a single array called page_data, sort it by number field and then render it.
You are on the correct path. There is no reason to do this on the server though. You are requesting JSON and you have the question tagged with reactjs so I'll assume the data will be delivered there.
You can combine the different items on the client, sort them based on number, then display them. No need to change server code. The snippet below is an example of how you could display the data in the desired manner.
In JavaScript you can combine 2 arrays using the spread ... operator (similar to the splat operator in Ruby).
const a = [1, 2];
const b = [3, 4];
[...a, ...b]; //=> [1, 2, 3, 4]
You can then use .sort() to sort the combined array in the desired order.
<script type="text/babel">
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = { page: null, pageData: null };
}
async componentDidMount() {
// You might want to wrap the code in a try/catch block to handle any errors
// that might occur (like no network connection).
const page = await fetchRailsPage();
// Concatenate the different items into a single array, but add a new type
// property first to differentiate the different types of items later.
const pageData = [
...page.videos.map((video) => ({ ...video, type: "video" })),
...page.images.map((image) => ({ ...image, type: "image" })),
...page.rtfs.map((rtf) => ({ ...rtf, type: "rtf" })),
...page.questions.map((question) => ({ ...question, type: "question" })),
].sort((a, b) => a.number - b.number);
this.setState({ page, pageData });
}
render() {
if (!this.state.page) return <>loading...</>;
return (
<>
<h1>{this.state.page.title}</h1>
{this.state.pageData.map((item) => (
<p key={JSON.stringify([item.type, item.id])}>
{item.number} - {item.title}
</p>
))}
</>
);
}
}
// mock fetching and parsing the JSON data
async function fetchRailsPage() {
await sleep(2000);
return {
id: 1,
title: "page title",
videos: [
{ id: 1, number: 3, title: "video title" },
],
images: [
{ id: 1, number: 1, title: "image title" },
],
rtfs: [],
questions: [
{ id: 1, number: 2, title: "question title #1", answers: [] },
{ id: 2, number: 4, title: "question title #2", answers: [] },
],
};
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
ReactDOM.render(<Demo />, document.querySelector("#demo"));
</script>
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="demo"></div>
In the above snippet .map((video) => ({ ...video, type: "video" })) is optional, but adds a type property to each item. This can be omitted, but might be useful since you might want to render a "video" object different than a "question" object.
An example of the added type property usage is changing the map() callback to something like (you could also use a switch statement):
{this.state.pageData.map((item) => {
if (item.type == "video") {
const video = item;
const video_id = `${video.vimeo_id}`;
return (
<React.Fragment key={video.id}>
<Label pointing="below">{video.title}</Label>
<Embed id={video_id} source="vimeo" />
<Label pointing>{video.description}</Label>
</React.Fragment>
);
}
if (item.type == "image") {
// ...
}
})}
Another alternative would be to pass a render component or function instead of the type.
// in your class component
renderVideo(video) {
const video_id = `${video.vimeo_id}`;
return (
<React.Fragment key={video.id}>
<Label pointing="below">{video.title}</Label>
<Embed id={video_id} source="vimeo" />
<Label pointing>{video.description}</Label>
</React.Fragment>
);
}
Then instead of the type, pass the name of the render method:
...page.videos.map((video) => ({ ...video, render: "renderVideo" })),
And finally, update the map() callback in your render method:
{this.state.pageData.map((item) => this[item.render](item)}

Search filter won't accept "Round" for one value because "Ground" is a value in every result

I am working on a photo gallery for swimming pools. The search bar updates state per character entered, similar to the Netflix search bar. It updates on all keys in the JSON file, similar to how you can search for either "Good Will Hunting" as a title or "Matt Damon" as an actor on Netflix.
The issue I am running into is that "round" will be a common search term, triggering "shape":"round" in the JSON. But another category is "type": "above-ground" or "type":"in-ground".
As a result, if you search for a round pool, the query doesn't filter to show only round pools because "round" is also triggering the word "ground". It shows a mish-mash of round pools and any pool that triggers "above-ground" or "in-ground".
I have tried messing around with the .filter and .includes methods to no avail. I'm not sure if I'm missing something there or what.
I'm also about to look into regular expressions to maybe dictate "if the search term "round" does not have a preceding character, show round pools and ignore the ground trigger".
import React, { Component } from "react";
class App extends Component {
state = {
filter: "",
data: [
{
shape: "Round",
type: "Above-ground"
},
{
shape: "Rectangle",
type: "In-ground"
},
{
shape: "Kidney",
type: "In-ground"
}
]
};
handleChange = event => {
this.setState({
filter: event.target.value
});
};
render() {
const { filter, data } = this.state;
const lowerCasedFilter = filter.toLowerCase();
const filteredData = data.filter(item => {
return Object.keys(item).some(key =>
item[key].toLowerCase().includes(lowerCasedFilter)
);
});
return (
<div>
<input value={filter} onChange={this.handleChange} />
{filteredData.map(item => (
<div>
<p>Shape: {item.shape}</p>
<p>Type: {item.type}</p>
<hr />
</div>
))}
</div>
);
}
}
export default App;
I expect for the search filter to only show "round" pools when the user enters "round", and not show excess results due to the word "ground"
You could utilize a dynamic RegExp that uses word boundaries \b for "complete word" matches.
const lowerCasedFilter = filter.toLowerCase();
const filteredData = data.filter(item => {
return Object.keys(item).some(key => {
const stringToSearch = item[key].toLowerCase();
const regex = new RegExp("\\b"+ lowerCasedFilter +"\\b"); // build regex around the given search term ("round" in your case)
return regex.test(stringToSearch); // use "test" to see if your term is present in the property value
});
});
Relevant information in this post: whole word match in javascript.
find if one word starts like lowerCasedFilter in any field of data objects
item[key].toLowerCase().split(' ').some(word => word.slice(0, lowerCasedFilter.length) === lowerCasedFilter)
const data = [
{
shape: "Round",
type: "Above-ground"
},
{
shape: "Rectangle",
type: "In-ground"
},
{
shape: "Kidney",
type: "In-ground"
}
]
filteredData = (lowerCasedFilter) => data.filter(item => {
return Object.keys(item).some(key =>
item[key].toLowerCase().split(' ').some(word => word.slice(0, lowerCasedFilter.length) === lowerCasedFilter)
);
});
console.log( filteredData('round') )
If it's specific to the five letters "round", you could just use
const filteredData = lowerCasedFilter === "round" ?
data.filter(item => item.shape === "Round") :
data.filter(item => {
return Object.keys(item).some(key =>
item[key].toLowerCase().includes(lowerCasedFilter)
);
});

Dynamically update Highcharts chart in react

I am using highcharts-react-official in react-redux to create a drilldown chart.
However when I click on a bar to drilldown, I also update some props which causes the component to re-render - which seems to prevent the drilldown event.
I kind of gathered from Change series data dynamically in react-highcharts without re-render of the chart that I should use shouldComponentUpdate and getChart() to prevent re-render and instead dynamically update the data.
My issue is that getChart() doesn't seem to work for the official highcharts react package. I'm getting Uncaught TypeError: this.refs.chart.getChart is not a function
Is there an alternative I'm meant to be using to get and dynamically update the chart? Or some examples that I could look at?
Just including render and shouldComponentUpdate parts here:
shouldComponentUpdate(nextProps, nextState) {
let chart = this.refs.chart.getChart();
//dynamically update data using nextProps
return false;
}
render () {
const options = {
chart: {
type: 'column',
height: 300,
events: {
drillup: (e) => {this.drilledUp(e)}
}
},
plotOptions: {
series: {
events:{
click: (e) => {this.categoryClicked(e)}
}
}
},
xAxis: {type: "category"},
yAxis: {title: {text: 'Amount Spent ($)'}},
series: [{
name: 'weekly spending',
showInLegend: false,
data: this.props.transactionChartData.series_data,
cursor: 'pointer',
events: {
click: (e)=> {this.weekSelected(e)}
}
}],
drilldown: {
series: this.props.transactionChartData.drilldown_data
}
};
return (
<HighchartsReact
highcharts={Highcharts}
options={options}
ref="chart"
/>
)
}
In highcharts-react-official v2.0.0 has been added allowChartUpdate option, which should work great in your case. By using this option you can block updating the chart with updating the component:
categoryClicked() {
this.allowChartUpdate = false;
this.setState({
...
});
}
...
<HighchartsReact
ref={"chartComponent"}
allowChartUpdate={this.allowChartUpdate}
highcharts={Highcharts}
options={...}
/>
Moreover, to get the chart instance use refs:
componentDidMount(){
const chart = this.refs.chartComponent.chart;
}
Live demo: https://codesandbox.io/s/98nl4pp5r4
// react native functional
import React, {useState, useRef,useLayoutEffect} from "react"
import HighchartsReactNative from '#highcharts/highcharts-react-native'
function chartcomponent(props){
const [options, setoptions] = useState({});
const chartRef = useRef(null);
useEffect(() => {
// create the options for your chart.
setoptions({chart:{}, yaxis:{}, xAxis:{},})// etc.
}, []);
useLayoutEffect(() => {
// new point to add, you can get new data via props, fetch, socket, etc.
var x = new Date(hr.timestamp).getTime();
var y = 10
// these are the important parts here:
var series = chartRef.current.props.options.series;
if (!Array.isArray(series)){return;}
var seriesdata = series[0].data;
seriesdata.push([x,y]);
// next line limits points in chart to 10, so new poitn replaces first point
if (seriesdata.length>10){seriesdata.splice(0,1);}
return () => {};
}, [props]);
return(
<View style={styles.charting}><HighchartsReactNative styles={{height:300, width:600}} options={options} ref={chartRef} ></HighchartsReactNative></View>
)
}

Vis.js/React/JavaScript: Render Vis.timeline

I am in the middle of trying to solve a problem with vis.js timeline I hope to get some guidance from you folks. console.log is showing data but the browser shows a blank screen. Unfortunately I am all out of ideas on what else to try it to make it work.
I have the following code. I have tried different ways to make it work but so far no luck. Any help will be greatly appreciated.
// Config for the Timeline as JSON
const options = {
width: '100%',
height: '60px',
stack: false,
showMajorLabels: true,
showCurrentTime: true,
zoomMin: 1000000,
type: 'background',
format: {
minorLabels: {
minute: 'h:mma',
hour: 'ha'
}
}
}
class ScaleTime extends Component{
constructor({data=[]}) {
super({data})
this.state = {data, id:''}
//console.log('ScaleTime Data:', data)
}
render(){
const { data } = this.state
const newAction = data.action.map((actionItem, index) => ({
...actionItem,
id: index + 1
}));
const items = {
...data,
action: newAction
};
const timeLineData = new vis.DataSet([{items}])
console.log('timeLineData:', timeLineData)
var container = document.getElementById('timeline');
return(
<div className="timeline">
<Timeline
items={items.action}
options={options}
container={container}
/>;
</div>
)
}
}//component
Update:
After adding id now I need to change the 'timestamp' property to start. The error message I am now getting is: Property "start" missing in item 1.
you need to make sure that items has content before calling Timeline. You can do:
if (!items) return <SpinLoader />; return <Timeline items={items.action} options={options} container={container} />;

Categories