stacked barchart with svg (no 3rd party library) - javascript

I am trying to create a stacked barchart with svg and html and not using any 3rd party library. Unfortunately, there is not a single document online which shows how to create a stacked bar chart using plain svg.
I have created a codepen and i am midway to achieving that stacked barchart. Can anyone please let me know what else is needed to make this a stacked barchart.
https://codepen.io/a166617/pen/qBXvzQd
Here is the code that i currently have
const ReleaseScopeCharts = () => {
const data = [
{
name: 'Transit',
passed: 2,
skipped: 5,
failed: 22,
},
{
name: 'Access',
passed: 7,
skipped: 2,
failed: 11,
},
];
const width = 500;
const colors = ['#30D158', '#005EA7', '#FF453A'];
const entries = data.map((d) => ({
name: d.name,
total: ['passed', 'skipped', 'failed'].reduce((acc, key) => acc + d[key], 0),
bars: ['passed', 'skipped', 'failed'].map((key, i) => ({
value: d[key],
color: colors[i],
}))
.filter((bar) => bar.value),
}));
const rows = (entry) => entry.bars.map((bar, index) => {
const height = (bar.value / entry.total) * 100;
return (
<g key={index}>
<rect
width={50}
height={`${height}%`}
fill={bar.color}
x={index * 60} // multiply with the width (50) + 10 for space
/>
</g>
);
});
return (
<div className="new-card">
<div />
{entries.map((entry) => (
<>
{entry.name}
<svg viewBox={`0, 0, ${width}, ${500}`}
height={500}
width={width}
style={{ transform: `rotateX(180deg)` }}
>
{rows(entry)}
</svg>
</>
))}
</div>
);
};
With stacked barchart, i mean showing one over the other.

