Prevent rechart from overflowing into margin? - recharts

I would like to display the most recent N points, but the chart overflows to the left when set the domain to anything less than chartData.length.
I see that I can shift everything to the left by adding margin={{left:59}} to <ComposedChart>, but that seems a little hacky. Is there a better way to hide this overflowing line?
/*
* NOTE: the axis `domain` requires a _function_ if you want to display less than the total number of
* data points (see: https://stackoverflow.com/questions/53751564/can-i-set-the-recharts-axis-domain-max-lower-than-datamax)
*/
const pointsToDisplay = 8;
const xAxisStart = chartData.length - pointsToDisplay;
return (
<ResponsiveContainer height={210}>
<ComposedChart data={chartData}>
<XAxis dataKey="index" tick={true} type="number" domain={[() => xAxisStart, 'dataMax']} />
<YAxis tick={false} domain={['dataMin - 3', 'dataMax + 3']} />
<Line type="monotone" dataKey="score" strokeWidth={5} dot={false} stroke="var(--accentColor)" />
</ComposedChart>
</ResponsiveContainer>
);

According to the documentation allowDataOverflow has to be set to true for the data to be clipped. You can then also remove the function from the domain array and simply use the variable:
/*
* NOTE: the axis `domain` requires a _function_ if you want to display less than the total number of
* data points (see: https://stackoverflow.com/questions/53751564/can-i-set-the-recharts-axis-domain-max-lower-than-datamax)
*/
const pointsToDisplay = 8;
const xAxisStart = chartData.length - pointsToDisplay;
return (
<ResponsiveContainer height={210}>
<ComposedChart data={chartData}>
<XAxis dataKey="index" tick={true} type="number" domain={[xAxisStart, 'dataMax']} allowDataOverflow={true}/>
<YAxis tick={false} domain={['dataMin - 3', 'dataMax + 3']} />
<Line type="monotone" dataKey="score" strokeWidth={5} dot={false} stroke="var(--accentColor)" />
</ComposedChart>
</ResponsiveContainer>
);

Related

Buildings in three.js

I am working on a web game and using three.js to do the 3d of the game. I am building a city and I want the buildings to look exactly like the picture below.
I want them to be a base material, without textures or colors because I will add colors later depending on the building status (like the game below).
This is what I have right now:
As you can see, I just have a bunch of different-sized boxes, with no details at all. How would I achieve the details seen in the game?
This is my current code:
export default function Map() {
const RenderBuildings = () => {
const buildings = []
for (let i = 0; i < 10; i++) {
buildings.push(<Box color="#dad3cb" width={1} height={1} depth={1} />)
}
return buildings
}
return (
<Canvas>
<CameraController />
<ambientLight intensity={1} />
<Ground color="#b0aa9d" width={40} height={1} depth={40} />
{RenderBuildings()}
</Canvas>
)
}
export const Box = (props: Props) => {
const { color, width, height, depth, ...rest } = props
const mesh = useRef<THREE.Mesh>()
const boxRef = useRef<THREE.Mesh>()
useEffect(() => {
if (!mesh.current) return
const _mesh = mesh.current
_mesh.position.x = Math.floor(Math.random() * 20)
_mesh.position.z = Math.floor(Math.random() * 20)
_mesh.scale.x =
Math.random() * Math.random() * Math.random() * Math.random() * 5 + 3
_mesh.scale.z = _mesh.scale.x
_mesh.scale.y =
Math.random() * Math.random() * Math.random() * _mesh.scale.x * 5 + 3
}, [mesh])
useEffect(() => {
if (!boxRef.current) return
const _boxRef = boxRef.current
_boxRef.applyMatrix4(new Matrix4().makeTranslation(0, 0.5, 0))
}, [boxRef])
return (
<mesh {...rest} ref={mesh}>
<boxGeometry args={[width, height, depth]} ref={boxRef} />
<meshToonMaterial color={color} />
</mesh>
)
}
Your first step should be to look at lighting your scene. This will allow for better depth to your buildings. A hemisphere light may be your best bet: https://threejs.org/examples/#webgl_lights_hemisphere. You can learn more about lights here: https://threejs.org/manual/#en/lights.
Next, take a look at geometry. You want those buildings to have some level of detail at the geometry level. You can use primitives like these: https://threejs.org/manual/#en/primitives or build a model in any modelling software and import it in. At that point, maybe make a few models and then instance them randomly.
Depending on how you do geometry, you will need to pick an appropriate material: https://threejs.org/manual/#en/materials. This should give you plenty of options.
I would also add in a bit of animation to keep things lively: https://threejs.org/examples/#webgl_animation_keyframes
Finally, a bit of fog always helps for ambiance: https://threejs.org/manual/#en/fog
Some more inspiration: https://demos.littleworkshop.fr/infinitown
Ultimately, building a scene like this is going to be a labour of love. There are no easy shortcuts. Keep at it!
Use a texture tile(s) to coordinate the layout (like a QR code but unencrypted). That is, a ~64x64 image with RGB values which correspond to scene logic. This method will be easier to block in features. You can store height in R, type in G, and owner in B. Maybe there is a sidecar file with more raw data (i.e. bills or level), referenced by owner and x/y.
Map generation could be defined by: matrix, random forest, evolutions... with rules for adjacent tiles and minimum areas. Then once you have a satisfactory system, it can be parsed as a Three.js scene. Geometry can be as easy or as hard as you make it. Extrude contiguous pixel groups & refine.
Don't forget to add dust clouds when you synchronize the data to obscure visual artifacts. You can probably do a lot with texture offsets in terms of variety, from a forced perspective.

