D3 multi line y-axis chart from textarea input - d3.js

I want to make multiple line chart from textarea inout. When we paste some data from excel then textarea values convers into array and used by d3 to plot the data. Now, I am confused how to apply nest() function here, as I have seen people applying it for .csv files. Or what we need to do to make it multiple line chart. Please take a look at my code.
function mainData(){
var data = document.getElementById('dinput').value.trim().split('\n').map(line => {
let tokens = line.trim().split(/\s+/).map(str => Number(str, 10));
return { x: tokens[0], y: tokens[1] }
});return data;};
document.getElementById('dinput').addEventListener('paste', function (e) {
document.getElementById('dinput').addEventListener('input', function () {
d3.select(".chart").remove();
draw();
});});
async function draw() {
const dataset = mainData();
const xAccessor = d => { return d.x; }
const yAccessor = d => { return d.y; }
let dimensions = { width: 1280, height: 720, margins: 50,};
dimensions.ctrWidth = dimensions.width - dimensions.margins * 2
dimensions.ctrHeight = dimensions.height - dimensions.margins * 2
const svg = d3.select('#chartdiv').append("svg").classed("chart",true).attr("viewBox", "0 0 "+ dimensions.width +" "+ dimensions.height +"")
const ctr = svg.append("g").attr("transform",`translate(${dimensions.margins}, ${dimensions.margins})`)
const yScale = d3.scaleLinear().domain(d3.extent(dataset, yAccessor)).range([dimensions.ctrHeight, 0]).nice();
const xScale = d3.scaleLinear().domain(d3.extent(dataset, xAccessor)).range([0, dimensions.ctrWidth]).nice();
const lineGenerator = d3.line().x((d) => xScale(xAccessor(d))).y((d) => yScale(yAccessor(d)))
ctr.append('path').datum(dataset).attr('d', lineGenerator).attr('fill', 'none').attr('stroke', '#30475e').attr('stroke-width', 2)
ctr.append('g').call(d3.axisLeft(yScale))
ctr.append('g').style('transform', `translateY(${dimensions.ctrHeight}px)`).call(d3.axisBottom(xScale))
}
draw()
<textarea id="dinput">
280 0.013 0.013
290 0.035 0.013
300 0.140 0.013
310 0.425 0.313
320 0.820 0.013
330 1.152 0.013
340 0.802 0.013
350 0.423 0.013
360 0.123 0.013
370 0.040 0.013
380 0.020 0.013
390 0.020 0.831
400 0.020 0.013
410 0.020 0.013
420 0.015 0.013
</textarea>
<div id="chartdiv"></div>
<script src="https://d3js.org/d3.v7.min.js"></script>

Related

Swap image sprite in HTML5 Canvas game