To stack barchart, you need to calculate the current columns and space widths. Wrap the svg to div, also offset the text in to div and centered with display:flex.
Add the y key to the bars, where:
start point = passed = 0
middle point = skipped = passed value
end point = failed = passed value + skipped value
y: key === 'passed' ? 0 : key === 'skipped' ? d['passed'] : d['skipped'] + d['passed'],
// Basic style
const newCardStyle = {
display: 'flex',
};
const contentStyle = {
display: 'flex',
flexFlow: 'column',
alignItems: 'center',
};
// multiply 50 (width) * 3 (columns) + 10 (space width) * 2 ( space between columns)
const width = 50 * 3 + 10 * 3;
function App() {
const data = [
{
name: 'Transit',
passed: 2,
skipped: 5,
failed: 22,
},
{
name: 'Access',
passed: 7,
skipped: 2,
failed: 11,
},
];
// Basic style
const newCardStyle = {
display: 'flex',
};
const contentStyle = {
display: 'flex',
flexFlow: 'column',
alignItems: 'center',
};
// multiply 50 (width) * 3 (columns) + 10 (space width) * 2 ( space between columns)
const width = 50 * 3 + 10 * 3;
const colors = ['#30D158', '#005EA7', '#FF453A'];
const entries = data.map(d => ({
name: d.name,
total: ['passed', 'skipped', 'failed'].reduce(
(acc, key) => acc + d[key],
0
),
bars: ['passed', 'skipped', 'failed']
.map((key, i) => ({
value: d[key],
color: colors[i],
y:
key === 'passed'
? 0
: key === 'skipped'
? d['passed']
: d['skipped'] + d['passed'],
}))
.filter(bar => bar.value),
}));
const rows = entry => {
return entry.bars.map((bar, index) => {
const height = (bar.value / entry.total) * 100;
const y = (bar.y / entry.total) * 100;
return (
<g key={Math.random()}>
<rect
width={50}
height={`${height}%`}
fill={bar.color}
x={60} // multiply with the width (50) + 10 for space
y={`${y}%`}
/>
</g>
);
});
};
return (
<div className="new-card" style={newCardStyle}>
{entries.map(entry => (
<div style={contentStyle} key={Math.random()}>
<svg
viewBox={`0, 0, ${width}, ${500}`}
height={500}
width={width}
style={{ transform: `rotateX(180deg)` }}
>
{rows(entry)}
</svg>
{entry.name}
</div>
))}
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#17/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js" crossorigin></script>
<div id="root"></div>

Related

How to change the color in the BarChart

Hi i have this BarChart that shows the procent of some dadta. How to change the color of the bar if the procent is < 20 in red for example?
This is My Barchart:
<BarChart
//style={graphStyle}
style={{marginBottom: -20}}
data={this.state.chartData}
width={screenWidth - 40}
height={220}
//yAxisLabel="$"
chartConfig={this.state.chartConfig}
verticalLabelRotation={30}
/>
<View style={{padding: 5, paddingTop: 0}}>
{
this.state.labels.map((obj, i) => {
return (
<Text style={{fontSize: 14, marginBottom: 2.5, color: '#666'}}>{i+1}. {obj} ({this.state.chartData.datasets[0].data[i]} gallons({this.state.procent[i]}%))</Text>
)
})
}
</View>
This is chartConfig:
const chartConfig = {
backgroundGradientFrom: "#fff",
//backgroundGradientFromOpacity: 0,
backgroundGradientTo: "#fff",
//backgroundGradientToOpacity: 0.5,
color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
strokeWidth: 1, // optional, default 3
barPercentage: 0.75,
useShadowColorFromDataset: false // optional
};
Where to add a second color if my procent of data is lover than 20?
This is the picture how it looks.
And if someone knows i am doing a formula to get the procent for the barchart and the Barchart expects data ti fill the vertical row with numbers how can i hardCode it so the number is always 100 at the top of the y?
Since version 6.7.0 you can use the withCustomBarColorFromData prop as introduced with this PR.
This works as follows:
Set the withCustomBarColorFromData prop to true
Provide a colors array within your data object. There is bijection between the indices of the colors array and the data indices. You need to provide a function that maps the opacity to a color string.
In order to have a custom color if the data point is less then 20, you could manage a separate colors state containing all the necessary functions.
Here is minimal example.
const init = [19, 18, 25, 20, 16];
const MyBarChart = () => {
const [colors, setcolors] = useState(init.map(item => (opacity = 1) => `rgba(0, 0, 0, ${opacity})`))
const [data, setData] = useState(init)
React.useEffect(() => {
setcolors(data.map(item => item < 20 ? (opacity = 1) => `rgba(255, 0, 0, ${opacity})` : (opacity = 1) => `rgba(0, 0, 0, ${opacity})`))
}, [data])
const dataset = {
labels: ["january", "february", "march", "april", "may"],
datasets: [
{
data: data,
colors: colors
}
]
}
return (
<>
<BarChart
data={dataset}
width={300}
height={220}
withCustomBarColorFromData={true}
flatColor={true}
chartConfig={{
backgroundColor: '#ffffff',
backgroundGradientFrom: '#ffffff',
backgroundGradientTo: '#ffffff',
data: dataset.datasets,
color: (opacity = 1) => '#fff',
labelColor: () => '#6a6a6a',
}}
/>
</>
);
};
The result is as follows.

React Native Wheel of Fortune

I am developing the react native application which contains the wheel of fortune. There are two wheels in my application. Wheel#1 is running smoothly but in Wheel#2 is being required to add Inner & Outer wheels. I am using D3 shape and React Native SVG packages to achieve my goal.
import React, {Component} from 'react';
import {
View,
StyleSheet,
Dimensions,
Animated,
TouchableOpacity,
Image,
} from 'react-native';
import * as d3Shape from 'd3-shape';
import Svg, {G, Text, TSpan, Path, Pattern} from 'react-native-svg';
const AnimatedSvg = Animated.createAnimatedComponent(Svg);
const {width, height} = Dimensions.get('screen');
class WheelOfFortune extends Component {
constructor(props) {
super(props);
this.state = {
enabled: false,
started: false,
finished: false,
winner: null,
gameScreen: new Animated.Value(width - 40),
wheelOpacity: new Animated.Value(1),
imageLeft: new Animated.Value(width / 2 - 30),
imageTop: new Animated.Value(height / 2 - 70),
};
this.angle = 0;
this.prepareWheel();
}
prepareWheel = () => {
this.Rewards = this.props.options.rewards;
this.RewardCount = this.Rewards.length;
this.numberOfSegments = this.RewardCount;
this.fontSize = 20;
this.oneTurn = 360;
this.angleBySegment = this.oneTurn / this.numberOfSegments;
this.angleOffset = this.angleBySegment / 2;
this.winner = this.props.options.winner
? this.props.options.winner
: Math.floor(Math.random() * this.numberOfSegments);
this._wheelPaths = this.makeWheel();
this._angle = new Animated.Value(0);
this.props.options.onRef(this);
};
resetWheelState = () => {
this.setState({
enabled: false,
started: false,
finished: false,
winner: null,
gameScreen: new Animated.Value(width - 40),
wheelOpacity: new Animated.Value(1),
imageLeft: new Animated.Value(width / 2 - 30),
imageTop: new Animated.Value(height / 2 - 70),
});
};
_tryAgain = () => {
this.prepareWheel();
this.resetWheelState();
this.angleListener();
this._onPress();
};
angleListener = () => {
this._angle.addListener(event => {
if (this.state.enabled) {
this.setState({
enabled: false,
finished: false,
});
}
this.angle = event.value;
});
};
componentWillUnmount() {
this.props.options.onRef(undefined);
}
componentDidMount() {
this.angleListener();
}
makeWheel = () => {
const data = Array.from({length: this.numberOfSegments}).fill(1);
const arcs = d3Shape.pie()(data);
var colors = this.props.options.colors
? this.props.options.colors
: [
'#E07026',
'#E8C22E',
'#ABC937',
'#4F991D',
'#22AFD3',
'#5858D0',
'#7B48C8',
'#D843B9',
'#E23B80',
'#D82B2B',
];
return arcs.map((arc, index) => {
const instance = d3Shape
.arc()
.padAngle(0.01)
.outerRadius(width / 2)
.innerRadius(this.props.options.innerRadius || 100);
return {
path: instance(arc),
color: colors[index % colors.length],
value: this.Rewards[index],
centroid: instance.centroid(arc),
};
});
};
_getWinnerIndex = () => {
const deg = Math.abs(Math.round(this.angle % this.oneTurn));
// wheel turning counterclockwise
if (this.angle < 0) {
return Math.floor(deg / this.angleBySegment);
}
// wheel turning clockwise
return (
(this.numberOfSegments - Math.floor(deg / this.angleBySegment)) %
this.numberOfSegments
);
};
_onPress = () => {
const duration = this.props.options.duration || 10000;
this.setState({
started: true,
});
Animated.timing(this._angle, {
toValue:
365 -
this.winner * (this.oneTurn / this.numberOfSegments) +
360 * (duration / 1000),
duration: duration,
useNativeDriver: true,
}).start(() => {
const winnerIndex = this._getWinnerIndex();
this.setState({
finished: true,
winner: this._wheelPaths[winnerIndex].value,
});
this.props.getWinner(this._wheelPaths[winnerIndex].value, winnerIndex);
});
};
_textRender = (x, y, number, i) => (
<Text
x={x - number.length * 5}
y={y - 80}
fill={
this.props.options.textColor ? this.props.options.textColor : '#fff'
}
textAnchor="middle"
fontSize={this.fontSize}>
{Array.from({length: number.length}).map((_, j) => {
// Render reward text vertically
if (this.props.options.textAngle === 'vertical') {
return (
<TSpan x={x} dy={this.fontSize} key={`arc-${i}-slice-${j}`}>
{number.charAt(j)}
</TSpan>
);
}
// Render reward text horizontally
else {
return (
<TSpan
y={y - 40}
dx={this.fontSize * 0.07}
key={`arc-${i}-slice-${j}`}>
{number.charAt(j)}
</TSpan>
);
}
})}
</Text>
);
_renderSvgWheel = () => {
return (
<View style={styles.container}>
{this._renderKnob()}
<Animated.View
style={{
alignItems: 'center',
justifyContent: 'center',
transform: [
{
rotate: this._angle.interpolate({
inputRange: [-this.oneTurn, 0, this.oneTurn],
outputRange: [
`-${this.oneTurn}deg`,
`0deg`,
`${this.oneTurn}deg`,
],
}),
},
],
backgroundColor: this.props.options.backgroundColor
? this.props.options.backgroundColor
: '#fff',
width: width - 20,
height: width - 20,
borderRadius: (width - 20) / 2,
borderWidth: this.props.options.borderWidth
? this.props.options.borderWidth
: 2,
borderColor: this.props.options.borderColor
? this.props.options.borderColor
: '#fff',
opacity: this.state.wheelOpacity,
}}>
<AnimatedSvg
width={this.state.gameScreen}
height={this.state.gameScreen}
viewBox={`0 0 ${width} ${width}`}
style={{
transform: [{rotate: `-${this.angleOffset}deg`}],
margin: 10,
}}>
<G y={width / 2} x={width / 2}>
{this._wheelPaths.map((arc, i) => {
const [x, y] = arc.centroid;
const number = arc.value.toString();
return (
<G key={`arc-${i}`}>
<Path d={arc.path} strokeWidth={2} fill={arc.color} />
<G
rotation={
(i * this.oneTurn) / this.numberOfSegments +
this.angleOffset
}
origin={`${x}, ${y}`}>
{this._textRender(x, y, number, i)}
</G>
</G>
);
})}
</G>
</AnimatedSvg>
</Animated.View>
</View>
);
};
_renderKnob = () => {
const knobSize = this.props.options.knobSize
? this.props.options.knobSize
: 20;
// [0, this.numberOfSegments]
const YOLO = Animated.modulo(
Animated.divide(
Animated.modulo(
Animated.subtract(this._angle, this.angleOffset),
this.oneTurn,
),
new Animated.Value(this.angleBySegment),
),
1,
);
return (
<Animated.View
style={{
width: knobSize,
height: knobSize * 2,
justifyContent: 'flex-end',
zIndex: 1,
opacity: this.state.wheelOpacity,
transform: [
{
rotate: YOLO.interpolate({
inputRange: [-1, -0.5, -0.0001, 0.0001, 0.5, 1],
outputRange: [
'0deg',
'0deg',
'35deg',
'-35deg',
'0deg',
'0deg',
],
}),
},
],
}}>
<Svg
width={knobSize}
height={(knobSize * 100) / 57}
viewBox={`0 0 57 100`}
style={{
transform: [{translateY: 8}],
}}>
<Image
source={
this.props.options.knobSource
? this.props.options.knobSource
: require('../assets/images/knob.png')
}
style={{ width: knobSize, height: (knobSize * 100) / 57 }}
/>
</Svg>
</Animated.View>
);
};
_renderTopToPlay() {
if (this.state.started == false) {
return (
<TouchableOpacity onPress={() => this._onPress()}>
{this.props.options.playButton()}
</TouchableOpacity>
);
}
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity
style={{
position: 'absolute',
width: width,
height: height / 2,
justifyContent: 'center',
alignItems: 'center',
}}>
<Animated.View style={[styles.content, {padding: 10}]}>
{this._renderSvgWheel()}
</Animated.View>
</TouchableOpacity>
{this.props.options.playButton ? this._renderTopToPlay() : null}
</View>
);
}
}
export default WheelOfFortune;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
content: {},
startText: {
fontSize: 50,
color: '#fff',
fontWeight: 'bold',
textShadowColor: 'rgba(0, 0, 0, 0.4)',
textShadowOffset: {width: -1, height: 1},
textShadowRadius: 10,
},
});
End Result Look like this
Now I want to add an Inner wheel to it Like this

