Related
This question already has an answer here:
React won't draw anything in the canvas in useEffect
(1 answer)
Closed 16 days ago.
I use Matter js for physics and canvas for rendering 100 items, after every change in props the app gets slower and slower. At the same time, subsequent downloads with the same props occur instantly, i.e. past images and elements remained and were not deleted, which loads the application.
enter image description here
"Top" dropdown menu changes props for bubbles component, code:
const Bubbles = ({ coins }) => {
var winwidth = window.innerWidth;
var winheight = window.innerHeight - 80;
const [physics, setPhysics] = useState({
frictionAir: 0,
friction: 0.2,
frictionStatic: 0.2,
restitution: 0.1,
inertia: Infinity,
density: 0.8,
});
useEffect(() => {
setSize(coins);
// create an engine
const engine = Engine.create();
const world = engine.world;
// create a renderer
const render = Render.create({
element: document.querySelector(".bubbles"),
engine: engine,
options: {
width: winwidth,
height: winheight,
showAngleIndicator: false,
background: "transparent",
wireframeBackground: "transparent",
wireframes: false,
showDebug: false,
},
});
const bubbles = [];
// create a canvas
const canvas = document.createElement("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.querySelector(".bubbles").appendChild(canvas);
const ctx = canvas.getContext("2d");
// create 100 random bubbles
for (let i = 0; i < coins.length; i++) {
const x = Math.random() * window.innerWidth;
const y = Math.random() * window.innerHeight;
const bubble = Bodies.circle(x, y, coins[i].size / 2, {
...physics,
render: {
fillStyle: "transparent",
},
});
Composite.add(engine.world, bubble);
// store the bubble and its content data in an object
bubbles.push({
bubble,
content: {
radius: coins[i].radius,
text: `${i + 1}`,
},
});
}
// update the canvas on each engine update
Matter.Events.on(engine, "afterUpdate", function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < bubbles.length; i++) {
const bubble = coins[i];
const pos = bubbles[i].bubble.position;
const size = bubble.size;
const text = bubble.symbol;
const priceChange = bubble.price_change_percentage_24h.toFixed(1);
const image = new Image();
image.src = bubble.image;
if (priceChange > 0) {
ctx.fillStyle = "rgba(5, 222, 5, 0.15)";
ctx.strokeStyle = "rgba(5, 222, 5, 0.6)";
} else if (priceChange < 0) {
ctx.fillStyle = "rgba(166, 53, 53, 0.15)";
ctx.strokeStyle = "rgba(166, 53, 53, 0.8)";
} else {
ctx.fillStyle = "rgba(255, 255, 89, 0.069)";
ctx.strokeStyle = "rgba(187, 231, 11, 0.443)";
}
ctx.beginPath();
ctx.arc(pos.x, pos.y, size / 2, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
ctx.drawImage(
image,
pos.x - size / 5,
pos.y - size / 2.5,
size / 2.5,
size / 2.5
);
ctx.font = `${size / 6}px Acme`;
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, pos.x, pos.y + size / 6.5);
ctx.font = `${size / 6.5}px Acme`;
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(priceChange + "%", pos.x, pos.y + size / 3);
}
});
// fit the render viewport to the scene
window.addEventListener("onload", () => {
Render.lookAt(render, {
min: { x: 0, y: 0 },
max: { x: 100000, y: 10000 },
});
});
// run the engine
Runner.run(engine);
// run the renderer
Render.run(render);
return () => {
// stop the engine and renderer
Engine.clear(engine);
Render.stop(render);
Runner.stop(engine);
// remove the canvas from the DOM
canvas.remove();
};
}, [coins]);
return <></>;
};
export default Bubbles;
after each update of the coins props, I deleted the canvas and stopped the engine, but still the speed worsened
return () => {
// stop the engine and renderer
Engine.clear(engine);
Render.stop(render);
Runner.stop(engine);
// remove the canvas from the DOM
canvas.remove();
};
I want to either stop re-creating the engine and the matter js world, canvas with images.
Only without window.location.reload(), because I need to save props like "Top" and "Earth, Trampoline", etc.
I solved the problem. I didn't properly clear the js canvas value and also didn't set events.off when unmounting the component.
Here is the solution to my problem
return () => {
//set Matter Js events off. drawCanvas - afterUpdate event function
Events.off(engine, 'afterUpdate', drawCanvas);
Render.stop(render);
Composite.clear(engine.world);
Runner.stop(engine)
Engine.clear(engine);
render.canvas.remove();
render.canvas = null;
render.context = null;
// remove the canvas from the DOM
canvas.remove();
};
I'm working with React and ChartJS to draw a doughnut chart with a 3*Pi/2 circumference
and rounded corner.
I saw these two posts where they explain how to round corners for data sets and it is working as expected with a complete circle and with half a circle:
ChartJs - Round borders on a doughnut chart with multiple datasets
Chartjs doughnut chart rounded corners for half doghnut
One answer on this post is to change "y" or "x" translation by factor of n, for example 2 in the following case: ctx.translate(arc.round.x, arc.round.y*2);
With this in mind I started to change values for x and y but have not yet reach the correct set of values that will make it work.
For example I tried to use a factor of 3/2 on the translation of y and this is what I get.
ctx.translate(arc.round.x, (arc.round.y * 3) / 2);
with no factor I get the following:
ctx.translate(arc.round.x, arc.round.y);
The code to round the end corner is exactly the same as in the posts I refer. But here it is just in case:
let roundedEnd = {
// #ts-ignore
afterUpdate: function (chart) {
var a = chart.config.data.datasets.length - 1;
for (let i in chart.config.data.datasets) {
for (
var j = chart.config.data.datasets[i].data.length - 1;
j >= 0;
--j
) {
if (Number(j) == chart.config.data.datasets[i].data.length - 1)
continue;
var arc = chart.getDatasetMeta(i).data[j];
arc.round = {
x: (chart.chartArea.left + chart.chartArea.right) / 2,
y: (chart.chartArea.top + chart.chartArea.bottom) / 2,
radius:
chart.innerRadius +
chart.radiusLength / 2 +
a * chart.radiusLength,
thickness: (chart.radiusLength / 2 - 1) * 2.5,
backgroundColor: arc._model.backgroundColor,
};
}
a--;
}
},
// #ts-ignore
afterDraw: function (chart) {
var ctx = chart.chart.ctx;
for (let i in chart.config.data.datasets) {
for (
var j = chart.config.data.datasets[i].data.length - 1;
j >= 0;
--j
) {
if (Number(j) == chart.config.data.datasets[i].data.length - 1)
continue;
var arc = chart.getDatasetMeta(i).data[j];
var startAngle = Math.PI / 2 - arc._view.startAngle;
var endAngle = Math.PI / 2 - arc._view.endAngle;
ctx.save();
ctx.translate(arc.round.x, arc.round.y);
console.log(arc.round.startAngle);
ctx.fillStyle = arc.round.backgroundColor;
ctx.beginPath();
//ctx.arc(arc.round.radius * Math.sin(startAngle), arc.round.radius * Math.cos(startAngle), arc.round.thickness, 0, 2 * Math.PI);
ctx.arc(
arc.round.radius * Math.sin(endAngle),
arc.round.radius * Math.cos(endAngle),
arc.round.thickness,
0,
2 * Math.PI
);
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
}, };
These are the options to configure the chart:
const chartJsOptions = useMemo<chartjs.ChartOptions>(() => {
if (data) {
return {
elements: {
center: {
text: `${data.impact > 0 ? "%"}`,
color: isDarkTheme ? darkText : greyAxis, // Default is #000000
fontStyle: "Open Sans Hebrew, sans-serif",
sidePadding: 20, // Default is 20 (as a percentage)
minFontSize: 15, // Default is 20 (in px), set to false and text will not wrap.
lineHeight: 20, // Default is 25 (in px), used for when text wraps
},
},
legend: {
display: false,
},
// rotation: Math.PI / 2,
rotation: (3 * Math.PI) / 4,
circumference: (3 * Math.PI) / 2,
maintainAspectRatio: false,
animation: {
duration: ANIMATION_DURATION,
},
plugins: {
datalabels: false,
labels: false,
},
cutoutPercentage: 90,
tooltips: {
enabled: false,
rtl: true,
},
};
} else {
return {};
} }, [data, isDarkTheme]);
Here is where I call the react component for the chart:
<Doughnut
data={chartJsData}
options={chartJsOptions}
plugins={[roundedEnd]} />
How can I correctly calculate the rounded edges on a 3*Pi/2 circumference or any other circumference between complete and half?
This issue may be more of a math than programing and my geometrical math is also a bit rusty.
I'd like to animate/change colour based on the current section of an oscillator wave, to create a pulsing colour effect that matches the wave? The waves can be any of the types e.g. sine, triangle etc. so this would create different pulsing depending on the wave type and the frequency can be changed for the oscillator too (which I would then like to also change the pulsing timings).
Presumably I need to get the amplitude and wavelength (are these even the right terms for what i need?) from the oscillator object and use it in correlation with the Draw object while using some css animation but I am a bit stuck as to where to start? Specifically around the tone.js part of how I would get the values needed and how I would morph from colour A to B and back etc over time. Would I need to include an external library like p5 or can I do this all through tone.js with draw alone?
cheers
david
EDIT - #paulwheeler - Thanks so much for all your comments, that is a big help. To be clearer, and to answer your queries, here is what I have and need, should you have any more input -
i) I have two oscillators continuously playing from Tone.js. They are 'playing' at frequency A and B, to create a binaural sound. The difference between the two i will call the range. So for example, Osc1 might be 100hz, osc2 might be 104hz and the range is 4hz. These oscillators can be of any type allowed by tone.js (sawtooth, sine, triangle..)
ii) When the sound is playing i want to take the range freqency and attach it to two colours. So at the peak of the frequency colour A will be showing as a background colour and at the trough of the frequency colour B will be showing. Inbetween those times the colour will be morphing between the two i.e. at any time t the colour will be a representation of the Y position on the wave, that is reflective of the distance from 0. This will then make it appear that the colour is changing to match the shape of the waves. kind of like here in css (but just using keyframes alone in this example and just to give an example of what i mean visually) -
https://jsfiddle.net/CF3Np/4/
#keyframes animation {
0% {background-color:red;}
50.0% {background-color:green;}
100.0% {background-color:red;}
}
From a cursory scan of the tone.js documentation it looks like it has everything you could want to synthesize and analyze sounds. However it doesn't appear have any facility for graphics. As far as drawing graphics goes, p5.js is certainly one option. However, the reference to "css animation" is a bit out of place here, because libraries like p5.js aren't really designed with css styling in mind. Instead you would be doing all of the animation yourself with calls to drawing functions for each frame.
The thing you are looking for when it comes to analyzing sounds in order to process them as data, and potentially visualize them, is a Fast Fourier Transform. This is an algorithm that decomposes a sample of a sound wave into separate sine wave components, and you can use the amplitudes of those sine waves to measure the sound in terms of component frequencies. Both p5.js (via the p5.sound add-in) and tone.js have FFT classes. If you're already using tone.js you'll probably want to stick with that for analyzing sounds, but you can certainly create visualizations for them using p5.js. Just as a conceptual example, here's a pure p5.js example:
const notes = [{
name: 'c',
freq: 261.6
},
{
name: 'c#',
freq: 277.2,
sharp: true
},
{
name: 'd',
freq: 293.7
},
{
name: 'd#',
freq: 311.1,
sharp: true
},
{
name: 'e',
freq: 329.6
},
{
name: 'f',
freq: 349.2
},
{
name: 'f#',
freq: 370.0,
sharp: true
},
{
name: 'g',
freq: 392.0
},
{
name: 'g#',
freq: 415.3,
sharp: true
},
{
name: 'a',
freq: 440.0
},
{
name: 'a#',
freq: 466.2,
sharp: true
},
{
name: 'b',
freq: 493.9
},
];
let playing = {};
let fft;
let bg;
function setup() {
createCanvas(windowWidth, windowHeight);
colorMode(HSB);
bg = color('lightgray');
for (let note of notes) {
note.osc = new p5.Oscillator();
note.osc.freq(note.freq);
note.osc.amp(0);
}
fft = new p5.FFT();
}
function toggleNote(name) {
let note = notes.filter(n => n.name === name)[0];
if (playing[name] === undefined) {
// First play
note.osc.start();
}
if (playing[name] = !playing[name]) {
// fade in a little
note.osc.amp(0.2, 0.2);
} else {
// fade out a little
note.osc.amp(0, 0.4);
}
}
function playNote(name) {
let note = notes.filter(n => n.name === name)[0];
if (playing[name] === undefined) {
// First play
note.osc.start();
}
playing[name] = true;
note.osc.amp(0.2, 0.2);
}
function releaseNote(name) {
let note = notes.filter(n => n.name === name)[0];
playing[name] = false;
note.osc.amp(0, 0.4);
}
function draw() {
background(bg);
let w = width / 3;
let h = min(height, w * 0.8);
drawSpectrumGraph(w, 0, w, h);
drawWaveformGraph(w * 2, 0, w, h);
drawKeyboard();
bg = color((fft.getCentroid() * 1.379) % 360, 30, 50);
}
function drawSpectrumGraph(left, top, w, h) {
let spectrum = fft.analyze();
stroke('limegreen');
fill('darkgreen');
strokeWeight(1);
beginShape();
vertex(left, top + h);
for (let i = 0; i < spectrum.length; i++) {
vertex(
left + map(log(i), 0, log(spectrum.length), 0, w),
top + map(spectrum[i], 0, 255, h, 0)
);
}
vertex(left + w, top + h);
endShape(CLOSE);
}
function drawWaveformGraph(left, top, w, h) {
let waveform = fft.waveform();
stroke('limegreen');
noFill();
strokeWeight(1);
beginShape();
for (let i = 0; i < waveform.length; i++) {
let x = map(i * 5, 0, waveform.length, 0, w);
let y = map(waveform[i], -1, 2, h / 10 * 8, 0);
vertex(left + x, top + y);
}
endShape();
}
function drawKeyboard() {
let w = width / 3;
let h = min(height, w * 0.8);
let x = 1;
let keyWidth = (w - 8) / 7;
let sharpWidth = keyWidth * 0.8;
noStroke();
let sharpKeys = [];
for (let note of notes) {
fill(playing[note.name] ? 'beige' : 'ivory');
if (note.sharp) {
sharpKeys.push({
fill: playing[note.name] ? 'black' : 'dimgray',
rect: [x - sharpWidth / 2, 0, sharpWidth, h / 2, 0, 0, 4, 4]
});
} else {
rect(x, 0, keyWidth, h - 1, 0, 0, 4, 4);
x += keyWidth + 1;
}
}
for (let key of sharpKeys) {
fill(key.fill);
rect(...key.rect);
}
}
let keymap = {
'z': 'c',
's': 'c#',
'x': 'd',
'd': 'd#',
'c': 'e',
'v': 'f',
'g': 'f#',
'b': 'g',
'h': 'g#',
'n': 'a',
'j': 'a#',
'm': 'b',
}
function keyPressed(e) {
let note = keymap[e.key];
if (note) {
playNote(note);
}
}
function keyReleased(e) {
let note = keymap[e.key];
if (note) {
releaseNote(note);
}
}
function mouseClicked() {
if (mouseX < width / 3) {
let w = width / 3;
let h = w * 0.8;
let x = 1;
let keyWidth = (w - 8) / 7;
let sharpWidth = keyWidth * 0.8;
let naturalKeys = [];
let sharpKeys = [];
for (let note of notes) {
if (note.sharp) {
sharpKeys.push({
name: note.name,
bounds: {
left: x - sharpWidth / 2,
top: 0,
right: x - sharpWidth / 2 + sharpWidth,
bottom: h / 2
}
});
} else {
naturalKeys.push({
name: note.name,
bounds: {
left: x,
top: 0,
right: x + keyWidth,
bottom: h - 1
}
});
x += keyWidth + 1;
}
}
for (let {
bounds,
name
} of sharpKeys.concat(naturalKeys)) {
if (mouseX > bounds.left && mouseX < bounds.right &&
mouseY > bounds.top && mouseY < bounds.bottom) {
toggleNote(name);
break;
}
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/addons/p5.sound.min.js"></script>
I've created a half doughnut chart and wanted to show like a speedometer, but couldn't able to make it. I have gone through this link I want to display my chart Either as similar to the above link or as per this snippet
Here is my code:
import React, { Component } from "react";
import { Doughnut } from "react-chartjs-2";
import Data from "./Data.json";
const data = {
// labels: ["Red", "Green", "Yellow"],
datasets: [
{
data: [500, 500, 500],
backgroundColor: ["red", "#FFCE56", "lightgreen"],
hoverBackgroundColor: ["red", "#FFCE56", "lightgreen"]
}
],
options: {
rotation: 1 * Math.PI,
circumference: 1 * Math.PI,
legend: {
display: false
},
tooltip: {
enabled: false
},
cutoutPercentage: 70
}
};
export default class DonutChart extends Component {
render() {
return (
<div>
<h2>Sample</h2>
<Doughnut height="100px" data={data} options={data.options} />
</div>
);
}
}
Here is the sample code
Can anyone help me in this query to achieve in this format?
Base on the code sample from Konstantin S., you could register a plugin globally inside the componentDidMount() method and implement an afterDraw function as follows:
componentDidMount() {
Chart.pluginService.register({
afterDraw: chart => {
var needleValue = chart.chart.config.data.datasets[0].needleValue;
var dataTotal = chart.chart.config.data.datasets[0].data.reduce((a, b) => a + b, 0);
var angle = Math.PI + (1 / dataTotal * needleValue * Math.PI);
var ctx = chart.chart.ctx;
var cw = chart.chart.canvas.offsetWidth;
var ch = chart.chart.canvas.offsetHeight;
var cx = cw / 2;
var cy = ch - 6;
ctx.translate(cx, cy);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(0, -3);
ctx.lineTo(ch - 10, 0);
ctx.lineTo(0, 3);
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fill();
ctx.rotate(-angle);
ctx.translate(-cx, -cy);
ctx.beginPath();
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
ctx.fill();
}
});
}
This expects the property needleValue to be defined inside the dataset.
datasets: [{
data: [500, 500, 500],
needleValue: 580,
...
}],
Please have a look at your amended CodeSandbox
I got a scenario where in I need to display value for every stack in stacked multi-bar chart - nvd3 graph as we can display value in discrete value - nvd3 graph.
I understand, 'showvalue' is used in discrete bar controller, can we use showvalue in stacked graph, if not please suggest with an alternative solution.
Thanks in advance
Currently isn't possible. That option is available only in the discrete bar chart.
From the maintainer:
We don't have this ability. Stacked/Grouped charts also have complex animations making this a tricky thing to solve. We use tooltips instead.
Source https://github.com/novus/nvd3/issues/150
If you want to do this you need to make your own implementation using d3. There is a good example here http://plnkr.co/edit/BNpAlFalKz0zkkSszHh1?p=preview. It's using the angular wrapper but it's a good starting point.
var app = angular.module('app', ['nvd3ChartDirectives']);
app.controller('chartCtrl', function($scope, $timeout) {
var ANIMATION_TIME = 1500,
countSeriesDisplayed = 2,
promise,
labels = ["label1", "label2", "label3", "label4", "label5"];
$scope.isStacked = false;
// Example data
$scope.chartData = [{
"key": "Series 1",
"values": [
[0, 10],
[1, 20],
[2, 30],
[3, 40],
[4, 50]
]
}, {
"key": "Series 2",
"values": [
[0, 10],
[1, 40],
[2, 60],
[3, 20],
[4, 40]
]
}];
/* To add labels, images, or other nodes on the created SVG, we need to wait
* for the chart to be rendered with a callback.
* Once the chart is rendered, a timeout is set to wait for the animation to
* finish.
*
* Then, we need to find the position of the labels and set it with the
* transform attribute in SVG.
* To do so, we have to get the width and height of each bar/group of bar
* which changes if stacked or not
*
*/
// Callback called when the chartData is assigned
$scope.initLabels = function() {
return function(graph) {
promise = $timeout(function() {
var svg = d3.select("svg"),
lastRects, rectWidth,
heightForXvalue = []; // Used for grouped mode
// We get one positive rect of each serie from the svg (here the last serie)
lastRects = svg.selectAll("g.nv-group").filter(
function(d, i) {
return i == countSeriesDisplayed - 1;
}).selectAll("rect.positive");
if ($scope.isStacked) {
// If stacked, we get the width of one rect
rectWidth = lastRects.filter(
function(d, i) {
return i == countSeriesDisplayed - 1;
}).attr("width");
} else {
// If grouped, we need to get the greatest height of each bar
var nvGroups = svg.selectAll("g.nv-group").selectAll("rect.positive");
nvGroups.each(
function(d, i) {
// Get the Min height space for each group (Max height for each group)
var rectHeight = parseFloat(d3.select(this).attr("y"));
if (angular.isUndefined(heightForXvalue[i])) {
heightForXvalue[i] = rectHeight;
} else {
if (rectHeight < heightForXvalue[i]) {
heightForXvalue[i] = rectHeight;
}
}
}
);
// We get the width of one rect multiplied by the number of series displayed
rectWidth = lastRects.filter(
function(d, i) {
return i == countSeriesDisplayed - 1;
}).attr("width") * countSeriesDisplayed;
}
// We choose a width equals to 70% of the group width
var labelWidth = rectWidth * 70 / 100;
var groupLabels = svg.select("g.nv-barsWrap").append("g");
lastRects.each(
function(d, index) {
var transformAttr = d3.select(this).attr("transform");
var yPos = parseFloat(d3.select(this).attr("y"));
groupLabels.append("text")
.attr("x", (rectWidth / 2) - (labelWidth /2)) // We center the label
// We add a padding of 5 above the highest rect
.attr("y", (angular.isUndefined(heightForXvalue[index]) ? yPos : heightForXvalue[index]) - 5)
// We set the text
.text(labels[index])
.attr("transform", transformAttr)
.attr("class", "bar-chart-label");
});
}, ANIMATION_TIME);
}
};
// Tooltips
$scope.toolTipContentFunction = function () {
return function (key, x, y, e, graph) {
return labels[x];
}
};
$scope.$on('$destroy', function () {
// Cancel timeout if still active
$timeout.cancel(promise);
});
});
UPDATE:
I've created a gist that could help you to implement this by yourself.
https://gist.github.com/topicus/217444acb4204f364e46