How to display bars from left to right with a fixed bar gap?

I want to display bars from left to right with a fixed bar gap, rather than display them evenly.
I have tried to add barCategoryGap or barGap prop with fixed number, but no influence for the chart like this:
And my code is:
<BarChart data={data} barSize={10} barCategoryGap={1}>
<YAxis
...some props
/>
<Tooltip />
<Bar dataKey="responseTime">
{data.map((item, index) => (
<Cell fill={item.isPass ? '#3c763d' : '#a94442'} key={index} />
))}
</Bar>
</BarChart>
In your graph, you don't have any XAxis specified to group your bars into categories. Because of this, the props barGap and barCategoryGap won't work, since there is no category to make changes on.
To solve your problem, you should add the missing XAxis which datakey would be a prop in your data object array, that shares the same object where your Bars values come from so that they can be regrouped. Afterwards you can "play" around with the gap between the different bars, just like a demo found on Recharts for Bars.

ChartJS line chart calls plugin each time I hover a point

I am using ChartJS v2.9.3.
I am trying to add to the time line chart a conditional vertical background color that is displayed depending on if dataset's y-value is above a threshold.
The thread Background colour of line charts in chart.js gave me the idea to use the following plugin
const backgroundPlugin = {
beforeDatasetDraw(chart, easing) {
const ctx = chart.chart.ctx;
const chartArea = chart.chartArea;
ctx.save();
const colors = [
'red',
'green',
'yellow',
'blue'
];
const randomNumber = Math.floor(Math.random() * colors.length);
// Set random background color to show how many times this function is called
ctx.fillStyle = colors[randomNumber];
console.log('refreshing background');
chart.getDatasetMeta(0).data.forEach((data, index, all) => {
// Check if value > threshold (here 350)
if (
chart.config.data.datasets[0].data[index].y > 350
) {
const nextPointX =
index === all.length - 1 ?
chartArea.left :
all[index + 1]._model.x;
// Create vertical rectangle of color between current point and next point
ctx.fillRect(
nextPointX,
chartArea.top,
Math.abs(all[index]._model.x - nextPointX),
chartArea.bottom - chartArea.top
);
}
});
ctx.restore();
}
};
The problem I observed is that every time I hover on one point of the line chart, the plugin's beforeDatasetDraw function gets called a huge number of time.
The problem can be seen on this JSFiddle : https://jsfiddle.net/avocado1/hgpukame/88/#
This is a problem for me because in my real case that causes a performance issue (I work with a more than 20.000 points datasets and I would like to have 5 different background colors depending on 5 different thresholds, meaning there could be 5 comparisons for each point of my dataset).
Would anyone have any clue on how to prevent hovering events to cause calls to my plugin ? (Or have any workaround to create a conditional background once and for all ?)

Recharts value at pointer to show in tooltip?