onLayoutChange is resetting my data from local storage

whenever my layout changes, it will save the new changes into localstorage and update my layout state
onLayoutChange(layouts) {
saveToLS("layouts", layouts);
}
However, the issue occurs When the page is refreshed. layout was updated before it was fetched from local storage, this means it discard all the changes and reset the layout to default. The image below shows what i mean
What should I do to prevent this from happening?? I am following this guide. It is working on the guide but not for me, really appreciate any help
Here is the full code I am using
const originalLayouts = getFromLS("layouts") || {};
const ResponsiveReactGridLayout = WidthProvider(Responsive);
class MinMaxLayout extends React.PureComponent {
static defaultProps = {
margin:[0,0],
className:"layout",
cols: { lg: 1, md: 10 },
rowHeight: 100
};
constructor(props) {
super(props);
this.state = {
layouts: JSON.parse(JSON.stringify(originalLayouts)),
inputWidthVal: "",
inputHeightVal:"",
items: [0, 1, 2, 3, 4].map(function (i, key, list) {
return {
id: uuidV4(),
i: i.toString(),
x: i * 2,
y: 0,
w: 1,
h: 1
};
}),
charts: [0].map(function (i, key, list) {
return {
id: uuidV4(),
i: i.toString(),
x: i,
y: 0,
w: 4,
h: 3
};
}),
newCounter: 0,
chartCounter:0
};
this.onAddItem = this.onAddItem.bind(this);
this.onChartItem = this.onChartItem.bind(this);
this.onBreakpointChange = this.onBreakpointChange.bind(this);
}
createElement(el) {
const removeStyle = {
position: "absolute",
left: "11px",
top: 0,
cursor: "pointer"
};
const i = el.i;
return (
<div key={el.id} data-grid={el} >
<span
className="remove"
style={removeStyle}
onClick={this.onRemoveItem.bind(this, i)}
>
x
</span>
</div>
);
}
// this is the charts that was created when the DOM was render
createChart(el) {
const removeStyle = {
position: "absolute",
left: "11px",
top: 0,
cursor: "pointer"
};
const i = el.i;
return (
// <div key={el.id} data-grid={el} onClick={((e) => this.handleClick(e))}>
<div key={el.id} data-grid={el} className="graph">
<Newvsresturnvisitors />
<span
className="remove"
style={removeStyle}
onClick={this.onRemoveChartItem.bind(this, i)}
>
x
</span>
</div>
);
}
onAddItem() {
this.setState({
// Add a new item. It must have a unique key!
items: this.state.items.concat({
id: uuidV4(),
i: "n" + this.state.newCounter,
x: (this.state.items.length * 2) % (this.state.cols || 12),
y: Infinity, // puts it at the bottom
w: 1,
h: 1
}),
// Increment the counter to ensure key is always unique.
newCounter: this.state.newCounter + 1
});
}
onChartItem() {
this.setState({
// Add a new item. It must have a unique key!
charts: this.state.charts.concat({
id: uuidV4(),
i: "n" + this.state.chartCounter,
x: (this.state.charts.length * 2) % (this.state.cols || 12),
y: 0, // puts it at the bottom
w: 4,
h: 3
}),
// Increment the counter to ensure key is always unique.
chartCounter: this.state.chartCounter + 1
});
}
// We're using the cols coming back from this to calculate where to add new items.
onBreakpointChange(breakpoint, cols) {
this.setState({
breakpoint: breakpoint,
cols: cols
});
}
onRemoveItem(i) {
this.setState({ items: _.reject(this.state.items, { i: i }) });
}
clearAllItem = ()=>{
this.setState({ items: [], charts: [] });
}
onRemoveChartItem(i){
this.setState({ charts: _.reject(this.state.charts, { i: i }) });
}
inputCanvasDimension = e =>{
const re = /^[0-9\b]+$/;
if (e.target.value === '' || re.test(e.target.value)) {
if(e.currentTarget.className === "inputWidth"){
this.setState({inputWidthVal: e.currentTarget.value});
}else if(e.currentTarget.className === "inputHeight"){
this.setState({inputHeightVal: e.currentTarget.value});
}
}
}
setCanvasDimension = () =>{
var canvas = document.getElementById("DetailLocationContainer");
if((this.state.inputWidthVal < 1000) || (this.state.inputHeightVal < 3000)){
alert("Width should not be less than 1000 and height should not be less than 3000");
}else {
canvas.style.width= this.state.inputWidthVal + "px";
canvas.style.height= this.state.inputHeightVal + "px";
}
}
saveToLS(key, value) {
var secondObject = Object.entries(value)[1];
if (global.localStorage) {
global.localStorage.setItem(
"rgl-8",
JSON.stringify({
[key]: value
})
);
}
}
onLayoutChange(layout, layouts) {
this.saveToLS("layouts", layouts);
}
onDrop = (layout, layoutItem, _event) => {
if(_event.dataTransfer.mozSourceNode.className === "textwidget"){
this.setState({
items: this.state.items.concat({
id: uuidV4(),
i: "n" + this.state.newCounter,
x: layoutItem.x,
y: layoutItem.y,
w: 1,
h: 1
}),
newCounter: this.state.newCounter + 1
});
}else if(_event.dataTransfer.mozSourceNode.className === "chart"){
this.setState({
// Add a new item. It must have a unique key!
charts: this.state.charts.concat({
id: uuidV4(),
i: "n" + this.state.chartCounter,
x: (this.state.charts.length * 2) % (this.state.cols || 12),
y: Infinity, // puts it at the bottom
w: 5,
h: 6
}),
// Increment the counter to ensure key is always unique.
chartCounter: this.state.chartCounter + 1
});
}
};
reset = () =>{
// window.location.reload(true);
this.setState({ layouts: {} });
}
render() {
return (
<div className="container" id="container">
<div className="btn_container">
<div>
<span>Width </span>
<input className="inputWidth" value={this.state.inputWidthVal} onChange={this.inputCanvasDimension}/>
<span> Height </span>
<input className="inputHeight" value={this.state.inputHeightVal} onChange={this.inputCanvasDimension}/>
<button onClick={this.setCanvasDimension}>Apply</button>
<button onClick={this.reset}><CachedIcon style={{fontSize: '14px'}}/></button>
<button onClick={openFullscreen}><FullscreenSharpIcon style={{fontSize: '14px'}}/></button>
</div>
</div>
<button id="createItemBtn" onClick={this.onAddItem}>Add Item</button>
<button id="createChartBtn" onClick={this.onChartItem}>Add Chart</button>
<button onClick={this.clearAllItem}>Clear All</button>
<div className="widgetcontainer">
<WidgetThumbnail
className="chart"
/>
<WidgetThumbnail
className="textwidget"
/>
</div>
<div className='DetailLocationContainer' id="DetailLocationContainer" >
<ResponsiveReactGridLayout
cols={{ lg: 1, md: 10, sm: 6, xs: 4, xxs: 2 }}
rowHeight={100}
onDrop={this.onDrop}
isDroppable={true}
isBounded={true}
layouts={this.state.layouts}
onLayoutChange={(layout, layouts) =>
this.onLayoutChange(layout, layouts)
}
onBreakpointChange={this.onBreakpointChange}
{...this.props}
>
{_.map(this.state.items, (el) => this.createElement(el))}
{_.map(this.state.charts, (el) => this.createChart(el))}
</ResponsiveReactGridLayout>
</div>
</div>
);
}
}
export default MinMaxLayout;
const rootElement = document.getElementById("root");
ReactDOM.render(<MinMaxLayout />, rootElement);
This is my getFrinLS
export function getFromLS(key) {
let md = {};
if (global.localStorage) {
try {
md = JSON.parse(global.localStorage.getItem("rgl-8")) || {};
} catch (e) {
/*Ignore*/
}
// console.log("return value ", md[key]);
return md[key];
}
}
Looks like incremental keys are needed to apply updated layout. Change the key value from el.id to i
createElement(el) {
const removeStyle = {
position: "absolute",
left: "11px",
top: 0,
cursor: "pointer"
};
const i = el.i;
return (
<div key={i} data-grid={el} >
<span
className="remove"
style={removeStyle}
onClick={this.onRemoveItem.bind(this, i)}
>
x
</span>
</div>
);
}

