This is the result I want to achieve.
But I can't find a way to add margin between the ReferenceLine label and the bar components.
This is my chart component
<BarChart
width={500}
height={300}
data={data}
>
<XAxis hide dataKey="name" />
{
data.map((entry, index) => (
<ReferenceLine key={`cell-${index}`} strokeWidth={0} x={entry.name} label={entry.name} />
))
}
<Bar dataKey="pv" fill="#8884d8">
{
data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.pv <= 0 ? "#FF645C" : "#29DB92"} />
))
}
</Bar>
</BarChart>
You can see the full example on my sandbox.
https://codesandbox.io/s/bar-chart-with-positive-negative-forked-g3kvz5?file=/src/App.tsx:673-1176
Important: This solution is technically not "correct" if you care for the integrity of your data because it modifies the data so every entry does not start at zero, but because in this case we do not want to show an y-axis anyway I think this can be a simple solution:
const transformData = (margin: number) => {
return data.map((item) => {
if (item.pv >= 0) {
return {
name: item.name,
pv: [margin, item.pv + margin]
};
} else {
return {
name: item.name,
pv: [-margin, item.pv - margin]
};
}
});
};
This simple function maps over every entry of the data and depending of the original pv value returns a new object with the new position of the
corresponding entry. The new pv value is an array where the first entry is the new starting position and the second one is the end position/length of the entry. The new starting position is ether the value of margin (When the original pv is positiv) or -margin (When the original pv is negativ). To keep the original length of the entries the second value of the new array is the original pv +/- the margin.
The App component in this example than has to be modified like this:
export default function App() {
const barData = useMemo(() => transformData(1000), []);
return (
<BarChart width={500} height={300} data={barData}>
<XAxis hide dataKey="name" />
{barData.map((entry, index) => (
<ReferenceLine
key={`cell-${index}`}
strokeWidth={0}
x={entry.name}
label={entry.name}
/>
))}
<Bar dataKey="pv" fill="#8884d8">
{barData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.pv[0] <= 0 ? "#FF645C" : "#29DB92"}
/>
))}
</Bar>
</BarChart>
);
}
Note: Since the new pv value of each entry is no longer a simple number but an array we need to change the fill property of each Cell component from entry.pv <= 0 ? "#FF645C" : "#29DB92" to entry.pv[0] <= 0 ? "#FF645C" : "#29DB92". You also can play with the margin value to fit your purpose. I choose 1000 here because I thought it looks nice :)
Link to the full code: https://codesandbox.io/s/bar-chart-with-positive-negative-forked-cu6d7q?file=/src/App.tsx:971-1026
The problem is it's not really just a margin change. A bar chart still uses a coordinate system. If you add space around the origin it becomes really a different type of chart, even though the difference is subtle. You effectively need 2 different Y axes for this, to split the negative and positive values. So it's a bit tricky to come up with a "right way" to approach this.
Rechart just uses 1 axis. Given the current implementation, trying to modify the component to do this anyway will be highly prone to bugs if Rechartjs's implementation changes. Changing your data, as suggested in the other answer, is probably the safest approach at the moment. But it's still not a very nice solution.
You could also create 2 separate bar charts. But it's also not nice and adds a lot of risk for mismatches.
I see you already opened an issue on Github, which seems the right place to discuss this in detail. It's a very sensible looking use case that a component library should at least have some kind of answer to. Even if that answer is "no that's deliberately unsupported". But it may very well also be "sure that's easy to add as an option". Some patience for this answer can save you a lot of time and hassle.
Rechart has some pretty extensive documentation, pretty sure they're happy to fill in gaps like this.
Related
i'm new to react native and i'm trying to do a moving arrow after a variable get a value, I thought about using a switch case and changing the style, but it seemed impossible to change the padding properties, how could i solve this?
Your question needs a lot of information to be answered thoroughly so I will assume you use hooks, I will try to guide you by giving you an example.
The whole principle is to place your arrow in an 'absolute' position and animate it with an animation. Then, all you need to do is to set the value of the variable 'arrowValue', example : setArrowValue(0.3). Your arrow will then places itself at 30% (according to your interpolation) from the left of your container.
Here is a code snippet to show you the right way :
import {StyleSheet, View, Animated} from "react-native";
export default function ArrowMover(props)
{
// This is the value you will set and your arrow will automatically be placed
const [arrowValue, setArrowValue] = React.useState(0); // From 0 to 1 for example
// This is your animation
// It is the value you have to change to make your arrow move
const [animation, setAnimation] = React.useState(new Animated.Value(0));
// This is an interpolation of your animation
// See this as a black box, it allows you to output value (and ranges of values)
// based on the value of your animation
const animateArrowPosition = animation.interpolate({
inputRange: [0, 1],
outputRange: ["0%", "100%"],
});
// This is where everthing happens automatically
React.useEffect(() =>
{
moveArrowTo(arrowValue);
}, [arrowValue]); // Anytime 'arrowValue' changes, execute the 'moveArrowTo' function
const moveArrowTo = (newArrowValue) =>
{
Animated.timing(animation, { toValue: newArrowValue, duration: 500, }).start(() =>
{
// Do something (or nothing) once the animation finished
});
};
return(
<View style={s.container}>
// Your animation interpolation will change along with 'animation' to follow your interpolation
// of the value
<Animated.View style={[s.arrow, {left:animateArrowPosition}]}> // Notice the 'Animated.View'
<View>
// Your arrow (icon, image, etc...)
</View>
</Animated.View>
</View>
);
}
let s = StyleSheet.create(
{
container:
{
// Your container style
},
arrow:
{
// Your arrow style
height:30,
aspectRatio:1,
position:'absolute',
// left:0 (no need for that, your animation is taking care of the 'left' property
},
}
I am trying to set up a scene in react-three-fiber that uses a raycaster to detect if an object intersects it.
My Scene: scene
I have been looking at examples like this example and this other example, of raycasters that use simple three objects but don't utilize separated component jsx ".gltf" meshes or their not in jsx. So I'm not sure how to add my group of meshes to a "raycaster.intersectObject();".
It seems that all you do is set up your camera, scene, and raytracer separately in different variables, but my camera and scene are apart of the Canvas Component.
Question: How do I add raycasting support to my scene? This would obscure the text that is on the opposite side of the sphere.
Thanks!
This is the approach I used. Note that I used useState instead of useRef", since I had problems with the latter
const Template = function basicRayCaster(...args) {
const [ray, setRay] = useState(null);
const [box, setBox] = useState(null);
useEffect(() => {
if (!ray || !box) return;
console.log(ray.ray.direction);
const intersect = ray.intersectObject(box);
console.log(intersect);
}, [box, ray]);
return (
<>
<Box ref={setBox}></Box>
<raycaster
ref={setRay}
ray={[new Vector3(-3, 0, 0), new Vector3(1, 0, 0)]}
></raycaster>
</>
);
};
Try CycleRayCast from Drei lib. It is react-three-fiber-friendly
"This component allows you to cycle through all objects underneath the cursor with optional visual feedback. This can be useful for non-trivial selection, CAD data, housing, everything that has layers. It does this by changing the raycasters filter function and then refreshing the raycaster."
You can retrieve the raycaster from useThree.
Example to modify the raycaster threshold for points onMount:
const { raycaster } = useThree();
useEffect(() => {
if (raycaster.params.Points) {
raycaster.params.Points.threshold = 0.1;
}
}, []);
Once done you are enable to modify its properties or to use it
I was testing my websocket implementation by sending the mouse positions of my connected clients and rendering them with a red <div> box. When I added the "visualizer" to the connected clients the framerate was cut in half for each new client that connected.
I am not sure how to optimize something like this, I've tried minimizing the amount of looping (which helped a bit, but after some time it became as slow as my first approach).
The pointer is updated (debounced to) 60 times a second, what would I need to do to let that pointer update 60 times per second for at least four clients ?
I would like to keep those pointers, even though they are not part of the main application. React is probably not meant for this kind of thing, the question then is what should I have used instead ?
First Aproach
const MousePointer = ({ GameLobby }) => {
console.log(GameLobby);
if (!GameLobby) return null;
return Object.values(GameLobby).map((data, i) => {
const pos = data.currentMousePosition ? data.currentMousePosition : [0, 0];
const backgroundColor = 'red';
return (
<div
key={i}
css={{
backgroundColor,
height: '15px',
width: '15px',
boxSizing: 'border-box',
position: 'absolute',
left: pos[0],
top: pos[1],
}}></div>
);
});
};
One less loop
const MousePointer = ({ GameLobby }) => {
if (!GameLobby) return null;
const {
[Object.keys(GameLobby)[0]]: {
backgroundColor = 'red',
currentMousePosition: pos = [0, 0],
},
} = GameLobby;
return (
<div
key={1}
css={{
backgroundColor,
height: '15px',
width: '15px',
boxSizing: 'border-box',
position: 'absolute',
left: pos[0],
top: pos[1],
}}></div>
);
};
Yeah, rerendering at that rate will cause performance problems. You should try attaching a callback ref to the div and updating only the properties that need to change using setInterval.
In order to check the most updated version of GameLobby without rerendering, you will have to refer it some other way (it can't be a prop). One very easy (and extremely questionable) way to do this is by sticking it on the window object or creating another global. I have also seen people add variables as instance properties of their components.
You might also just choose to handle the cursor ghosts outside of your React tree, right when you receive the GameLobby object. Probably easy enough to just append absolutely positioned divs directly to the DOM.
Note that these patterns shouldn't be generalized for other things you build in React, I would categorize them as "dirty tricks" that you need in rare situations (usually related to animation).
I'm trying to make a multi colored bar chart with uber's react-vis library. The issue I'm having is that the left most bar chart overflows underneath the YAxis instead of being contained to the right of it.
You can check out this REPL
https://codepen.io/anon/pen/mozzeE?editors=0011
function Chart() {
const data = [1,2, 3]
return <XYPlot
// xType="ordinal"
width={300}
height={300}
xDistance={100}
>
<HorizontalGridLines />
{data.map((n, k) => {
const y = data.length+5 - n
return <VerticalBarSeries
className="vertical-bar-series-example"
color={makeHexString()}
data={[
{x: n, y}
]}/>
})}
<YAxis />
</XYPlot>;
}
For who ever will straggle with this:
You need to specify xDomain for XYPlot with like:
xDomain={[min_X_value, max_X_value]}
Where min_X_value, max_X_value - your minimum and maximum values for X
I have a group of graphs visualizing a bunch of data for me (here), based off a csv with approximately 25,000 lines of data, each having 12 parameters. However, doing any interaction (such as selecting a range with the brush on any of the graphs) is slow and unwieldy, completely unlike the dc.js demo found here, which deals with thousands of records as well but maintains smooth animations, or crossfilter's demo here which has 10 times as many records (flights) as I do.
I know the main resource hogs are the two line charts, since they have data points every 15 minutes for about 8 solid months. Removing either of them makes the charts responsive again, but they're the main feature of the visualizations, so is there any way I can make them show less fine-grained data?
The code for the two line graphs specifically is below:
var lineZoomGraph = dc.lineChart("#chart-line-zoom")
.width(1100)
.height(60)
.margins({top: 0, right: 50, bottom: 20, left: 40})
.dimension(dateDim)
.group(tempGroup)
.x(d3.time.scale().domain([minDate,maxDate]));
var tempLineGraph = dc.lineChart("#chart-line-tempPer15Min")
.width(1100).height(240)
.dimension(dateDim)
.group(tempGroup)
.mouseZoomable(true)
.rangeChart(lineZoomGraph)
.brushOn(false)
.x(d3.time.scale().domain([minDate,maxDate]));
Separate but relevant question; how do I modify the y-axis on the line charts? By default they don't encompass the highest and lowest values found in the dataset, which seems odd.
Edit: some code I wrote to try to solve the problem:
var graphWidth = 1100;
var dataPerPixel = data.length / graphWidth;
var tempGroup = dateDim.group().reduceSum(function(d) {
if (d.pointNumber % Math.ceil(dataPerPixel) === 0) {
return d.warmth;
}
});
d.pointNumber is a unique point ID for each data point, cumulative from 0 to 22 thousand ish. Now however the line graph shows up blank. I checked the group's data using tempGroup.all() and now every 21st data point has a temperature value, but all the others have NaN. I haven't succeeded in reducing the group size at all; it's still at 22 thousand or so. I wonder if this is the right approach...
Edit 2: found a different approach. I create the tempGroup normally but then create another group which filters the existing tempGroup even more.
var tempGroup = dateDim.group().reduceSum(function(d) { return d.warmth; });
var filteredTempGroup = {
all: function () {
return tempGroup.top(Infinity).filter( function (d) {
if (d.pointNumber % Math.ceil(dataPerPixel) === 0) return d.value;
} );
}
};
The problem I have here is that d.pointNumber isn't accessible so I can't tell if it's the Nth data point (or a multiple of that). If I assign it to a var it'll just be a fixed value anyway, so I'm not sure how to get around that...
When dealing with performance problems with d3-based charts, the usual culprit is the number of DOM elements, not the size of the data. Notice the crossfilter demo has lots of rows of data, but only a couple hundred bars.
It looks like you might be attempting to plot all the points instead of aggregating them. I guess since you are doing a time series it may be unintuitive to aggregate the points, but consider that your plot can only display 1100 points (the width), so it is pointless to overwork the SVG engine plotting 25,000.
I'd suggest bringing it down to somewhere between 100-1000 bins, e.g. by averaging each day:
var daysDim = data.dimension(function(d) { return d3.time.day(d.time); });
function reduceAddAvg(attr) {
return function(p,v) {
if (_.isLegitNumber(v[attr])) {
++p.count
p.sums += v[attr];
p.averages = (p.count === 0) ? 0 : p.sums/p.count; // gaurd against dividing by zero
}
return p;
};
}
function reduceRemoveAvg(attr) {
return function(p,v) {
if (_.isLegitNumber(v[attr])) {
--p.count
p.sums -= v[attr];
p.averages = (p.count === 0) ? 0 : p.sums/p.count;
}
return p;
};
}
function reduceInitAvg() {
return {count:0, sums:0, averages:0};
}
...
// average a parameter (column) named "param"
var daysGroup = dim.group().reduce(reduceAddAvg('param'), reduceRemoveAvg('param'), reduceInitAvg);
(reusable average reduce functions from the FAQ)
Then specify your xUnits to match, and use elasticY to auto-calculate the y axis:
chart.xUnits(d3.time.days)
.elasticY(true)