Trying to add basically different looking zombie. Is there a way to just swap the sprite or would I have to keep adding the code for class, scrolling etc.
class Zombie1 {
constructor({
position,
velocity,
distance = {
limit: 50,
traveled: 0
}
}) {
this.position = {
x: position.x,
y: position.y
}
this.velocity = {
x: velocity.x,
y: velocity.y
}
this.width = 97.49
this.height = 150
this.image = createImage(spriteZombie1)
this.frames = 0
this.distance = distance
}
draw() {
// c.fillStyle = 'red'
// c.fillRect(this.position.x, this.position.y, this.width, this.height)
c.drawImage(
this.image,
130 * this.frames,
0,
130,
150,
this.position.x,
this.position.y,
this.width,
this.height
)
}
update() {
this.frames++
if (this.frames >= 58) this.frames = 0
this.draw()
this.position.x += this.velocity.x
this.position.y += this.velocity.y
this.enemies = [];
if (this.position.y + this.height + this.velocity.y <= canvas.height)
this.velocity.y += gravity
// walk the zombie back and forth
this.distance.traveled += Math.abs(this.velocity.x)
if (this.distance.traveled > this.distance.limit) {
this.distance.traveled = 0
this.velocity.x = -this.velocity.x
}
}
}
zombies1 = [
new Zombie1({
position: {
x: 4000,
y: -950
},
velocity: {
x: -1,
y: 0
},
distance: {
limit: 7,
traveled: 0
}
}),
I have no problem adding more classes, it gets tedious and clutters the code. It won't let me post anymore code but that seems to be the important ones, other than importing the sprites.

A-frame random animation delay

I am creating a scene using A-frame (https://aframe.io) and I'm currently using an animation in my scene. I'm wondering how I can randomize the delay on my animation so the animation delays from a random amount from 0 to 2 seconds. How can this be done? My current code:
<html>
<head>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
</head>
<body>
<a-scene>
<!--Animte the box with a ranom delay from 0 - 2 seconds. -->
<a-box position="-1 1.6 -5" animation="property: position; delay: 1000; to: 1 8 -10; dur: 2000; easing: linear; loop: true" color="tomato"></a-box>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
</body>
</html>
I currently have the delay set to 1000 (1 second since the delay is in milliseconds). What should happen instead is the delay should be a random amount from 0 - 2000 since 2000 milliseconds is 2 seconds. How can this be done?
Random delay only at the start - first cycle
Register the a-box component with a custom one that will do the math and apply it to the animation on the a-box <a-box position="-1 1.6 -5" random-delay animation color="tomato"></a-box>.
Building the custom aframe component:
AFRAME.registerComponent('random-delay', {
init: function (el) {
// this.el points to the element the component is placed on
var el = this.el;
}
});
Set your min and max values
AFRAME.registerComponent('random-delay', {
init: function (el) {
var el = this.el;
var min = 0;
var max = 2000;
}
});
Math.floor(Math.random() * (max - min + 1)) + min returns an integer random number between min and max and than we build the string for the animation:
AFRAME.registerComponent('random-delay', {
init: function (el) {
var el = this.el;
var min = 0;
var max = 2000;
// generates a random number between the set min and max
var delay = Math.floor(Math.random() * (max - min + 1)) + min;
var animation = `property: position; delay: ${delay}; from: -1 1.6 -5; to: 1 8 -10; dur: 2000; easing: linear; loop: true`
}
});
Apply the animation string with the delay generated with .setAttribue():
AFRAME.registerComponent('random-delay', {
init: function (el) {
var el = this.el;
var min = 0;
var max = 2000;
// generates a random number between the set min and max
var delay = Math.floor(Math.random() * (max - min + 1)) + min;
// builds the string for the animation
var animation = `property: position; delay: ${delay}; to: 1 8 -10; dur: 2000; easing: linear; loop: true`
// updating the animation component with the .setAttribute() function
el.setAttribute('animation', animation)
}
});
Random delay on every animation cycle
place these lines of code into a new function:
function setAnimation(min, max) {
// generates a random number between the set min and max
var delay = Math.floor(Math.random() * (max - min + 1)) + min;
var animation = `property: position; delay: ${delay}; from: -1 1.6 -5; to: 1 8 -10; dur: 2000; easing: linear; `
console.log(delay);
// updating the animation component with the .setAttribute function
el.setAttribute('animation', animation)
}
At every animationcomplete event set a new animation with the function we created
function setAnimation(min, max) {
// generates a random number between the set min and max
var delay = Math.floor(Math.random() * (max - min + 1)) + min;
var animation = `property: position; delay: ${delay}; from: -1 1.6 -5; to: 1 8 -10; dur: 2000; easing: linear; `
console.log(delay);
// updating the animation component with the .setAttribute function
el.setAttribute('animation', animation)
}
The result should look something like this:
<html>
<head>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
</head>
<script>
AFRAME.registerComponent('random-delay', {
init: function (el) {
// this.el points to the element the component is placed on
var el = this.el;
let min = 0;
let max = 2000;
// initial animation
setAnimation(min, max)
function setAnimation(min, max) {
// generates a random number between the set min and max
let delay = Math.floor(Math.random() * (max - min + 1)) + min;
let animation = `property: position; delay: ${delay}; from: -1 1.6 -5; to: 1 8 -10; dur: 2000; easing: linear; `
console.log(delay);
// updating the animation component with the .setAttribute function
el.setAttribute('animation', animation)
}
el.addEventListener('animationcomplete', () => {
setAnimation(min, max)
});
}
});
</script>
<body>
<a-scene>
<a-box position="-1 1.6 -5" random-delay animation autoplay color="tomato">
</a-box>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
</body>
</html>

Aframe update camera fov at runtime

I've been trying to update properties of the camera on the fly to no avail - really don't want to setup multiple cameras and switch between them.
AFRAME.registerComponent("camera-controller", {
init: function() {
this.el.addEventListener('model-loaded', this.update.bind(this));
},
update: function() {
var data = this.data;
var el = this.el;
// el.fov = 75;
el.object3D.el.components.camera.data.fov = 20;
el.updateProjectionMatrix();
console.log(el.object3D.el.components.camera.data.fov);
}
});
camera.fov just returns undefined and updateProjectionMatrix() is not a function.
You can simply update the [camera]s FOV property via setAttribute("camera", "fov", newFOV). Something like this:
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("foo", {
init: function() {
// grab the camera
const cam = document.querySelector("[camera]")
// on click
document.body.addEventListener("click", e => {
// grab the current FOV
const fov = cam.getAttribute("camera").fov
// update the camera with the new FOV
cam.setAttribute("camera", "fov", fov == 80 ? 120 : 80)
})
}
})
</script>
<a-scene foo>
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
You can make the transition animated with the standard animation component (animating camera.fov) - here's an example with one animation (to utilize the above js logic, you could make two animations if you prefer a more 'declarative' style).

Place several symbols on top of each candle on the canvas chart

Created a simplified example of what I need. There are 50 items in the series. Each item has a vertical list of five + symbols.
const data = [...Array(50).keys()];
const container = document.querySelector('d3fc-canvas');
const xScale = d3.scaleLinear().domain([0, data.length - 1]);
const yScale = d3.scaleLinear().domain(fc.extentLinear()(data));
const series = fc
.seriesCanvasPoint()
.xScale(xScale)
.yScale(yScale)
.crossValue((_, i) => i)
.mainValue(d => d)
.size((_, i) => 0)
.decorate((context, datum) => {
// Draw 5 symbols above current value
for (let i = 0; i < 5; i++) {
const y = yScale.invert(datum + i)
context.textAlign = 'center';
context.fillStyle = '#000';
context.font = '25px Arial';
context.fillText('+', 0, 0);
context.translate(0, y);
}
});
d3.select(container)
.on('draw', () => {
series(data);
})
.on('measure', () => {
const {
width,
height
} = event.detail;
xScale.range([0, width]);
yScale.range([height, 0]);
const ctx = container.querySelector('canvas').getContext('2d');
series.context(ctx);
});
container.requestRedraw();
body {
color: #1b1e23;
font-size: small;
font-family: sans-serif;
height: calc(100vh - 2em);
margin: 1em;
display: flex;
}
body>* {
flex: auto;
}
.domain,
.gridline-y,
.gridline-x,
.annotation-line>line {
stroke: currentColor;
stroke-opacity: 0.1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/d3fc/build/d3fc.js"></script>
<d3fc-canvas use-device-pixel-ratio></d3fc-canvas>
Now, I'm trying to make it work with an actual financial chart and place these five + symbols vertically within each candle. It doesn't work because I don't know how to correctly convert Y value of each + to the coordinate system of the canvas.
const stream = fc.randomFinancial().stream();
const data = stream.take(20);
const scaleX = d3.scalePoint();
const scaleY = d3.scaleLinear();
const yExtent = fc.extentLinear().accessors([d => d.high, d => d.low]);
const xExtent = fc.extentLinear().accessors([d => d.date.getTime()]);
const gridlines = fc.annotationCanvasGridline();
const candlestick = fc.seriesCanvasCandlestick()
.crossValue((o, i) => o.date.getTime())
.lowValue((o, i) => o.low)
.highValue((o, i) => o.high)
.openValue((o, i) => o.open)
.closeValue((o, i) => o.close);
const points = fc
.seriesCanvasPoint()
.crossValue((o, i) => o.date.getTime())
.mainValue(d => d.close)
.size((_, i) => 0)
.decorate((context, datum) => {
const arrows = getMarks(datum.open, datum.close);
//context.translate(0, 0);
// Draw 5 symbols above current value
for (let i = 0; i < arrows.length; i++) {
const posCurrent = scaleY.invert(arrows[i])
//context.translate(0, 15);
context.translate(0, posCurrent); // move to current arrow's position
context.textAlign = 'center';
context.fillStyle = '#000';
context.font = '25px Arial';
context.fillText('+', 0, 0);
context.translate(0, -posCurrent); // reset and go back to start point
}
});
const multi = fc
.seriesCanvasMulti()
.series([gridlines, candlestick, points]);
const chart = fc
.chartCartesian(scaleX, scaleY)
.canvasPlotArea(multi);
function getMarks(open, close) {
let price = close;
const arrows = [];
const gap = Math.abs(close - open) / 5;
for (let i = 0; i < 5; i++) {
arrows.push(close > open ? price -= gap : price += gap);
}
return arrows;
}
function renderChart() {
data.push(stream.next());
data.shift();
scaleX.domain(data.map(o => o.date.getTime()));
scaleY.domain(yExtent(data));
chart
.xDomain(scaleX.domain())
.yDomain(scaleY.domain());
d3.select('#chart')
.datum(data)
.call(chart);
}
renderChart();
setInterval(renderChart, 1000);
#chart {
height: 500px;
}
<script src="https://unpkg.com/d3"></script>
<script src="https://unpkg.com/d3fc"></script>
<div id="chart"></div>
Is there a way to properly show these marks on top of the candle?
Cross-reference to Github for more details and examples.
https://github.com/d3fc/d3fc/issues/1635
Firstly, scale.invert is meant for when you have the pixel coordinate, but not the corresponding datum value. This is relevant when you are doing clicks on a screen, for example. In this case, it's not the right tool for the job. You already have access to datum, so you just need to call scaleY(datum.close).
However, d3fc uses canvas.translate() to change the zero point to .mainvalue(). Therefore, you cannot simply use scaleY(datum.close) to get the y-coordinate of the close value. Instead of (x, y), each point you want to draw becomes (0, y(arrow) - y(d.close)). So you need to change the logic to reflect this.
Needing to clean up after yourself with context.translate() is a recipe for bugs, It's almost always better to avoid such things.
As a final note, I had some confusion about your getMarks function. If you want to draw 5 arrows evenly distributed over your bar, you need to have a gap of 1/4th of the bar height. Think of it like trees on a street. If the trees at 10 feet apart and the street is 100 feet long, you'll have 11 trees, not 10. Because of this, I drew 6 plus signs, not 5, because otherwise it looked kind of off balance.
const stream = fc.randomFinancial().stream();
const data = stream.take(20);
const scaleX = d3.scalePoint();
const scaleY = d3.scaleLinear();
const yExtent = fc.extentLinear().accessors([d => d.high, d => d.low]);
const xExtent = fc.extentLinear().accessors([d => d.date.getTime()]);
const gridlines = fc.annotationCanvasGridline();
const candlestick = fc.seriesCanvasCandlestick()
.crossValue((o, i) => o.date.getTime())
.lowValue((o, i) => o.low)
.highValue((o, i) => o.high)
.openValue((o, i) => o.open)
.closeValue((o, i) => o.close);
const points = fc
.seriesCanvasPoint()
.crossValue((o, i) => o.date.getTime())
.mainValue(d => d.close)
.size((_, i) => 0)
.decorate((context, datum) => {
const arrows = getMarks(datum.open, datum.close);
const top = scaleY(datum.close);
// We need a little bit of offset, because the "+" is 20 pixels high
// and we want it in the middle of the point, not above it.
const offset = 10;
// Draw 5 symbols above current value
for (let i = 0; i < arrows.length; i++) {
context.textAlign = 'center';
context.fillStyle = '#000';
context.font = '25px Arial';
context.fillText('+', 0, scaleY(arrows[i]) - top + offset);
}
});
const multi = fc
.seriesCanvasMulti()
.series([gridlines, candlestick, points]);
const chart = fc
.chartCartesian(scaleX, scaleY)
.canvasPlotArea(multi);
function getMarks(open, close) {
let price = Math.min(open, close);
const arrows = [];
const gap = Math.abs(close - open) / 5;
for (let i = 0; i < 6; i++) {
arrows.push(price + i * gap);
}
return arrows;
}
function renderChart() {
data.push(stream.next());
data.shift();
scaleX.domain(data.map(o => o.date.getTime()));
scaleY.domain(yExtent(data));
chart
.xDomain(scaleX.domain())
.yDomain(scaleY.domain());
d3.select('#chart')
.datum(data)
.call(chart);
}
renderChart();
setInterval(renderChart, 1000);
#chart {
height: 500px;
}
<script src="https://unpkg.com/d3"></script>
<script src="https://unpkg.com/d3fc"></script>
<div id="chart"></div>

Updating the transform matrix of one fabric object based on the changes to the transform matrix of another object

I am trying to synchronize the move,resize and rotate operations of two fabric objects.
Consider there are two polygons - Poly1 and Poly2.
When Poly1 is modified in any manner, I need to apply the same modification on Poly2 and need to get the updated points for Poly2. Below is the approach being followed :
Get the transform matrices of Poly1 before and after modification.
Use them to get the matrix which modified the transform matrix from
its original to the modified state.
Multiply the matrix from the above step to the current transform
matrix of Poly2 to get the new transform matrix.
Use the new transform matrix and get the updated points and redraw
Poly2.
However, this approach is only working for move operation. If you do resize or rotate on Poly1, the same is not being applied correctly on Poly2. Please let me know what I am doing wrong here.
Thanks in advance for any help!!
Sample code - Blue is Poly1 and Red is Poly2
var oldPoly1Matrix;
var canvas = new fabric.Canvas('c');
var srcOptions = {
stroke: 'blue',
fill: '',
type: 'src'
};
var destOptions = {
stroke: 'red',
fill: '',
selectable: false,
hasControls: false,
hasBorders: false,
type: 'dest'
};
var poly1 = new fabric.Polygon([{
x: 60,
y: 40
}, {
x: 160,
y: 40
}, {
x: 160,
y: 140
}, {
x: 60,
y: 140
}], srcOptions);
var poly2 = new fabric.Polygon([{
x: 60,
y: 300
}, {
x: 160,
y: 300
}, {
x: 160,
y: 400
}, {
x: 60,
y: 400
}], destOptions);
canvas.add(poly1).add(poly2);
oldPoly1Matrix = poly1.calcTransformMatrix();
var originalPoly2Matrix = poly2.calcTransformMatrix();
poly2.matrix = originalPoly2Matrix;
function updatePoly2() {
var newPoly1Matrix = poly1.calcTransformMatrix();
var oldPoly1MatrixInverted = fabric.util.invertTransform(oldPoly1Matrix);
//newMatrix = oldMatrix * someMatrix
//therefore,someMatrix = newMatrix / oldMatrix = newMatrix * inverse(oldMatrix);
var diffMatrix = fabric.util.multiplyTransformMatrices(newPoly1Matrix, oldPoly1MatrixInverted);
var oldPoly2Matrix = poly2.matrix;
//Apply the same someMatrix to find out the new transform matrix for poly2.
var newPoly2Matrix = fabric.util.multiplyTransformMatrices(oldPoly2Matrix, diffMatrix);
var updatedPoints = poly2.get('points')
.map(function(p) {
return new fabric.Point(p.x - poly2.minX - poly2.width / 2, p.y - poly2.minY - poly2.height / 2);
})
.map(function(p) {
return fabric.util.transformPoint(p, newPoly2Matrix);
});
oldPoly1Matrix = newPoly1Matrix;
poly2.remove();
poly2 = new fabric.Polygon(updatedPoints, destOptions);
poly2.matrix = newPoly2Matrix;
canvas.add(poly2);
}
canvas {
border: 1px solid;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.19/fabric.js"></script>
<canvas id="c" height="500" width="600"></canvas>
<button id="update" onclick="updatePoly2()">
Update red shape
</button>
jsfiddle - https://jsfiddle.net/btxp0ck6/
It is working fine after changing the translateX and translateY calculation from matrix mulplication to simple subtraction.Posting the code here for reference.
var oldPoly1Matrix;
var canvas = new fabric.Canvas('c');
var srcOptions = {
stroke: 'blue',
fill: '',
type: 'src'
};
var destOptions = {
stroke: 'red',
fill: '',
selectable: false,
hasControls: false,
hasBorders: false,
type: 'dest'
};
var poly1 = new fabric.Polygon([{
x: 60,
y: 40
}, {
x: 160,
y: 40
}, {
x: 160,
y: 140
}, {
x: 60,
y: 140
}], srcOptions);
var poly2 = new fabric.Polygon([{
x: 60,
y: 300
}, {
x: 160,
y: 300
}, {
x: 160,
y: 400
}, {
x: 60,
y: 400
}], destOptions);
canvas.add(poly1).add(poly2);
oldPoly1Matrix = poly1.calcTransformMatrix();
var originalPoly2Matrix = poly2.calcTransformMatrix();
poly2.matrix = originalPoly2Matrix;
function updatePoly2() {
var newPoly1Matrix = poly1.calcTransformMatrix();
var oldPoly1MatrixInverted = fabric.util.invertTransform(oldPoly1Matrix);
//newMatrix = oldMatrix * someMatrix
//therefore,someMatrix = newMatrix / oldMatrix = newMatrix * inverse(oldMatrix);
var diffMatrix = fabric.util.multiplyTransformMatrices(newPoly1Matrix, oldPoly1MatrixInverted,true);
diffMatrix[4] = newPoly1Matrix[4] - oldPoly1Matrix[4];
diffMatrix[5] = newPoly1Matrix[5] - oldPoly1Matrix[5];
var oldPoly2Matrix = poly2.calcTransformMatrix();
//Apply the same someMatrix to find out the new transform matrix for poly2.
var newPoly2Matrix = fabric.util.multiplyTransformMatrices(oldPoly2Matrix, diffMatrix,true);
newPoly2Matrix[4] = oldPoly2Matrix[4] + diffMatrix[4];
newPoly2Matrix[5] = oldPoly2Matrix[5] + diffMatrix[5];
var updatedPoints = poly2.get('points')
.map(function(p) {
return new fabric.Point(p.x - poly2.minX - poly2.width / 2, p.y - poly2.minY - poly2.height / 2);
})
.map(function(p) {
return fabric.util.transformPoint(p, newPoly2Matrix);
});
oldPoly1Matrix = newPoly1Matrix;
poly2.remove();
poly2 = new fabric.Polygon(updatedPoints, destOptions);
poly2.matrix = newPoly2Matrix;
canvas.add(poly2);
}
canvas {
border: 1px solid;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.19/fabric.js"></script>
<canvas id="c" height="500" width="600"></canvas>
<button id="update" onclick="updatePoly2()">
Update red shape
</button>
Jsfiddle - https://jsfiddle.net/btxp0ck6/1/

Resources