How to create a charjs bar graph with many bars?

I want to get the effect of something like this:
And here is my best attempt as a combo graph:
The problem is I need to vastly ramp up the number of bars in this chart. However, when I try to do this the bars disappear.
Here is my code as a typescript reactjs setup:
import './App.css';
import React from 'react';
import { Bar, Line } from 'react-chartjs-2';
const createRandomFollowersData = () => {
const maxDate = new Date();
const minDate = new Date(maxDate.valueOf() - 5 * 365 * 24 * 60 * 60 * 1000);
const dataPoints = Array.from({ length: 500 }).map(() => ({
timestamp: new Date(
Math.floor(Math.random() * (maxDate.valueOf() - minDate.valueOf())) +
minDate.valueOf()
).toISOString(),
followers: Math.floor(Math.random() * 1000000) + 0,
}));
return dataPoints.sort(
(a, b) => new Date(a.timestamp).valueOf() - new Date(b.timestamp).valueOf()
);
};
const createRandomAssetData = () => {
const maxDate = new Date();
const minDate = new Date(maxDate.valueOf() - 5 * 365 * 24 * 60 * 60 * 1000);
const dataPoints = Array.from({ length: 500 }).map(() => ({
timestamp: new Date(
Math.floor(Math.random() * (maxDate.valueOf() - minDate.valueOf())) +
minDate.valueOf()
).toISOString(),
price: Math.floor(Math.random() * 45) + 1,
}));
return dataPoints.sort(
(a, b) => new Date(a.timestamp).valueOf() - new Date(b.timestamp).valueOf()
);
};
const followersData = createRandomFollowersData();
const yAxisFollowers = {
type: 'linear',
id: 'followers',
};
const yAxisDelta = {
type: 'linear',
position: 'right',
id: 'change',
};
const yAxisRank = {
type: 'linear',
id: 'rank',
ticks: {
reverse: true,
},
};
const yAxisAssets = {
type: 'linear',
position: 'right',
id: 'assets',
};
const selectChartAxes = (
containsFollowers: boolean,
containsRank: boolean,
showDelta: boolean,
showAssets: boolean
) => {
const yAxes = [];
if (containsFollowers) yAxes.push(yAxisFollowers);
if (containsRank) yAxes.push(yAxisRank);
if (showDelta) yAxes.push(yAxisDelta);
if (showAssets) yAxes.push(yAxisAssets);
return yAxes;
};
const decimateChart = (
data: {
t: Date;
y: number;
}[],
numBuckets: number,
startDate?: Date,
endDate?: Date
) => {
if (!startDate) {
startDate = data[0].t;
}
if (!endDate) {
endDate = data[data.length - 1].t;
}
// create evenly spaced dates
const dt = endDate.valueOf() - startDate.valueOf();
const startValue = startDate.valueOf();
const spacedDates = Array.from({ length: numBuckets + 1 }).map((_, idx) => {
return new Date(startValue + (idx * dt) / numBuckets);
});
// make buckets
const buckets = Array.from({ length: numBuckets + 2 }).map(() => []) as {
t: Date;
y: number;
}[][];
const filteredData = data.filter(
(e) => e.t >= spacedDates[0] && e.t <= spacedDates[spacedDates.length - 1]
);
// place data into buckets
let jdx = 0;
spacedDates.forEach((e, idx) => {
for (; jdx < filteredData.length; ) {
const e = filteredData[jdx];
const date = new Date(e.t);
if (date >= spacedDates[idx] && date <= spacedDates[idx + 1]) {
buckets[idx].push({
t: date,
y: e.y,
});
++jdx;
} else break;
}
});
// one plot per bucket
return buckets.map((bucket, idx) => {
const date = spacedDates[idx];
if (bucket.length === 0) {
return {
t: date,
y: NaN,
};
}
return bucket[bucket.length - 1];
});
};
const chartMappedFollowersData = followersData.map((followerData) => ({
t: new Date(followerData.timestamp),
y: followerData.followers,
}));
// const decimatedData = decimateChart(chartMappedFollowersData, 75);
const decimatedData = decimateChart(chartMappedFollowersData, 75).map(
(e, idx) => {
if (idx > 1 && idx < 10) {
return {
t: e.t,
y: NaN,
};
}
if (idx > 30 && idx < 45) {
return {
t: e.t,
y: NaN,
};
}
return e;
}
);
const decimatedDataToBars = (
data: {
t: Date;
y: number;
}[]
) => {
if (data.length < 2) {
return {
t: data[0].t,
y: data[0].y,
};
}
const bars = [];
const indexedData = data.map((e, idx) => ({
...e,
idx,
}));
const filteredIndexedData = indexedData.filter((e) => !isNaN(e.y));
for (let idx = 0; idx < filteredIndexedData.length - 1; ++idx) {
const dt = data[idx + 1].t.valueOf() - data[idx].t.valueOf();
for (
let idy = 0;
idy < filteredIndexedData[idx + 1].idx - filteredIndexedData[idx].idx;
++idy
) {
const t = new Date(filteredIndexedData[idx].t.valueOf() + idy * dt);
const deltaY =
(filteredIndexedData[idx + 1].y - filteredIndexedData[idx].y) /
(filteredIndexedData[idx + 1].idx - filteredIndexedData[idx].idx);
bars.push({
t,
y: deltaY,
});
}
}
return bars;
};
const chartOptionsLinear = {
scales: {
yAxes: selectChartAxes(true, false, true, true),
xAxes: [
{
type: 'time',
time: {
unit: 'day',
displayFormats: { day: 'MMM DD, Y' },
min: chartMappedFollowersData[0].t,
max: chartMappedFollowersData[chartMappedFollowersData.length - 1].t,
},
ticks: {
source: 'labels',
},
},
],
maintainAspectRatio: false,
},
};
const chartData = {
labels: decimatedData.map((e) => e.t).filter((_, idx) => idx % 3 === 0),
datasets: [
{
yAxisID: 'followers',
cubicInterpolationMode: 'monotone',
backgroundColor: 'rgb(54, 162, 235)',
borderColor: 'rgb(88, 88, 88)',
fill: false,
type: 'line',
label: 'followers',
spanGaps: true,
data: decimatedData,
},
{
yAxisID: 'change',
type: 'bar',
backgroundColor: 'rgb(235, 54, 162)',
label: 'delta',
data: decimatedDataToBars(decimatedData),
barThickness: 1,
},
],
};
function App(): JSX.Element {
return (
<div style={{ margin: '1em' }}>
<Bar data={chartData} options={chartOptionsLinear} />
</div>
);
}
export default App;
If you swap out data: decimatedDataToBars(decimatedData), to data: decimatedDataToBars(chartMappedFollowersData), you can see the effect; The bars disappear. Does anyone have any insight into this problem and how I can fix it?
So the issue was a bug in 2.8.0 that caused the bars to not show. Upgrading to 2.9.4 fixed the issue for me (but broke some other functionality of why I was using 2.8.0 in the first place.)