Is it possible in Recharts to show a Horizontal line at the Y location where the user has their mouse over and retrieve that Y value so we can display it on the Tooltip?
https://meridian.a2z.com/components/tooltip/?platform=react-web
I've been trying to do some research into how we could get the Y value on the graph where the mouse is hovering or clicked, but I'm having trouble seeing where we could even pull that out.
Any tips on attributes or components we could use to grab this data? Is it even something we have access to from the library?
To clarify, we're trying to get the value of the Y axis at where the user has their cursor over the graph.
So if the graph looks like this and the user has their mouse at the pink dot location, I would be trying to grab out the value of ~7000 - what the y value would be at that graph location
Edit:
Note about responsiveness:
If you want to make this responsive, just adjust the chartBounds based on the padding/margin you've applied to the chart component and you should be good to go.
If you're trying something more advanced and need the height and width to pass to the chart component for more calculations, the following article should help: https://www.pluralsight.com/tech-blog/getting-size-and-position-of-an-element-in-react/
NOTE: This is a bit of a hack and may not be a perfect solution but it should be enough to get you on the right track
You should be able to use the chartX and chartY fields from onMouseMove. Unfortunately, this is just the pixel value under the cursor but you should be able to translate it into the range you are using for your graph.
Here is an example put together using the SimpleLineChart example recharts has up. This should work if you just want to get the Y value under the user's cursor and can be extended to get the X value as well.
const {LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend} = Recharts;
const data = [
{name: 'Page A', uv: 4000, pv: 2400, amt: 2400},
{name: 'Page B', uv: 3000, pv: 1398, amt: 2210},
{name: 'Page C', uv: 2000, pv: 9800, amt: 2290},
{name: 'Page D', uv: 2780, pv: 3908, amt: 2000},
{name: 'Page E', uv: 1890, pv: 4800, amt: 2181},
{name: 'Page F', uv: 2390, pv: 3800, amt: 2500},
{name: 'Page G', uv: 3490, pv: 4300, amt: 2100},
];
//The pixel bounds for the LineChart, 0,0 is the top left corner
// these were found using the inspector built into the web browser
// these are in pixels but correspond to the values used in your graph
// so 246 is 0 Y on the graph and 5 is 10000 Y on the graph (according to your data)
const chartBoundsY = {min: 246, max: 5}
// The bounds we are using for the chart
const chartMinMaxY = {min: 0, max: 10000}
// Convert the pixel value from the cursor to the scale used in the chart
const remapRange = value => {
let fromAbs = value - chartBoundsY.min
let fromMaxAbs = chartBoundsY.max - chartBoundsY.min
let normal = fromAbs / fromMaxAbs
let toMaxAbs = chartMinMaxY.max - chartMinMaxY.min
let toAbs = toMaxAbs * normal
return Math.ceil(toAbs + chartMinMaxY.min)
}
const SimpleLineChart = React.createClass({
render () {
return (
<LineChart
width={600} height={300} data={data}
margin={{top: 5, right: 30, left: 20, bottom: 5}}
onMouseMove={props => {
// We get the values passed into the onMouseMove event
if(props.isTooltipActive) {
// If the tooltip is active then we display the Y value
// under the mouse using our custom mapping
console.log(remapRange(props.chartY))
}
}}
>
<XAxis dataKey="name"/>
<YAxis/>
<CartesianGrid strokeDasharray="3 3"/>
<Tooltip/>
<Legend />
<Line type="monotone" dataKey="pv" stroke="#8884d8" activeDot={{r: 8}}/>
<Line type="monotone" dataKey="uv" stroke="#82ca9d" />
</LineChart>
)
}
})
ReactDOM.render(
<SimpleLineChart />,
document.getElementById('container')
)
You can open this example in jsfiddle and paste in the code above in the JS editor to try it out for yourself. http://recharts.org/en-US/examples
Here is the documentation for the mouse event for the LineChart: http://recharts.org/en-US/api/LineChart
This can be done with the axis scale option together with d3's invert method.
The following code excerpt should give you an idea.
const domainY = d3.extent(data, d => d[keyY])
const scaleY = d3.scaleLinear().domain(domainY).range([0, 1])
<AreaChart
onMouseDown={(e) => console.log(scaleY.invert(e.chartY))}
...
<YAxis
domain={['auto', 'auto']}
dataKey={keyY}
type="number"
scale={scaleY}
...

Creating a light trail effect by dynamically updating the curve of a TubeBufferGeometry

I have a third person game using react-three-fiber and I want to add a sort of trailing light effect wherever the player moves. The light trail will disappear after a while so I was thinking of having a fixed size array for the points. This was my initial attempt at a solution:
const point = new THREE.Vector3();
const points = new Array(50).fill(null).map(p => new THREE.Vector3());
let index = 0;
const Trail = () => {
const ref = useRef();
const playerBody = useStore(state => state.player); // contains player position
const [path, setPath] = useState(new THREE.CatmullRomCurve3(points));
useFrame(() => { // equivalent to raf
const { x, y, z } = playerBody.position;
point.set(x, y, z);
points[index].copy(point);
index = (index + 1) % 50;
setPath(new THREE.CatmullRomCurve3(points));
if (ref && ref.current) {
ref.current.attributes.position.needsUpdate = true;
ref.current.computeBoundingSphere();
}
});
return (
<mesh>
<tubeBufferGeometry ref={ref} attach="geometry" args={[path, 20, .5, 8, false]} />
<meshBasicMaterial attach="material" color={0xffffff} />
</mesh>
)
}
Basically my thought process was to update the curve on every frame (or every x frames to be more performant) and to use an index to keep track of which position in the array of points to update.
However I get two problems with this:
TubeBufferGeometry doesn't update. Not sure if it's even possible to update the geometry after instantiation.
The pitfall I foresee in using this fixed array / index method is that once I hit the end of the array, I will have to wrap around to index 0. So then the curve interpolation would mess up because I'm assuming it takes the points sequentially. The last point in the array should connect to the first point now but it won't be like that.
To solve #2, I tried something like
points.unshift();
points.push(point.clone);
instead of points[index].copy(point); but I still couldn't get the Tube to update in the first place.
I wanted to see if there's a better solution for this or if this is the right approach for this sort of problem.
If you want to update the path of a TubeBufferGeometry, you also need to update all the vertices and normals, it is like building again the geometry.
Take a look here to understand how it works : https://github.com/mrdoob/three.js/blob/r118/src/geometries/TubeGeometry.js#L135
The important part is the generateSegment() function, and don't forget this part before :
const frames = path.computeFrenetFrames( tubularSegments, closed );
I made an example last year, feel free to use my code : https://codepen.io/soju22/pen/JzzvbR

Resources