When extending traces with plotly the points are drawn in the past. As you can see in the picture, any
Picture of the problem
The chart now consists of 2 parts one on the right is data from the database, and to the left is data being plotted in "real-time". The data that is being plotted in realtime is to the left of the data from the database is even though the timestamps are after the ones from the databases.
The time stamp on the console logs is correct but plotly is placing them at the wrong time on the x-axis. The should be drawn at the end of the graph.
updateData (sensorData) {
if (!this.initiated || !this.isLiveData) {
return
}
let y = []
let i = 0
let x = []
let traces = []
for (let sensor in this.configuration) {
for (let signal in this.configuration[sensor]) {
let xDate = new Date(sensorData[sensor].timestamp)
if (sensorData.hasOwnProperty(sensor) && sensorData[sensor].hasOwnProperty(signal)) {
y.push([sensorData[sensor][signal]])
x.push([xDate.getTime()])
}
// x time seems to be good here
console.log('Update data', xDate.getTime(), xDate)
traces.push(i)
i++
}
}
console.log('Plotting', y, x, this.widget.getXRange())
if (y.length > 0) {
this.$plotly.extendTraces('plotly-chart', {
y: y,
x: x
}, traces)
}
},
The data from the db is added with following code.
updateFromDb (sensorData) {
let data = []
let x = []
let yData = {}
for (let sensor in this.configuration) {
for (let i = 0, len = sensorData[sensor].length; i < len; i++) {
let timestamp = sensorData[sensor][i].timestamp
x.push(timestamp)
for (let source in this.configuration[sensor]) {
let name = sensor + ' ' + getSignalName(source, this.angleLabel)
if (!yData.hasOwnProperty(name)) {
yData[name] = {
data: [],
color: this.configuration[sensor][source].color
}
}
if (!sensorData[sensor][i].hasOwnProperty(source)) {
yData[name].data.push(0)
} else {
yData[name].data.push(sensorData[sensor][i][source])
}
if (this.configuration[sensor][source].hasOwnProperty('yaxis')) {
yData[name]['yaxis'] = 'y' + this.configuration[sensor][source].yaxis
}
}
}
}
for (let name in yData) {
let sensorData = {
name: name,
x: x,
y: yData[name].data,
type: 'line',
mode: 'lines',
line: {
width: 2,
color: yData[name].color
},
marker: {
width: 2,
color: yData[name].color
}
}
if (yData[name].hasOwnProperty('yaxis')) {
sensorData['yaxis'] = yData[name].yaxis
}
data.push(sensorData)
}
this.$plotly.react(
document.getElementById('plotly-chart'),
data,
this.getLayout(false),
this.chartProperties
)
}
There is also a function that scroll the window over xaxis every 50 ms to make it look smooth.
windowScroller () {
if (!this.initiated || !this.isLiveData) {
return
}
let timeDifference = 0
if (this.timeDifference !== null) {
timeDifference = this.timeDifference
}
/**
* Make sure the line never gets behind the scroller.
*/
if (Object.keys(this.latestPoint).length > 0) {
let latestTime = this.latestPoint[Object.keys(this.latestPoint)[0]].timestamp
let scrollDiff = new Date().getTime() - latestTime
if (scrollDiff !== this.scrollDelay && this.lastDelayTime !== latestTime && scrollDiff < 60000) {
this.lastDelayTime = latestTime
this.scrollDelay = scrollDiff
// console.log('update scroll', scrollDiff, 'from', latestTime)
}
}
let currentTime = new Date().getTime() - timeDifference - this.scrollDelay
let firstTime = new Date().getTime() - this.getMilisecondsFromMinutes(this.widget.getXRange()) - timeDifference - this.scrollDelay
let relayoutPromise = new Promise((resolve, reject) => {
this.$plotly.relayout('plotly-chart', {
xaxis: {
color: '#fff',
type: 'date',
range: [firstTime, currentTime]
}
})
})
relayoutPromise.then(() => {
console.log('relayout')
})
let data = document.getElementById('plotly-chart').data
// We calculate the max points using 4 hertz
let maxPoints = (this.getSecondsFromMinutes(this.widget.getXRange()) + 10) * this.widget.getRefreshRate()
if (this.minMax) {
maxPoints = maxPoints * 2
}
for (let i in data) {
if (data[i].y.length >= maxPoints) {
data[i].y.shift()
data[i].x.shift()
}
}
}
Related
I am making a Discord Bot command using Canvas-Constructor where users can draw something and pick any color. The only issue I am facing is that when you draw and select a different color it overwrites the already colored pixels on the canvas.
I would like to know how to fix this problem, here is a video showing how the issue occurs:
https://streamable.com/uv8mp1
Note: Zoom Factor is set as 10.
penx: Math.floor(10 / 2);
peny: this.penx;
The command uses a JavaScript class to create and manage the canvas drawing. The code below is how the color, pen xy axis and setting the canvas pixel works and is what is affected in this problem. sendCanvas method simply just sends the generated image and buttons:
class Drawing {
constructor(int, size, name, brushRow, arrowRow, colorRow, bgcolor) {
this.int = int;
this.canvasmsg;
this.size = size;
this.name = name;
this.arrowRow = arrowRow;
this.colorRow = colorRow;
this.realsize = size * zoomfactor;
this.penx = Math.floor(size / 2);
this.peny = this.penx;
this.penstate = false;
this.toggledColor = [];
this.fcolor = this.fcolor || 'rgb(0, 0, 0)';
this.bcolor = bgcolor || 'rgb(255, 255, 255)';
this.initPixels();
this.c = new Canvas(this.realsize, this.realsize).setColor(this.bcolor).printRectangle(0, 0, this.realsize, this.realsize);
this.drawCanvas();
const filter = (i) => i.user.id === int.user.id;
this.collector = int.channel.createMessageComponentCollector({ filter });
const self = this;
this.collector.on('collect', async (i) => {
if (i.user.id === this.int.user.id) {
await i.deferUpdate();
self.handleComponent(i);
}
else {
i.reply({ content: `**Draw 🎨 |** **${this.int.user.username}** can only use the buttons and dropdown. *Sorry about that!*`, ephemeral: true });
}
});
}
stop() {
this.collector.stop();
this.drawCanvas(true);
inProgress.delete(this.int.channel.id);
const attachment = this.canvasmsg.attachments.first();
const shareRow = new MessageActionRow()
.addComponents(
new MessageButton()
.setLabel('Share on Twitter')
.setStyle('LINK')
.setURL(`https://twitter.com/intent/tweet?text=Check%20out%20my%20drawing%20made%20using%20Nate%20Bot!%20%F0%9F%8E%A8%20-%20via%20%40ntmcentral&url=${attachment.url}`)
.setEmoji('<:twitter:863917419625971712>'),
new MessageButton()
.setLabel('Open in Browser')
.setStyle('LINK')
.setURL(attachment.url)
.setEmoji('🌐'),
);
this.int.editReply({ content: `**Draw 🎨 |** **${this.name}** • **\`${this.size}x${this.size}\`**`, components: [shareRow] });
}
handleComponent(component) {
const customId = component.customId;
if (customId === 'drawLeft' || customId === 'drawRight' || customId === 'drawUp' || customId === 'drawDown') {
this.movePen(customId);
this.drawCanvas();
}
else if (customId === 'drawFinish') {
this.stop();
}
else if (customId === 'drawBrush') {
this.toggleBrushstate();
this.drawCanvas();
}
else if (customId === 'drawColor' && component.componentType === 'SELECT_MENU') {
this.toggleColor(component.values[0]);
}
}
movePen(dir) {
const movement = {
'drawLeft': 0,
'drawRight': 1,
'drawUp': 2,
'drawDown': 3,
};
const xactions = [-1, 1, 0, 0];
const yactions = [0, 0, -1, 1];
if ((this.penx > 0 || xactions[movement[dir]] === 1) && (this.penx < this.size || xactions[movement[dir]] === -1)) this.penx += xactions[movement[dir]];
if ((this.peny > 0 || yactions[movement[dir]] === 1) && (this.peny < this.size || yactions[movement[dir]] === -1)) this.peny += yactions[movement[dir]];
}
toggleBrushstate() {
this.penstate = !this.penstate;
const brushRow = new MessageActionRow()
.addComponents(
new MessageButton()
.setLabel('Finish')
.setCustomId('drawFinish')
.setStyle('PRIMARY')
.setEmoji('<:checkmark_white:925544348903424000>'),
new MessageButton()
.setLabel(`Brush [${this.penstate ? 'ON' : 'OFF'}]`)
.setCustomId('drawBrush')
.setStyle(this.penstate ? 'SUCCESS' : 'DANGER')
.setEmoji('🖌️'),
);
this.int.editReply({ components: [brushRow, this.arrowRow, this.colorRow] });
}
toggleColor(color) {
this.fcolor = color;
}
initPixels() {
this.pixels = [];
for (let i = 0; i < Math.pow(this.size, 2); i++) {
this.pixels.push(false);
}
}
setPixel(x, y) {
this.pixels[x + (y * this.size)] = true;
}
setCanvasPixel(x, y, color) {
this.c.setColor(color).printRectangle(x * zoomfactor, y * zoomfactor, zoomfactor, zoomfactor);
}
drawCanvas(end = false) {
if (this.penstate) this.setPixel(this.penx, this.peny);
for (let x = 0; x < this.size; x++) {
for (let y = 0; y < this.size; y++) {
this.setCanvasPixel(x, y, this.pixels[x + (y * this.size)] ? this.fcolor : this.bcolor);
}
}
if (!end) {
this.setCanvasPixel(this.penx, this.peny, this.pixels[this.penx + (this.peny * this.size)] ? '#5A0000' : 'red');
this.sendCanvas();
}
else {
this.sendCanvas();
}
}
async sendCanvas() {
if (this.canvasmsg) this.canvasmsg.delete().catch(e => console.error(e));
this.int.channel.send({
files: [new MessageAttachment(this.c.toBuffer(), `${this.name}.png`)],
}).then(msg => {
this.canvasmsg = msg;
});
}
}
I'm implementing A*(A-star) algorithm in react.js but my program crashes whenever startNode(green) or destinationNode(blue) have more than one neighbour or if there is a cycle in the graph. There is something wrong when adding and deleting the neighbours from/to openList or when updating the parentId in the getPath() function. I cant even see the console because the website goes down.
Each node has: id, name, x, y, connectedToIdsList:[], gcost:Infinity, fcost:0, heuristic:0, parentId:null.
I'm sending the path to another component "TodoList" which prints out the path. My program should not return the path but keeps updating the path as i'm adding nodes and edges to the list. Please help, I've been stuck for hours now:/
My code:
export default class TurnByTurnComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = { shortestPath: [] }
}
render() {
const {
destinationLocationId,
locations,
originLocationId
} = this.props;
let path = []
if (destinationLocationId != null && originLocationId != null) {
if (originLocationId == destinationLocationId) { //check if the startNode node is the end node
return originLocationId;
}
var openList = [];
let startNode = getNodeById(originLocationId);
let destinationNode = getNodeById(destinationLocationId)
if (startNode.connectedToIds.length > 0 && destinationNode.connectedToIds.length > 0) { //check if start and destination nodes are connected first
startNode.gcost = 0
startNode.heuristic = manhattanDistance(startNode, destinationNode)
startNode.fcost = startNode.gcost + startNode.heuristic;
//perform A*
openList.push(startNode); //starting with the startNode
while (openList.length > 0) {
console.log("inside while")
var currentNode = getNodeOfMinFscore(openList); //get the node of the minimum f cost of all nodes in the openList
if (currentIsEqualDistanation(currentNode)) {
path = getPath(currentNode);
}
deleteCurrentFromOpenList(currentNode, openList);
for (let neighbourId of currentNode.connectedToIds) {
var neighbourNode = getNodeById(neighbourId);
currentNode.gcost = currentNode.gcost + manhattanDistance(currentNode, neighbourNode);
if (currentNode.gcost < neighbourNode.gcost) {
neighbourNode.parentId = currentNode.id; // keep track of the path
// total cost saved in neighbour.g
neighbourNode.gcost = currentNode.gcost;
neighbourNode.heuristic = manhattanDistance(neighbourNode, destinationNode);
neighbourNode.fcost = neighbourNode.gcost + neighbourNode.heuristic; //calculate f cost of the neighbourNode
addNeighbourNodeToOpenList(neighbourNode, openList);
}
}
}
path = path.reverse().join("->");
}
}
function addNeighbourNodeToOpenList(neighbourNode, openList) {
//add neighbourNode to the open list to be discovered later
if (!openList.includes(neighbourNode)) {
openList.push(neighbourNode);
}
}
function deleteCurrentFromOpenList(currNode, openList) {
const currIndex = openList.indexOf(currNode);
openList.splice(currIndex, 1); //deleting currentNode from openList
}
function currentIsEqualDistanation(currNode) {
//check if we reached out the distanation node
return (currNode.id == destinationLocationId)
}
function getNodeById(nid) {
var node;
for (let i = 0; i < locations.length; i++) {
if (locations[i].id == nid) {
node = locations[i]
}
}
return node
}
function getPath(destNode) {
console.log("inside getpath")
var parentPath = []
var parent;
while (destNode.parentId != null) {
parentPath.push(destNode.name)
parent = destNode.parentId;
destNode = getNodeById(parent);
}
//adding startNode to the path
parentPath.push(getNodeById(originLocationId).name)
return parentPath;
}
function getNodeOfMinFscore(openList) {
var minFscore = openList[0].fcost; //initValue
var nodeOfminFscore;
for (let i = 0; i < openList.length; i++) {
if (openList[i].fcost <= minFscore) {
minFscore = openList[i].fcost //minFvalue
nodeOfminFscore = openList[i]
}
}
return nodeOfminFscore
}
//manhattan distance is for heuristic and gScore. Here I use Manhattan instead of Euclidean
//because in this example we dont have diagnosal path.
function manhattanDistance(stNode, dstNode) {
var x = Math.abs(dstNode.x - stNode.x);
var y = Math.abs(dstNode.y - stNode.y);
var dist = x + y;
return dist;
}
return (
<div className="turn-by-turn-component">
<TodoList
list={"Shortest path: ", [path]}
/>
<TodoList
list={[]}
/>
</div>
);
}
}
TurnByTurnComponent.propTypes = {
destinationLocationId: PropTypes.number,
locations: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
connectedToIds: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired
})),
originLocationId: PropTypes.number
};
Update new issue
Pictures of before and after linking a new node. When I update the graph and add a new node the path disappears. And sometimes come back and so on if I still add new nodes and edges to the graph. As I said, each node has: id, name, x, y, connectedToIdsList:[], gcost:Infinity, fcost:0, heuristic:0, parentId:null.
My new code now is:
export default class TurnByTurnComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = { shortestPath: [] }
}
render() {
const {
destinationLocationId,
locations,
originLocationId
} = this.props;
let path = []
if (destinationLocationId != null && originLocationId != null) {
console.log(JSON.stringify(locations))
path = [getNodeById(originLocationId).namne];
if (originLocationId != destinationLocationId) {
var openList = [];
let startNode = getNodeById(originLocationId);
let destinationNode = getNodeById(destinationLocationId)
if (startNode.connectedToIds.length > 0 && destinationNode.connectedToIds.length > 0) {
startNode.gcost = 0
startNode.heuristic = manhattanDistance(startNode, destinationNode)
startNode.fcost = startNode.gcost + startNode.heuristic;
openList.push(startNode); //starting with the startNode
while (openList.length > 0) {
var currentNode = getNodeOfMinFscore(openList); //get the node of the minimum f cost of all nodes in the openList
if (currentIsEqualDistanation(currentNode)) {
path = getPath(currentNode);
break;
}
deleteCurrentFromOpenList(currentNode, openList);
for (let neighbourId of currentNode.connectedToIds) {
var neighbourNode = getNodeById(neighbourId);
let gcost = currentNode.gcost + manhattanDistance(currentNode, neighbourNode);
if (gcost < (neighbourNode.gcost ?? Infinity)) {
neighbourNode.parentId = currentNode.id;
// keep track of the path
// total cost saved in neighbour.g
neighbourNode.gcost = gcost;
neighbourNode.heuristic = manhattanDistance(neighbourNode, destinationNode);
neighbourNode.fcost = neighbourNode.gcost + neighbourNode.heuristic; //calculate f cost of the neighbourNode
addNeighbourNodeToOpenList(neighbourNode, openList);
}
}
}
}
}
}
path = path.reverse().join("->");
function addNeighbourNodeToOpenList(neighbourNode, openList) {
//add neighbourNode to the open list to be discovered later
if (!openList.includes(neighbourNode)) {
openList.push(neighbourNode);
}
}
function deleteCurrentFromOpenList(currentNode, openList) {
const currIndex = openList.indexOf(currentNode);
openList.splice(currIndex, 1); //deleting currentNode from openList
}
function currentIsEqualDistanation(currentNode) {
//check if we reached out the distanation node
return (currentNode.id == destinationLocationId)
}
function getNodeById(id) {
var node;
for (let i = 0; i < locations.length; i++) {
if (locations[i].id == id) {
node = locations[i]
}
}
return node
}
function getPath(destinationNode) {
console.log("inside getpath")
var parentPath = []
var parent;
while (destinationNode.parentId != null) {
parentPath.push(destinationNode.name)
parent = destinationNode.parentId;
destinationNode = getNodeById(parent);
}
//adding startNode to the path
parentPath.push(getNodeById(originLocationId).name)
return parentPath;
}
function getNodeOfMinFscore(openList) {
var minFscore = openList[0].fcost; //initValue
var nodeOfminFscore;
for (let i = 0; i < openList.length; i++) {
if (openList[i].fcost <= minFscore) {
minFscore = openList[i].fcost //minFvalue
nodeOfminFscore = openList[i]
}
}
return nodeOfminFscore
}
//manhattan distance is for heuristic and gScore. Here I use Manhattan instead of Euclidean
//because in this example we dont have diagnosal path.
function manhattanDistance(startNode, destinationNode) {
var x = Math.abs(destinationNode.x - startNode.x);
var y = Math.abs(destinationNode.y - startNode.y);
var dist = x + y;
return dist;
}
return (
<div className="turn-by-turn-component">
<TodoList
title="Mandatory work"
list={[path]}
/>
<TodoList
title="Optional work"
list={[]}
/>
</div>
);
}
}
TurnByTurnComponent.propTypes = {
destinationLocationId: PropTypes.number,
locations: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
connectedToIds: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired
})),
originLocationId: PropTypes.number
};
There are a few issues in your code:
return originLocationId; should not happen. When the source is equal to the target, then set the path, and make sure the final return of this function is executed, as you want to return the div element.
When the target is found in the loop, not only should the path be built, but the loop should be exited. So add a break
currentNode.gcost = currentNode.gcost + manhattanDistance(currentNode, neighbourNode); is not right: you don't want to modify currentNode.gcost: its value should not depend on the outgoing edge to that neighbour. Instead store this sum in a temporary variable (like gcost)
The comparison with neighbourNode.gcost will not work when that node does not yet have a gcost member. I don't see in your code that it got a default value that makes sure this condition is true, so you should use a default value here, like (neighbourNode.gcost ?? Infinity)
path = path.reverse().join("->"); should better be executed always, also when there is no solution (so path is a string) or when the source and target are the same node.
Here is a corrected version, slightly adapted to run here as a runnable snippet:
function render() {
const {
destinationLocationId,
locations,
originLocationId
} = this.props;
let path = [];
if (destinationLocationId != null && originLocationId != null) {
path = [originLocationId]; // The value for when the next if condition is not true
if (originLocationId != destinationLocationId) {
var openList = [];
let startNode = getNodeById(originLocationId);
let destinationNode = getNodeById(destinationLocationId)
if (startNode.connectedToIds.length > 0 && destinationNode.connectedToIds.length > 0) {
startNode.gcost = 0
startNode.heuristic = manhattanDistance(startNode, destinationNode)
startNode.fcost = startNode.gcost + startNode.heuristic;
openList.push(startNode);
while (openList.length > 0) {
var currentNode = getNodeOfMinFscore(openList);
if (currentIsEqualDistanation(currentNode)) {
path = getPath(currentNode);
break; // Should end the search here!
}
deleteCurrentFromOpenList(currentNode, openList);
for (let neighbourId of currentNode.connectedToIds) {
var neighbourNode = getNodeById(neighbourId);
// Should not modify the current node's gcost. Use a variable instead:
let gcost = currentNode.gcost + manhattanDistance(currentNode, neighbourNode);
// Condition should also work when neighbour has no gcost yet:
if (gcost < (neighbourNode.gcost ?? Infinity)) {
neighbourNode.parentId = currentNode.id;
neighbourNode.gcost = gcost; // Use the variable
neighbourNode.heuristic = manhattanDistance(neighbourNode, destinationNode);
neighbourNode.fcost = neighbourNode.gcost + neighbourNode.heuristic;
addNeighbourNodeToOpenList(neighbourNode, openList);
}
}
}
}
}
}
// Convert the path to string in ALL cases:
path = path.reverse().join("->");
function addNeighbourNodeToOpenList(neighbourNode, openList) {
if (!openList.includes(neighbourNode)) {
openList.push(neighbourNode);
}
}
function deleteCurrentFromOpenList(currNode, openList) {
const currIndex = openList.indexOf(currNode);
openList.splice(currIndex, 1);
}
function currentIsEqualDistanation(currNode) {
return (currNode.id == destinationLocationId)
}
function getNodeById(nid) {
var node;
for (let i = 0; i < locations.length; i++) {
if (locations[i].id == nid) {
node = locations[i]
}
}
return node
}
function getPath(destNode) {
var parentPath = []
var parentId;
while (destNode.parentId != null) {
parentPath.push(destNode.name)
parentId = destNode.parentId;
destNode = getNodeById(parentId);
}
parentPath.push(getNodeById(originLocationId).name)
return parentPath;
}
function getNodeOfMinFscore(openList) {
var minFscore = openList[0].fcost;
var nodeOfminFscore;
for (let i = 0; i < openList.length; i++) {
if (openList[i].fcost <= minFscore) {
minFscore = openList[i].fcost
nodeOfminFscore = openList[i]
}
}
return nodeOfminFscore
}
function manhattanDistance(stNode, dstNode) {
var x = Math.abs(dstNode.x - stNode.x);
var y = Math.abs(dstNode.y - stNode.y);
var dist = x + y;
return dist;
}
return "Shortest path: " + path;
}
// Demo
let props = {
locations: [
{ id: 1, x: 312, y: 152, connectedToIds: [4,2], name: "Thetaham" },
{ id: 2, x: 590, y: 388, connectedToIds: [1,3], name: "Deltabury" },
{ id: 3, x: 428, y: 737, connectedToIds: [2], name: "Gammation" },
{ id: 4, x: 222, y: 430, connectedToIds: [1], name: "Theta City" },
],
originLocationId: 1,
destinationLocationId: 3,
};
console.log(render.call({props}));
I need to center the trace because on load the trace doesn't appears and the user should move the view.
I couldn't fin any documentation or question referencing this issue.
The data is retrieved from the server via service and
I'm using Angular 7 for the front-end and plotly to draw the plot.
When the page loads the plot looks like this: Image error.
If i move the view looks like this: Image okey.
Thanks you
Sample code:
private loadPlot(): void {
const minValues = [];
const maxValues = [];
const dataForPlot = [];
let traceIndex = 0;
for (const key in this.serverData.data) {
if (key === 'filename') {
continue;
}
const values = [];
const colorsForLine = [];
const markersForLine = [];
const mean = calculateMean(this.serverData.data[key]);
const textArray = [];
this.serverData.data[key].forEach(
(element, index) => {
let marker = 'circle';
const color = getPointColor(element['nc']);
const elementText = element['value'];
const value = element['value'];
if (index === this.serverData.data[key].length - 1) {
marker = 'diamond-cross';
}
values.push(value);
colorsForLine[index] = color;
markersForLine[index] = marker;
textArray[index] = elementText + '<br>' + truncateFilename(this.serverData.data['filename'][index], 50);
}
);
minValues.push(Math.min.apply(null, values.filter((n) => !isNaN(n))));
maxValues.push(Math.max.apply(null, values.filter((n) => !isNaN(n))));
const trace = {
x: this.serverData.dates,
y: values,
type: 'scatter',
mode: 'lines+markers',
marker: {
color: colorsForLine,
symbol: markersForLine,
size: 5
},
line: {
// color: colorsForLine,
},
connectgaps: false,
name: key,
description: 'number of ' + key,
filenames: this.serverData.data['filename'],
hoverinfo: 'x+text',
hovertext: textArray
};
dataForPlot.push(trace);
traceIndex++;
}
let MINVALUEFORPLOT;
let MAXVALUEFORPLOT;
if (this.plotThreshold === undefined) {
MINVALUEFORPLOT = Math.min.apply(null, minValues) - Math.abs((Math.min.apply(null, minValues) * 0.1));
MAXVALUEFORPLOT = Math.max.apply(null, maxValues) + (Math.max.apply(null, maxValues) * 0.1);
} else {
const height = (this.layoutShapes[this.layoutShapes.length - 1]['y0'] - this.layoutShapes[this.layoutShapes.length - 1]['y1']) * 0.3;
MINVALUEFORPLOT = this.layoutShapes[this.layoutShapes.length - 1]['y1'] - height;
MAXVALUEFORPLOT = this.layoutShapes[this.layoutShapes.length - 1]['y0'] + height;
}
this.layout = {
// title: this.chart.name,
title: this.generatePlotTitle(),
shapes: [],
colorway: traceColor.colorRange,
hovermode: 'closest',
xaxis: {
nticks: 10,
},
yaxis: {
type: 'linear',
range: [MINVALUEFORPLOT, MAXVALUEFORPLOT]
},
currentDiv: 'plot'
};
this.layout.shapes = this.layoutShapes;
Plotly.react('plot', dataForPlot, this.layout);
}
I have coded a neural network in JavaScript and implemented the Backpropagation algorithm described here.
Here is the code (typescript):
/**
* Net
*/
export class Net {
private layers: Layer[] = [];
private inputLayer: Layer;
private outputLayer: Layer;
public error: number = Infinity;
private eta: number = 0.15;
private alpha: number = 0.5;
constructor(...topology: number[]) {
topology.forEach((topologyLayer, iTL) => {
var nextLayerNeuronNumber = topology[iTL + 1] || 0;
this.layers.push(new Layer(topologyLayer, nextLayerNeuronNumber));
});
this.inputLayer = this.layers[0];
this.outputLayer = this.layers[this.layers.length - 1];
}
public loadWeights(weights) {
/*
[
[Layer
[Node weights, ..., ...]
]
]
*/
for (var iL = 0; iL < weights.length; iL++) {
var neuronWeights = weights[iL];
var layer = this.layers[iL];
for (var iN = 0; iN < neuronWeights.length; iN++) {
// Neuron
var connections = neuronWeights[iN];
for (var iC = 0; iC < connections.length; iC++) {
var connection = connections[iC];
this.layer(iL).neuron(iN).setWeights(iC, connection);
}
}
}
}
public train(data: number[][], iterartions = 2000) {
var inputs = this.inputLayer.neurons.length - 1;
for (var ite = 0; ite < iterartions; ite++) {
data.forEach(node => {
var inputData = [];
var outputData = [];
for (var i = 0; i < node.length; i++) {
if (i < inputs) {
inputData.push(node[i])
} else {
outputData.push(node[i])
}
}
this.feedForward(...inputData);
this.backProb(...outputData);
});
}
return this.calcDataError(data);
}
private calcDataError(data){
var overallDataErrorSum = 0;
var inputs = this.inputLayer.neurons.length - 1;
data.forEach(node => {
var outputData = node.splice(inputs);
var inputData = node;
this.feedForward(...inputData);
overallDataErrorSum += this.getNetError(outputData);
});
overallDataErrorSum /= data.length;
return overallDataErrorSum;
}
public saveWeights() {
// Ignore output layer
var ret = []
for (var iL = 0; iL < this.layers.length - 1; iL++) {
var layer = this.layers[iL];
var layer_ret = [];
layer.neurons.forEach(neuron => {
layer_ret.push(neuron.connections.map(c => c.weight));
});
ret.push(layer_ret);
}
return ret;
}
feedForward(...inputs: number[]) {
if (inputs.length != this.inputLayer.neurons.length - 1) return false;
this.inputLayer.neurons.forEach((neuron, i) => {
if (!neuron.isBias) {
neuron.output(inputs[i]);
}
});
this.layers.forEach((layer, i) => {
// Skip Input Layer
if (i > 0) {
var prevLayer = this.layers[i - 1]
layer.neurons.forEach(neuron => {
neuron.calcOutput(prevLayer);
});
}
});
}
public getNetError(targetVals) {
// Calc delta error of outputs
var deltas = [];
this.outputLayer.neurons.forEach((neuron, iN) => {
if (!neuron.isBias) {
neuron.calcOutputDelta(targetVals[iN]);
deltas.push(neuron.delta);
}
});
deltas = deltas.map(d => Math.pow(d, 2));
var sum = 0;
deltas.forEach(d => sum += d);
return sum / deltas.length;
}
backProb(...targetVals: number[]) {
// Calc delta error of outputs
this.outputLayer.neurons.forEach((neuron, iN) => {
if (!neuron.isBias) {
neuron.calcOutputDelta(targetVals[iN]);
}
});
// Backprop delta error through hidden layers
for (var iL = this.layers.length - 2; iL > 0; iL--) {
var layer = this.layers[iL];
var nextLayer = this.layers[iL + 1]
layer.neurons.forEach(neuron => {
neuron.calcHiddenDelta(nextLayer);
});
}
// Update weights
for (var iL = 1; iL < this.layers.length; iL++) {
var layer = this.layers[iL];
var prevLayer = this.layers[iL - 1];
layer.neurons.forEach(neuron => {
if (!neuron.isBias) {
neuron.updateWeights(prevLayer, this.eta);
}
});
}
this.error = this.getNetError(targetVals);
return this.error;
}
getOutputs(...inputs: number[]) {
var ret = [];
this.outputLayer.neurons.forEach(neuron => {
if (!neuron.isBias) {
ret.push(neuron.output())
}
});
return ret;
}
getResults(...inputs: number[]) {
this.feedForward(...inputs)
return this.getOutputs();
}
layer(i) {
return this.layers[i];
}
}
/**
* Layer
*/
class Layer {
public neurons: Neuron[] = [];
constructor(neuronNumber: number, nextLayerNeuronNumber: number) {
for (var iN = 0; iN < neuronNumber + 1; iN++) {
// +1 for bias neuron, which is last
if (iN < neuronNumber) {
// Create normal neuron
this.neurons.push(new Neuron(nextLayerNeuronNumber, iN, false));
} else {
this.neurons.push(new Neuron(nextLayerNeuronNumber, iN, true));
}
}
}
neuron(i) {
return this.neurons[i];
}
bias() {
return this.neurons[this.neurons.length - 1];
}
}
/**
* Neuron
*/
class Neuron {
public connections: Connection[] = [];
private outputVal: number;
public delta: number;
constructor(outputsTo: number, private index, public isBias = false) {
// Creates connections
for (var c = 0; c < outputsTo; c++) {
this.connections.push(new Connection());
}
this.outputVal = isBias ? 1 : 0;
}
calcOutput(prevLayer: Layer) {
// Only calcOutput when neuron is not a bias neuron
if (!this.isBias) {
var sum = 0;
prevLayer.neurons.forEach(prevLayerNeuron => {
sum += prevLayerNeuron.output() * prevLayerNeuron.getWeights(this.index).weight;
});
this.output(this.activationFunction(sum));
}
}
private activationFunction(x) {
//return Math.tanh(x);
return 1 / (1 + Math.exp(-x))
//return x;
};
private activationFunctionDerivative(x) {
// Small approximation of tanh derivative
//return 1 - x * x
// Sigmoid
var s = this.activationFunction(x);
return s * (1 - s);
// With general derivative formula where h = 1e-10
/*var h = 0.0001;
var dx = ((this.activationFunction(x + h) - this.activationFunction(x))/h)
return dx;*/
//return 1
};
// Backprop // Todo // Understand
public calcOutputDelta(targetVal) {
// Bias output neurons do not have delta error
if (!this.isBias) {
this.delta = targetVal - this.output();
}
}
public calcHiddenDelta(nextLayer: Layer) {
var sum = 0;
// Go through all neurons of next layer excluding bias
nextLayer.neurons.forEach((neuron, iN) => {
if (!neuron.isBias) {
sum += neuron.delta * this.getWeights(iN).weight;
}
});
this.delta = sum;
}
public updateWeights(prevLayer: Layer, eta: number) {
prevLayer.neurons.forEach((neuron, iN) => {
var weight = neuron.getWeights(this.index).weight;
var newWeight =
weight + // old weight
eta * // learning weight
this.delta * // delta error
this.activationFunctionDerivative(neuron.output())
neuron.getWeights(this.index).weight = newWeight;
});
}
// Backprop end
output(s?) {
if (s && !this.isBias) {
this.outputVal = s;
return this.outputVal;
} else {
return this.outputVal;
}
}
getWeights(i) {
return this.connections[i];
}
setWeights(i, s) {
return this.connections[i].weight = s;
}
}
/**
* Connection
*/
class Connection {
public weight: number;
public deltaWeight: number;
constructor() {
this.weight = Math.random();
this.deltaWeight = 0;
}
}
When training it for just one set of data, it works just fine. (example from here)
import {Net} from './ml';
var myNet = new Net(2, 2, 2);
var weights = [
[
[0.15, 0.25],
[0.20, 0.30],
[0.35, 0.35]
],
[
[0.40, 0.50],
[0.45, 0.55],
[0.60, 0.60]
]
];
// Just loads the weights given in the example
myNet.loadWeights(weights)
var error = myNet.train([[0.05, 0.10, 0.01, 0.99]]);
console.log('Error: ', error);
console.log(myNet.getResults(0.05, 0.10));
Console prints:
Error: 0.0000020735174706210714
[ 0.011556397089327321, 0.9886867357304885 ]
Basically, that's pretty good, right?
Then, I wanted to teach the network the XOR problem:
import {Net} from './ml';
var myNet = new Net(2, 3, 1);
var trainigData = [
[0, 0, 0],
[1, 0, 1],
[0, 1, 1],
[1, 1, 0]
]
var error = myNet.train(trainigData)
console.log('Error: ', error);
console.log('Input: 0, 0: ', myNet.getResults(0, 0));
console.log('Input: 1, 0: ', myNet.getResults(1, 0));
Here the network fails:
Error: 0.2500007370167383
Input: 0, 0: [ 0.5008584967899313 ]
Input: 1, 0: [ 0.5008584967899313 ]
What am I doing wrong?
Firstly perform gradient checks on the entire batch (meaining on the function calculating gradients on the batch), if you have not done so already. This will ensure you know what the problem is.
If gradients are not correctly computed, taking into account that your implementation works on single data sets, you are most likely mixing some values in the backwards pass.
If gradients are correctly computed, there is an error in your update function.
A working implementation of backpropagation for neural networks in javaScript can be found here
Here is the code snippet of the trainStep function using backpropagation
function trainStepBatch(details){
//we compute forward pass
//for each training sample in the batch
//and stored in the batch array
var batch=[];
var ks=[];
for(var a=0;a<details.data.in.length;a++){
var results=[];
var k=1;
results[0]={output:details.data.in[a]};
for(var i=1;i<this.layers.length;i++){
results[i]=layers[this.layers[i].type].evalForGrad(this.layers[i],results[i-1].output);
k++;
}
batch[a]=results;
ks[a]=k;
}
//We compute the backward pass
//first derivative of the cost function given the output
var grad=[];
for(i in batch)grad[i]={grad:costs[details.cost].df(batch[i][ks[i]-1].output,details.data.out[i])};
//for each layer we compute the backwards pass
//on the results of all forward passes at a given layer
for(var i=this.layers.length-1;i>0;i--){
var grads=[];
var test=true;
for(a in batch){
grads[a]=layers[this.layers[i].type].grad(this.layers[i],batch[a][i],batch[a][i-1],grad[a]);
if(grads[a]==null)test=false;
else grads[a].layer=i;
}
//we perform the update
if(test)stepBatch(this.layers[i].par,grads,details.stepSize);
}
}
And for the stepBatch function
function stepBatch(params,grads, stepSize){
for(i in params.w){
for(j in params.w[i]){
for(a in grads){
params.w[i][j]-=stepSize*grads[a].dw[i][j];
}
}
}
for(i in params.b){
for(a in grads){
params[a]-=stepSize*grads[a].db[i];
}
}
function stepBatch(params,grads, stepSize){
for(i in params.w){
for(j in params.w[i]){
for(a in grads){
params.w[i][j]-=stepSize*grads[a].dw[i][j];
}
}
}
for(i in params.b){
for(a in grads){
params[a]-=stepSize*grads[a].db[i];
}
}
}
I'm working with the chartJS library and trying to figure out what I need to do to get a single lines data to display in the tooltip.
For example,
I am hovering over the blue line here and see every data point at that mark. What I would like to do is see all three data points for the blue line only.
I've made some progress from chart js tooltip how to control the data that show
getPointsAtEvent: function(e) {
var pointsArray = [], eventPosition = helpers.getRelativePosition(e);
var breakLoop = 0;
helpers.each(this.datasets, function(dataset) {
helpers.each(dataset.points, function(point) {
if (point.inRange(eventPosition.x, eventPosition.y) && point.showTooltip && !point.ignore) {
if(eventPosition.y + 2 >= point.y && eventPosition.y - 2 <= point.y) {
pointsArray.push(point);
breakLoop = 1;
return false;
}
}
});
if(breakLoop) {
return false;
}
}, this);
//console.log(pointsArray);
return pointsArray;
},
Is my chart modification that will return 1 data point on the graph. I'm assuming the next step is to overwrite the showToolTip method.
If this is the only chart you have (i.e. because the following code changes some of the global chart.js elements), you can use the following bit of code
var originalMultiTooltip = Chart.MultiTooltip;
Chart.MultiTooltip = function () {
var argument = arguments[0];
// locate the series using the active point
var activeDatasetLabel = myChart.activeElements[0].datasetLabel;
myChart.datasets.forEach(function (dataset) {
if (dataset.label === activeDatasetLabel) {
// swap out the labels and colors in arguments
argument.labels = dataset.points.map(function (point) { return point.value; });
argument.legendColors = dataset.points.map(function (point) {
return {
fill: point._saved.fillColor || point.fillColor,
stroke: point._saved.strokeColor || point.strokeColor
};
});
argument.title = activeDatasetLabel;
// position it near the active point
argument.y = myChart.activeElements[0].y;
}
})
return new originalMultiTooltip(arguments[0]);
}
// this distance function returns the square of the distance if within detection range, otherwise it returns Infinity
var distance = function (chartX, chartY) {
var hitDetectionRange = this.hitDetectionRadius + this.radius;
var distance = Math.pow(chartX - this.x, 2) + Math.pow(chartY - this.y, 2);
return (distance < Math.pow(hitDetectionRange, 2)) ? distance : Infinity;
}
myChart.getPointsAtEvent = function (e) {
var pointsArray = [],
eventPosition = Chart.helpers.getRelativePosition(e);
var leastDistance = Infinity;
Chart.helpers.each(myChart.datasets, function (dataset) {
Chart.helpers.each(dataset.points, function (point) {
// our active point is the one closest to the hover event
var pointDistance = distance.call(point, eventPosition.x, eventPosition.y)
if (isFinite(pointDistance) && pointDistance < leastDistance) {
leastDistance = pointDistance;
pointsArray = [ point ];
}
});
}, myChart);
return pointsArray;
}
It does 2 things
Replaces the getPointsAtEvent to just pick one point
Wraps the MultiTooltip constructor to swap out the list of values passed with all the values from the active point's series.
Fiddle - http://jsfiddle.net/h93pyavk/
If you extend the line chart, use the code I have above, and the code I pasted below you can get the desired effect to some degree.
showTooltip: function(ChartElements, forceRedraw) { //custom edit
//we will get value from ChartElements (which should be only 1 element long in this case) and use it to match the line row we want to see.
try {
var numMatch = ChartElements[0].value;
}
catch(err) {
var isChanged = (function(Elements) {
var changed = true;
return changed;
}).call(this, ChartElements);
}
// Only redraw the chart if we've actually changed what we're hovering on.
if (typeof this.activeElements === 'undefined') this.activeElements = [];
var isChanged = (function(Elements) {
var changed = false;
if (Elements.length !== this.activeElements.length) {
changed = true;
return changed;
}
helpers.each(Elements, function(element, index) {
if (element !== this.activeElements[index]) {
changed = true;
}
}, this);
return changed;
}).call(this, ChartElements);
if (!isChanged && !forceRedraw) {
return;
} else {
this.activeElements = ChartElements;
}
this.draw();
if (this.options.customTooltips) {
this.options.customTooltips(false);
}
if (ChartElements.length > 0) {
// If we have multiple datasets, show a MultiTooltip for all of the data points at that index
if (this.datasets && this.datasets.length > 1) {
var dataArray,
dataIndex;
for (var i = this.datasets.length - 1; i >= 0; i--) {
dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments;
dataIndex = helpers.indexOf(dataArray, ChartElements[0]);
if (dataIndex !== -1) {
break;
}
}
var eleLast = "";
var eleFirst = "";
var tooltipLabels = [],
tooltipColors = [],
medianPosition = (function(index) {
// Get all the points at that particular index
var Elements = [],
dataCollection,
xPositions = [],
yPositions = [],
xMax,
yMax,
xMin,
yMin;
helpers.each(this.datasets, function(dataset) {
dataCollection = dataset.points || dataset.bars || dataset.segments;
//console.log(dataset);
for(i = 0; i < dataset.points.length; i++) {
if(dataset.points[i].value === numMatch) {
for(var k = 0; k < dataset.points.length; k++) {
Elements.push(dataset.points[k]);
}
}
}
});
//save elements last label string
eleLast = Elements[Elements.length-1].label;
eleFirst = Elements[0].label;
//console.log(Elements);
helpers.each(Elements, function(element) {
if(element.value === numMatch) {
xPositions.push(element.x);
yPositions.push(element.y);
}
//Include any colour information about the element
tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element));
tooltipColors.push({
fill: element._saved.fillColor || element.fillColor,
stroke: element._saved.strokeColor || element.strokeColor
});
}, this);
yMin = helpers.min(yPositions);
yMax = helpers.max(yPositions);
xMin = helpers.min(xPositions);
xMax = helpers.max(xPositions);
return {
x: (xMin > this.chart.width / 2) ? xMin : xMax,
y: (yMin + yMax) / 2
};
}).call(this, dataIndex);
var newLabel = eleFirst + " to " + eleLast;
new Chart.MultiTooltip({
x: medianPosition.x,
y: medianPosition.y,
xPadding: this.options.tooltipXPadding,
yPadding: this.options.tooltipYPadding,
xOffset: this.options.tooltipXOffset,
fillColor: this.options.tooltipFillColor,
textColor: this.options.tooltipFontColor,
fontFamily: this.options.tooltipFontFamily,
fontStyle: this.options.tooltipFontStyle,
fontSize: this.options.tooltipFontSize,
titleTextColor: this.options.tooltipTitleFontColor,
titleFontFamily: this.options.tooltipTitleFontFamily,
titleFontStyle: this.options.tooltipTitleFontStyle,
titleFontSize: this.options.tooltipTitleFontSize,
cornerRadius: this.options.tooltipCornerRadius,
labels: tooltipLabels,
legendColors: tooltipColors,
legendColorBackground: this.options.multiTooltipKeyBackground,
title: newLabel,
chart: this.chart,
ctx: this.chart.ctx,
custom: this.options.customTooltips
}).draw();
} else {
helpers.each(ChartElements, function(Element) {
var tooltipPosition = Element.tooltipPosition();
new Chart.Tooltip({
x: Math.round(tooltipPosition.x),
y: Math.round(tooltipPosition.y),
xPadding: this.options.tooltipXPadding,
yPadding: this.options.tooltipYPadding,
fillColor: this.options.tooltipFillColor,
textColor: this.options.tooltipFontColor,
fontFamily: this.options.tooltipFontFamily,
fontStyle: this.options.tooltipFontStyle,
fontSize: this.options.tooltipFontSize,
caretHeight: this.options.tooltipCaretSize,
cornerRadius: this.options.tooltipCornerRadius,
text: helpers.template(this.options.tooltipTemplate, Element),
chart: this.chart,
custom: this.options.customTooltips
}).draw();
}, this);
}
}
return this;
},
Obviously this is just a quick and dirty fix, if I get more time to work on it I would like to have each data point show its corresponding value above it.