React Spring - useTrail delay issues

Currently been using react-spring to do our animations in our app; unfortunately, animation is not something I excel in and the design our designer gave us for our new logo is leaving me stumped on implementation. It is pretty easy with plain old JS but implementing it in react-spring has proved a challenge that I can not get past.
The end goal for the animation is to look like this:
https://codepen.io/darylginn/pen/GRqZxBZ
Currently, I am up to this stage:
import React from "react";
import { useTrail, animated } from "react-spring";
const Loader: React.FC = () => {
// Animations
const paths = [
{
id: 1,
color: "#466FB5",
d: "M90.6672 33H16L53.3336 96.4409L90.6672 33Z",
},
{
id: 2,
color: "#0093D3",
d: "M53.3347 96.4443H128.002L90.6683 33.0034L53.3347 96.4443Z",
},
{
id: 3,
color: "#53BFA2",
d: "M128.001 96.3701H53.3336L90.6672 159.811L128.001 96.3701Z",
},
{
id: 4,
color: "#93C83F",
d: "M90.6675 159.892H165.335L128.001 96.4417L90.6675 159.892Z",
},
{
id: 5,
color: "#58B647",
d: "M165.334 159.892H90.6664L128 223.333L165.334 159.892Z",
},
{
id: 6,
color: "#F2E90B",
d: "M202.667 96.4436H128L165.334 159.894L202.667 96.4436Z",
},
{
id: 7,
color: "#FBB12C",
d: "M128.001 96.4443H202.668L165.335 33.0034L128.001 96.4443Z",
},
{
id: 8,
color: "#FF5E8D",
d: "M240 33H165.333L202.666 96.4409L240 33Z",
},
];
const trail = useTrail(paths.length, {
from: {
scale: 1,
},
to: async (next: any) => {
while (1) {
await next({ scale: 1.5 });
await next({ scale: 1 });
}
},
});
return (
<div style={{ width: "200px", height: "200px" }}>
<svg
width="72"
height="72"
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ overflow: "visible" }}>
{trail.map(({ scale }, index) => {
const path = paths[index];
return (
<animated.path
key={path.id}
fill={path.color}
d={path.d}
style={{
transformOrigin: "center",
transform: scale.interpolate((s: any) => `scale(${s})`),
}}
/>
);
})}
</svg>
</div>
);
};
The main issue I am at now is the scale of each triangle in the SVG needs to happen one after another, but nothing I do with the useTrail make this happen.
I did try adding a delay like this to the useTrail
delay: (key: any) => key * 200
But the delay doesn't even seem to make a difference. If someone could help make sure each triangle finish it sequences before the next one starts that would be great.
Bonus if you can also help me add the colour change as seen in the original design (see link).
I have tried posting in the react-spring community but got no replies
I would change the useTrail to one useSpring for all the triangles. If you change the x value from 0 to 8, then you can use interpolate and a range for each triangle to change. For example for the second triangle you can use range:[0,1,1.5,2,8],output:[1,1,1.5,1,1]. It means that when x is between 1 and 2 it will change the scale from 1 to 1.5 to 1 and all other places it will remain 1.
const props = useSpring({
from: {
x: 0
},
config: {
duration: 4000
},
to: async (next: any) => {
while (1) {
await next({ x: 8 });
await next({ reset: true });
}
}
});
I also added the color interpolation.
{paths.map((path, index) => {
const colors = [];
for (let i = 0; i < 8; i++) {
colors.push(paths[(i + index) % 8].color);
}
return (
<animated.path
key={path.id}
fill={path.color}
d={path.d}
style={{
transformOrigin: "center",
transform: props.x
.interpolate({
range: [0, index, index + 0.5, index + 1, 8],
output: [1, 1, 1.5, 1, 1]
})
.interpolate((x) => `scale(${x})`),
fill: props.x.interpolate({
range: [0, 1, 2, 3, 4, 5, 6, 7, 8],
output: colors
})
}}
/>
);
})}
I have an example. There is some glitch with the center triangle. But I think you get the idea.
Example: https://codesandbox.io/s/animate-triangles-sequentially-with-interpolating-and-range-oer84

Categories

Resources