I would like to create dynamically changing graph with timeline in javascript, That would look something like this.
Edit: I would like to decide by myself which node should be in which time slice.
I wonder, is there a library that I can use to do this, or I need to create it by myself ( by simply drawing on canvas )? I tried to find one, however it seems that there are many implementations of timelines and of graphs but the combination of those two is hard to find. The most suitable solution was using gojs. However I can't create a node with two parents in it because it is implemented as a tree data structure.
You may have to play around with the maths, but I hope this will be useful as a starting point:
DEMO: JSFiddle
HTML
<div id='board'>
<div id='titles'></div>
</div>
CSS
#board {
position: relative;
width: 500px;
height: 600px;
background-color:#f83213;
}
#titles {
color: #ffffff;
width: 100%;
height: 18px;
font-size: 12px;
}
#titles div {
display:inline-block;
margin: 10px;
}
.event{
border: 0px;
background-color: #3a2356;
color: #ffffff;
width: 18px;
height: 18px;
position: absolute;
padding: 4px;
font-size: 18px;
z-index: 2;
}
.line{
height: 1px;
width: 60px;
background-color: #3a2356;
position: absolute;
z-index: 1;
}
** JavaScript**
var margin = 20;
var events = {
"A": {
day: 0,
indexInDay: 0,
lineTos: ["D"]
},
"B": {
day: 0,
indexInDay: 1,
lineTos: ["D"]
},
"D": {
day: 1,
indexInDay: 0,
lineTos: ["E","F"]
},
"E": {
day: 2,
indexInDay: 0,
lineTos: null
},
"C": {
day: 0,
indexInDay: 2,
lineTos: ["F"]
},
"F": {
day: 2,
indexInDay: 2,
lineTos: null
},
};
drawAll(events);
function drawAll(events) {
drawTitles(events);
drawEvents(events);
drawLines(events);
}
function drawTitles(events) {
var titles = document.getElementById('titles');
var max = 0;
for (var name in events) {
if (events[name].day > max)
max = events[name].day;
}
for (var i = 0 ; i <= max ; i++)
titles.innerHTML += '<div>' + 'Day' + i + '</div>';
}
function drawEvents(events) {
var board = document.getElementById('board');
for (var name in events) {
var ev = events[name];
var eventDiv = document.createElement('DIV');
board.appendChild(eventDiv);
eventDiv.className = 'event';
setTopLeftEvent(ev, eventDiv);
eventDiv.innerText = name;
}
}
function drawLines(events) {
var board = document.getElementById('board');
for (var name in events) {
var from = events[name];
var tos = from.lineTos;
if (!tos) continue;
for (var j = 0 ; j < tos.length ; j++) {
var to = events[tos[j]];
var lineDiv = document.createElement('DIV');
board.appendChild(lineDiv);
lineDiv.className = 'line';
setTopLeftLine(from, lineDiv);
lineDiv.style.width = margin + 1 * margin * distance(to.indexInDay,from.indexInDay,to.day, from.day) + 'px';
var tan = (to.indexInDay - from.indexInDay) / (to.day - from.day);
lineDiv.style.top = lineDiv.offsetTop + (tan * margin) +'px';
var angle = Math.atan(tan) * 180/Math.PI;
// Code for Safari
lineDiv.style.WebkitTransform = "rotate(" + angle + "deg)";
// Code for IE9
lineDiv.style.msTransform = "rotate(" + angle + "deg)";
// Standard syntax
lineDiv.style.transform = "rotate(" + angle + "deg)";
}
}
}
function distance(x1, y1, x2, y2){
var res = Math.sqrt((y2-y1)*(y2-y1) + (x2-x1)*(x2-x1));
return res;
}
function setTopLeftEvent(event, eventDiv) {
eventDiv.style.left = (margin + event.day * (margin * 2)) + 'px';
eventDiv.style.top = (margin * 2 + event.indexInDay * (margin * 2)) + 'px';
}
function setTopLeftLine(event, lineDiv) {
lineDiv.style.left = (margin + event.day * (margin * 2)) + 'px';
lineDiv.style.top = (margin * 2.5 + event.indexInDay * (margin * 2)) + 'px';
}
As that GoJS sample mentions in the text, it is easy to replace the TreeLayout with a LayeredDigraphLayout and the TreeModel with a GraphLinksModel. Here's what I just did to modify the sample.
Replace go.TreeLayout with go.LayeredDigraphLayout, so that the custom layout no longer inherits from TreeLayout. Change the constructor not to bother setting TreeLayout specific properties. Change the diagram's layout to use LayeredDigraphLayout specific properties:
layout: $(LayeredTreeLayout, // custom layout is defined above
{
angle: HORIZONTAL ? 0 : 90,
columnSpacing: 5,
layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource
}),
Replace that sample's model with a GraphLinksModel holding the data that you want:
// define the node data
var nodearray = [
{ // this is the information needed for the headers of the bands
key: "_BANDS",
category: "Bands",
itemArray: [
{ text: "Day 0" },
{ text: "Day 1" },
{ text: "Day 2" },
{ text: "Day 3" },
{ text: "Day 4" },
{ text: "Day 5" }
]
}
];
var linkarray = [
{ from: "A", to: "D" },
{ from: "B", to: "D" },
{ from: "D", to: "E" },
{ from: "D", to: "F" },
{ from: "C", to: "F" }
];
myDiagram.model = $(go.GraphLinksModel,
{ // automatically create node data objects for each "from" or "to" reference
// (set this property before setting the linkDataArray)
archetypeNodeData: {},
nodeDataArray: nodearray,
linkDataArray: linkarray
});
Without having changed any of the templates or the styling, the result is:
Just to make sure it works, I also tried setting HORIZONTAL = false:
Related
I've been working on a side project to recreate diablo / tarkov like inventory screens with pure html, css, and js. I'm attempting to use the draggable api's built into html for this but am hitting a blocker. Everything working fine for 1x1 wide/high cell's / items. The problem begins when trying to add an item that is longer in width and/or height. I can still drag around the item but I can't get it to do two things.
Cover all the cells correctly so there is no bleed through from the cells background.
Stop the item from being placed in a cell that would cover another item.
My ideal solution modifies either the linked codepen below or provides direction / a solution on a more appropriate approach whatever that may be. The only constraint is that it must use html, css, and javascript. The only exception is jQuery.
The Code
var draggedItem = null;
function Inventory(options) {
// Setup Rows and Items
this.init = function (options) {
let slotSizeW = options.size.w / options.slots.w;
let slotSizeH = options.size.h / options.slots.h;
this.html = { inventory: options.selector };
// Build grid
let grid = document.createElement("grid");
grid.style.width = options.size.w + "px";
grid.style.height = options.size.h + "px";
grid.style.gridTemplateColumns = "1fr ".repeat(options.slots.w);
grid.style.gridTemplateRows = "1fr".repeat(options.slots.h);
grid.cells = [];
for (var i = 0; i < options.slots.w * options.slots.h; i++) {
var cell = document.createElement("cell");
cell.style.width = slotSizeW + "px";
cell.style.height = slotSizeH + "px";
grid.appendChild(cell);
grid.cells.push(cell);
}
this.html.grid = grid;
this.html.inventory.appendChild(this.html.grid);
// Add all items
let items = [];
options.items.forEach(function (item, index, array) {
items.push(item);
let itemEl = document.createElement("item");
itemEl.setAttribute("draggable", true);
itemEl.style.background = item.color;
itemEl.style.width = 100 * item.w + "%";
itemEl.style.height = 100 * item.h + "%";
grid.cells[options.slots.w * item.x + item.y].appendChild(itemEl);
});
this.html.items = items;
let itemTags = document.querySelectorAll("item");
for (let i = 0; i < itemTags.length; i++) {
itemTags[i].addEventListener("dragstart", this.dragStart);
itemTags[i].addEventListener("dragend", this.dragEnd);
}
let cellTags = document.querySelectorAll("cell");
for (let i = 0; i < cellTags.length; i++) {
cellTags[i].addEventListener("dragover", this.dragOver);
cellTags[i].addEventListener("dragenter", this.dragEnter);
cellTags[i].addEventListener("dragleave", this.dragLeave);
cellTags[i].addEventListener("drop", this.dragDrop);
}
};
this.dragStart = function () {
this.classList.toggle("hold");
window.requestAnimationFrame(() => this.classList.toggle("invisible"));
draggedItem = this;
};
this.dragEnd = function () {
this.classList.toggle("hold");
this.classList.toggle("invisible");
};
this.dragOver = function (e) {
e.preventDefault();
};
this.dragEnter = function (e) {
e.preventDefault();
this.classList.toggle("hovered");
};
this.dragLeave = function (e) {
this.classList.remove("hovered");
};
this.dragDrop = function (e) {
this.classList.remove("hovered");
if (draggedItem != null) {
draggedItem.parentElement.removeChild(draggedItem);
this.appendChild(draggedItem);
}
};
this.init(options);
}
var inventory = new Inventory({
selector: document.querySelector("inventory"),
size: { w: 300, h: 300 },
slots: { w: 4, h: 4 },
items: [
{ x: 0, y: 0, w: 2, h: 2, content: "2x2", color: "#ffd54f" },
{ x: 2, y: 0, w: 1, h: 2, content: "1x2", color: "#66bb6a" },
{ x: 3, y: 3, w: 1, h: 1, content: "1x1", color: "#e53935" }
]
});
html,
body {
background: darksalmon;
}
grid {
position: relative;
display: grid;
}
cell {
position: relative;
border: 3px salmon solid;
background-color: white;
}
item {
display: block;
position: relative;
color: white;
cursor: pointer;
z-index: 2;
}
.hold {
border: solid #ccc 4px;
}
.hovered {
background: #f4f4f4;
border-style: dashed;
}
.invisible {
display: none;
}
<container>
<inventory>
</inventory>
</container>
To get each item's color to fill the underlying cells you have to take into account the extra width of each cell created by its border.
This snippet sets the width and height of an item using this formula:
itemEl.style.width = "calc((100% + 6px) * " + item.w + " - 4.9px)";
itemEl.style.height = "calc((100% + 6px) * " + item.h + " - 4.9px)";
This says add on an allowance for all 4 borders but remove the width of the outer borders. However, there can be a 'quirk' where the calculation the system has to do to map CSS pixels to screen pixels (several screen pixels may be used for one CSS pixel in modern displays) results in the odd screen pixel being left behind - in this case resulting in a thin white line around the item's color.
The item is made slightly wider (by just over 1 CSS pixel) and is placed half a pixel up and left which, at least in the tests I did with Edge/Chrome Windows10 removed this white 'border' at all zoom levels.
var draggedItem = null;
function Inventory(options) {
// Setup Rows and Items
this.init = function (options) {
let slotSizeW = options.size.w / options.slots.w;
let slotSizeH = options.size.h / options.slots.h;
this.html = { inventory: options.selector };
// Build grid
let grid = document.createElement("grid");
grid.style.width = options.size.w + "px";
grid.style.height = options.size.h + "px";
grid.style.gridTemplateColumns = "1fr ".repeat(options.slots.w);
grid.style.gridTemplateRows = "1fr".repeat(options.slots.h);
grid.cells = [];
for (var i = 0; i < options.slots.w * options.slots.h; i++) {
var cell = document.createElement("cell");
cell.style.width = slotSizeW + "px";
cell.style.height = slotSizeH + "px";
grid.appendChild(cell);
grid.cells.push(cell);
}
this.html.grid = grid;
this.html.inventory.appendChild(this.html.grid);
// Add all items
let items = [];
options.items.forEach(function (item, index, array) {
items.push(item);
let itemEl = document.createElement("item");
itemEl.setAttribute("draggable", true);
itemEl.style.background = item.color;
//itemEl.style.width = 100 * item.w + "%";
//itemEl.style.height = 100 * item.h + "%";
itemEl.style.width = "calc((100% + 6px) * " + item.w + " - 4.9px)";
itemEl.style.height = "calc((100% + 6px) * " + item.h + " - 4.9px)";
grid.cells[options.slots.w * item.x + item.y].appendChild(itemEl);
});
this.html.items = items;
let itemTags = document.querySelectorAll("item");
for (let i = 0; i < itemTags.length; i++) {
itemTags[i].addEventListener("dragstart", this.dragStart);
itemTags[i].addEventListener("dragend", this.dragEnd);
}
let cellTags = document.querySelectorAll("cell");
for (let i = 0; i < cellTags.length; i++) {
cellTags[i].addEventListener("dragover", this.dragOver);
cellTags[i].addEventListener("dragenter", this.dragEnter);
cellTags[i].addEventListener("dragleave", this.dragLeave);
cellTags[i].addEventListener("drop", this.dragDrop);
}
};
this.dragStart = function () {
this.classList.toggle("hold");
window.requestAnimationFrame(() => this.classList.toggle("invisible"));
draggedItem = this;
};
this.dragEnd = function () {
this.classList.toggle("hold");
this.classList.toggle("invisible");
};
this.dragOver = function (e) {
e.preventDefault();
};
this.dragEnter = function (e) {
e.preventDefault();
this.classList.toggle("hovered");
};
this.dragLeave = function (e) {
this.classList.remove("hovered");
};
this.dragDrop = function (e) {
this.classList.remove("hovered");
if (draggedItem != null) {
draggedItem.parentElement.removeChild(draggedItem);
this.appendChild(draggedItem);
}
};
this.init(options);
}
var inventory = new Inventory({
selector: document.querySelector("inventory"),
size: { w: 300, h: 300 },
slots: { w: 4, h: 4 },
items: [
{ x: 0, y: 0, w: 2, h: 2, content: "2x2", color: "#ffd54f" },
{ x: 2, y: 0, w: 1, h: 2, content: "1x2", color: "#66bb6a" },
{ x: 3, y: 3, w: 1, h: 1, content: "1x1", color: "#e53935" }
]
});
html,
body {
background: darksalmon;
}
grid {
position: relative;
display: grid;
}
cell {
position: relative;
border: 3px salmon solid;
background-color: white;
}
item {
display: block;
position: relative;
color: white;
cursor: pointer;
z-index: 2;
top: -0.5px;
left: -0.5px;
}
.hold {
border: solid #ccc 4px;
}
.hovered {
background: #f4f4f4;
border-style: dashed;
}
.invisible {
display: none;
}
<container>
<inventory>
</inventory>
</container>
For the second part of the question on a drop you will have to loop through the items to see if any overlaps with the dragged item and if it doesn't then you can remove the dragged item from its current cell and append it to its new one.
I'm using the following options in Vis JS Timeline to produce a horizontal axis at the top of the timeline with time labels:
orientation: {
axis: 'both'
},
The horizontal axis looks like this:
My timeline has many rows, so the user needs to vertically scroll down the page to see everything. The problem is that the horizontal axis at the top does not stay in view when scrolling down the page.
Question: How can I freeze the horizontal axis at the top so that the time labels stay in view when scrolling down?
The following code snippet, or jsfiddle.net/nj1647tb, is my timeline:
const seed = '11';
Math.seedrandom(seed);
const nGroups = 40;
const maxSubGroups = 2;
const maxItemsPerSubGroup = 1;
const metaEventCount = 2;
const itemLengthScale = 200;
let now = moment().minutes(0).seconds(0).milliseconds(0);
var groupCount = 12;
var itemCount = 70;
var tcCrashProbability = 0.2;
function randInt(min, max) {
return Math.round(min + Math.random() * (max - min));
}
function getStartEnd(earliestStart) {
if (earliestStart === undefined) {
earliestStart = 0;
}
let startAdd = earliestStart + Math.random() * 200;
let length = Math.random() * itemLengthScale;
let endAdd = startAdd + length;
return {
startAdd: startAdd,
endAdd: endAdd
}
}
const stackTrace = `Traceback (most recent call last):
File "/usr/local/lib/python3.6/site-packages/requests/adapters.py", line 449, in send
timeout=timeout
File "/usr/local/lib/python3.6/site-packages/urllib3/connectionpool.py", line 756, in urlopen
method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]
File "/usr/local/lib/python3.6/site-packages/urllib3/util/retry.py", line 532, in increment
raise six.reraise(type(error), error, _stacktrace)
File "/usr/local/lib/python3.6/site-packages/urllib3/packages/six.py", line 769, in reraise
raise value.with_traceback(tb)
File "/usr/local/lib/python3.6/site-packages/urllib3/connectionpool.py", line 706, in urlopen
chunked=chunked,
File "/usr/local/lib/python3.6/site-packages/urllib3/connectionpool.py", line 445, in _make_request
six.raise_from(e, None)
File "<string>", line 3, in raise_from
File "/usr/local/lib/python3.6/site-packages/urllib3/connectionpool.py", line 440, in _make_request
httplib_response = conn.getresponse()
File "/usr/lib64/python3.6/http/client.py", line 1346, in getresponse
response.begin()
File "/usr/lib64/python3.6/http/client.py", line 307, in begin
version, status, reason = self._read_status()
File "/usr/lib64/python3.6/http/client.py", line 268, in _read_status
line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
File "/usr/lib64/python3.6/socket.py", line 586, in readinto
return self._sock.recv_into(b)
File "/usr/lib64/python3.6/ssl.py", line 971, in recv_into
`;
// create a data set with groups
var group_names = [];
var groups = new vis.DataSet();
for (let i = 0; i < nGroups; i++) {
group_names.push('GROUP_' + i);
groups.add({
id: group_names[i],
content: group_names[i]
});
}
// add meta group
groups.add({
id: -1,
content: ' '
});
// create a dataset with items
let items = new vis.DataSet();
for (let i = 0; i < nGroups; i++) {
let nSubGroups = randInt(1, maxSubGroups);
//console.log('group='+i+' nSubGroups='+nSubGroups);
let lastStartAdd = 0;
for (let sg = 0; sg < nSubGroups; sg++) {
let start_end = getStartEnd(lastStartAdd);
let start = now.clone().add(start_end['startAdd'], 'hours');
let end = now.clone().add(start_end['endAdd'], 'hours');
let itemID = 'G' + i + '_S' + sg + '_item';
let subgroupID = 'G' + i + '_S' + sg;
let subgroupOrder = sg;
let newItem = {
id: itemID,
group: group_names[i],
subgroup: subgroupID,
subgroupOrder: subgroupOrder,
content: 'ITEM_DU_' + 'G' + i + '_S' + sg,
start: start,
end: end,
title: 'ITEM_DU_' + 'G' + i + '_S' + sg
};
//console.log(group_names[i] + ', ' + 'S' + sg + ', ' +start_end['startAdd'] + ', ' + start_end['endAdd']);
items.add(newItem);
lastStartAdd = start_end['startAdd'];
// random crashes
if(Math.random() <= tcCrashProbability) {
let crashStart = now.clone().add(randInt(start_end['startAdd'], start_end['endAdd']), 'hours');
let newCrashItem = {
id: 'crash_' + itemID,
group: group_names[i],
subgroup: subgroupID,
subgroupOrder: subgroupOrder,
content: 'Crash',
start: crashStart,
type: 'box',
className: 'timeline-tc-crash',
title: '<pre>' + stackTrace + '</pre>'
};
items.add(newCrashItem);
}
}
}
// generate some meta events
for (let i = 0; i < metaEventCount; i++) {
let start = now.clone().add(Math.random() * 200, 'hours');
items.add({
id: 'M' + i,
group: -1,
content: 'Crash',
title: '<pre>' + stackTrace + '</pre>',
className: 'timeline-event-crash',
start: start,
type: 'box'
});
}
// create visualization
var container = document.getElementById('visualization');
var options = {
groupOrder: 'content',
stack: false,
stackSubgroups: true,
orientation: {
axis: 'both'
},
showCurrentTime: false
};
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setGroups(groups);
timeline.setItems(items);
#visualization {
box-sizing: border-box;
width: 100%;
height: 300px;
}
.timeline-event-crash {
background-color: red !important;
border-color: darkred !important;
color: white !important;
font-family: monospace;
box-shadow: 0 0 10px gray;
}
.timeline-tc-crash {
color: red !important;
border-color: red !important;
background-color: #F4BBB5 !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.3.10/seedrandom.min.js"></script>
<link href="https://visjs.github.io/vis-timeline/styles/vis-timeline-graph2d.min.css" rel="stylesheet" />
<script src="https://visjs.github.io/vis-timeline/standalone/umd/vis-timeline-graph2d.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<html>
<head>
<title>Timeline</title>
</head>
<body>
<div id="visualization"></div>
</body>
</html>
I don't know if you ever tried this, but when i am investigating the issue, when my mouse is over the left column i was able to scroll vertically. So not a fix from code side, but if you could not find a proper solution maybe you can add some guidance for your users like "if you want to see groups down below you should scroll when the mouse is over them".
P.S Though i did not deeply investigated this yet but i think this is an expected behavior from vis side, because as far as i have seen from their examples they do not have any option to horizontal scroll when mouse is over timeline
If I use the below line of code:
return Plotly.newPlot(div1, data, layout, { displayModeBar: false, staticPlot: true }) ;
in the below plotly plot function and call plot(crypto("BTC")) in the online code editor I get
[object Promise]
and if I change the above line of code to:
CreateInputDiv();
return Plotly.newPlot(div1, data, layout, { displayModeBar: false, staticPlot: true }).Promise.resolve();
then I get the plot in the online code editor but then I also get the following error in the console.log
TypeError: Plotly.newPlot(...).Promise is undefined
I doubt I am doing thing correctly because I have a hard time understanding how async functions and promises work. The solution to my problem might be very simple or not. If the above code was working correctly I would not have to create a new input div because function parse() creates input and output divs and I would also not get an error in the console log. How can the error message in the console.log be solved?
JavaS.js and HTML below
// counts the number of input divs created
function increment() {
increment.n = increment.n || 0;
return ++increment.n;
}
// creates an input div
function CreateInputDiv() {
increment();
cc = increment.n;
//console.log("increment.n = " + cc);
input = document.createElement("div");
input.setAttribute("id", "input" + cc);
input.setAttribute("class", "input");
input.innerHTML = " ";
input.setAttribute("contenteditable", "true");
input.setAttribute("onkeypress", "parse(event, this)");
document.getElementById('calc').appendChild(input);
input.focus();
}
// creates an output div
function CreateOutputDiv() {
output = document.createElement("div");
output.setAttribute("id", "output" + cc);
output.setAttribute("class", "output");
output.setAttribute("tabindex", "0");
output.setAttribute("contenteditable", "false");
document.getElementById('calc').appendChild(output);
}
function parse(e1, e2) {
console.log("e2 = " + e2);
if (e1.keyCode == 13) { // keycode for enter
event.preventDefault();
var inId = e2.id;
console.log("inId = " + inId);
var outId = "output" + inId.substring(5);
console.log("outId = " + outId);
var inz = input.innerText;
// check if input contains a colon. Hides output if colon exist.
if (inz.indexOf(':') > -1) {
var inz = input.innerText.replace(/:/g, '');
console.log("input with colon = " + inz);
var outz = eval(inz);
console.log("hidden out = " + outz);
document.getElementById("count").value += '\n' + '\n' + eval(cc + 1);
CreateOutputDiv();
CreateInputDiv();
}
else { // no colon = display and revaluate input
if (document.getElementById(outId)) {
console.log("Already created");
inz = document.getElementById(inId).innerText;
console.log("inz = " + inz);
var outz = eval(inz);
console.log("outz = " + outz);
document.getElementById(outId).innerHTML = outz;
input.focus();
}
else { // no colon = display create new lines
document.getElementById("count").value += '\n' + '\n' + eval(cc + 1);
CreateOutputDiv();
// calculate and assign output value to output div
// console.log("input = " + inz);
var outz = eval(inz);
// console.log("out z = " + outz);
output.innerHTML = outz;
CreateInputDiv();
}
}
}
}
function T(UNIX_timestamp) {
var MyDate = new Date(UNIX_timestamp * 1000);
var MyDateString = MyDate.getFullYear() + '-' + ('0' + (MyDate.getMonth() + 1)).slice(-2) + '-' + ('0' + MyDate.getDate()).slice(-2);
return JSON.stringify(MyDateString);
}
function crypto(ticker) {
var ApiKey = "ddd85b386e1a7c889e468a4933f75f22f52b0755b747bdb637ab39c88a3bc19b";
var urlA = "https://min-api.cryptocompare.com/data/histoday?fsym=" + ticker + "&tsym=USD&limit=1000&api_key=" + ApiKey;
var result = null;
$.ajax({
url: urlA,
async: false, // makes a synchrously data call to cryptocompare
dataType: "json",
success: function (data) { result = data; }
});
var y = result.Data;
var D1 = [];
var D2 = [];
for (var i = 0; i < y.length; i++) {
D1.push(T(y[i].time));
D2.push(y[i].close);
}
console.log(D2);
return D2;
}
// plots a give data array
function plot(z) {
var yy = z;
var xx = [];
for (var i = 0; i <= yy.length; i++) {
xx[i] = JSON.stringify(i);
}
var data = [{
x: xx,
y: yy,
type: 'scatter',
line: { color: 'green', width: 2 }
}];
var layout =
{
width: 700,
height: 300,
paper_bgcolor: 'lightblue',
plot_bgcolor: 'lightblue',
margin: { l: 60, b: 60, r: 20, t: 20 },
title: "",
xaxis: {
title: 'x-axis', titlefont: {
family: 'Courier New, monospace', size: 18,
color: 'black'
}
},
yaxis: {
title: 'y-axis', titlefont: { family: 'Courier New, monospace', size: 18, color: 'black' },
width: 1000, height: 380,
xaxis: {
tickfont: { size: 12, color: 'black' }, showgrid: true, tickmode: "linear",
gridcolor: 'black', linecolor: 'black'
},
yaxis: {
tickfont: { size: 12, color: 'black' }, showgrid: true,
gridcolor: 'black', linecolor: 'black'
}
}
};
cc = increment.n;
div1 = 'output' + cc;
// return Plotly.newPlot(div1, data, layout, { displayModeBar: false, staticPlot: true }) ; // object promise
CreateInputDiv();
return Plotly.newPlot(div1, data, layout, { displayModeBar: false, staticPlot: true }).Promise.resolve();
}
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="JavaS.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<meta charset="utf-8" />
<style>
.input {
background-color: lightgreen;
width: 980px;
border: none;
font-size: 16px;
resize: none;
}
.output {
background-color: lightblue;
width: 980px;
line-height: 20px;
border: none;
font-size: 16px;
resize: none;
overflow-wrap: break-word;
}
#count {
background-color: lightblue;
color: black;
width: 25px;
height: 500px;
font-size: 17px;
resize: none;
border: none;
}
#calc {
background-color: lightblue;
vertical-align: top;
border: none;
overflow: hidden;
}
</style>
</head>
<body bgcolor="grey">
<table align="center" width="1000px" height="500px" bgcolor="lightblue" overflow="hidden">
<tr>
<td><textarea id="count" disabled>1 </textarea> </td>
<td id="calc"></td>
</tr>
</table>
<script> CreateInputDiv(); </script>
</body>
</html>
A working solution to the above plotting problem can be found below. Replace
the line
return Plotly.newPlot(div1, data, layout, { displayModeBar: false, staticPlot: true }) ; // object promise
in the plot function with
setTimeout(function(){Plotly.newPlot(div1, data, layout, { displayModeBar: false, staticPlot: true });}, 10);
return "";
That will give you the plotly.js plot in the web editor without getting [object Promise] or any error messages in the console log.
I know you are probably wondering self-made barchart? Why not an existing library? Well I hate to use files with 20000 lines of code while only 500 are necessary.
Oh and it's fun :) The main objective is that I'll be using this script for an app I'll be making using Phonegap. So the lower the size, the better.
So the idea is to achieve the following:
I've been able to to draw the bars, make sure they are of equal width and have their height dependent on the height of the parent container. As you'll see in the code below I also added a font-size to the options. As some chart will expand around 300px of height (which would be using the default 16px for example). And some only 50px with a font-size of 12 or less. So I reduced the bar ChartContainer by 3 x the fontsize (+ rest) to make sure there is enough space for the top (amounts) & bottom (legends + title)
Now I'm not entirely sure how to add and center the amounts. I tried searching existing chart libraries to check on how it all has been rendered, unfortunately they all use canvasses or SVG containers. Any suggestions?
/* dataset
------------------
add legends
add result / amount
add bottom-border: 8px extra to both sides?
add chart name
*/
(function ($) {
var methods = {
init : function(options) {
return this.each(function() {
var $this = $(this),
dataset = options.dataset,
fontSize = options.fontSize,
widthOfContainer = $this.width(),
heightOfContainer = $this.height() - (3 * (fontSize + 4)), // make room for title (bottom), legend (bottom), amounts (top)
widthOfBar = parseInt(widthOfContainer / options.dataset.length) - 2,
bar;
$this.bind('draw', function(e) {
$this.empty();
var maxValueInDataset = Math.max.apply(Math, dataset.map(function(o){return o.a;})),
heightPerUnit = parseInt(heightOfContainer / maxValueInDataset);
for (var i = 0; i < dataset.length; i++) {
bar = $(document.createElement('div'));
bar.addClass('bar');
bar.css({
'height': parseInt(dataset[i].a * heightPerUnit) + 'px',
'width': parseInt(widthOfBar) + 'px',
'margin-left': parseInt(i * 2 + i * widthOfBar) + 'px',
'bottom': 2 * (fontSize + 4)
});
$this.append(bar);
}
});
$this.trigger('draw');
});
},
draw : function(n) {
$(this).trigger('draw');
}
};
$.fn.chart = function(methodOrOptions) {
if ( methods[methodOrOptions] ) {
return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
// Default to "init"
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + methodOrOptions + ' does not exist on jQuery.tooltip' );
}
};
$(document).ready(function(){
$('div.barchart').chart({
// Add font-size?
fontSize: 14,
name: 'mana cost',
dataset: [
{a: 2, label: '0'},
{a: 8, label: '1'},
{a: 9, label: '2'},
{a: 4, label: '3'},
{a: 7, label: '4'},
{a: 3, label: '5'},
{a: 1, label: '6'},
{a: 1, label: '7'},
{a: 2, label: '8'},
{a: 5, label: '9'}
]
});
});
}( jQuery ));
/* Barchart
========================================================================== */
.barchart {
color: black;
}
/* Bar
========================================================================== */
.bar {
position: absolute;
height: 0px;
width: 0px;
margin-left: 0px;
bottom: 0px;
background: black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div style="padding: 20px;">
<div class="barchart" style="height: 100px; position: relative"></div>
</div>
Nice chart! It looks clean. I know what you mean. I spent months trying to manipulate the layout of bxSlider then I realized it was less code to write my own. Here's an attempt at answering your query. I've made it width responsive by using percentages (don't worry; it's easy to change back), added an extra css class for the legend, values, and count, and modified your plugin to include your name option (called legend). These bits are then just formatted and appended. Hope this helps.
/* dataset
------------------
add legends
add result / amount
add bottom-border: 8px extra to both sides?
add chart name
*/
(function ($) {
var methods = {
init : function(options) {
return this.each(function() {
var $this = $(this),
dataset = options.dataset,
fontSize = options.fontSize,
legend = options.name,
widthOfContainer = $this.width(),
heightOfContainer = $this.height() - (3 * (fontSize + 4)), // make room for title (bottom), legend (bottom), amounts (top)
widthOfBar = parseInt(widthOfContainer / options.dataset.length) - 2,
widthOfBarPer = (widthOfBar / widthOfContainer) *100,
bar;
$this.bind('draw', function(e) {
$this.empty();
var maxValueInDataset = Math.max.apply(Math, dataset.map(function(o){return o.a;})),
heightPerUnit = parseInt(heightOfContainer / maxValueInDataset);
for (var i = 0; i < dataset.length; i++) {
var dataVal = dataset[i].a;
bar = $(document.createElement('div'));
bar.addClass('bar');
bar.css({
'height': parseInt( dataVal * heightPerUnit) + 'px',
'width': widthOfBarPer + '%', // percentages to make more responsive?
'margin-left': (i + i * widthOfBarPer ) + '%', // no need to parseInt as you have already on widthOfBar. now your chart stretches with the width .
'bottom': 2 * (fontSize + 4)
});
bar.append('<p class="count">'+ i +'</p>'); // defines the bar count, this could be dataset[i].label but if you just use i then you don't need to type it out for each bar?
bar.append('<p class="value">'+ dataVal +'</p>'); // defines the bar value
$this.append(bar); // adds the bar count
}
var chartHeight = $this.height();
$('.bar .count').css({ bottom: fontSize - chartHeight * 0.5 });
$('.bar .value').css({ bottom: chartHeight * 0.5 - fontSize });
if(legend){
legend = '<p class="legend">'+legend+'</p>';
$this.append(legend);
// $this.css({ border: '1px solid #f90'}); // temp to see the current chart size
$this.find('.legend').css({ top: chartHeight - fontSize });
}
});
$this.trigger('draw');
});
},
draw : function(n) {
$(this).trigger('draw');
}
};
$.fn.chart = function(methodOrOptions) {
if ( methods[methodOrOptions] ) {
return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
// Default to "init"
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + methodOrOptions + ' does not exist on jQuery.tooltip' );
}
};
$(document).ready(function(){
$('div.barchart').chart({
// Add font-size?
fontSize: 14,
name: 'mana cost',
dataset: [
{a: 2, label: '0'},
{a: 8, label: '1'},
{a: 9, label: '2'},
{a: 4, label: '3'},
{a: 7, label: '4'},
{a: 3, label: '5'},
{a: 1, label: '6'},
{a: 1, label: '7'},
{a: 2, label: '8'},
{a: 5, label: '9'}
]
});
});
}( jQuery ));
/* Barchart
========================================================================== */
.barchart {
color: black;
}
/* Bar
========================================================================== */
.bar {
position: absolute;
height: 0px;
width: 0px;
margin-left: 0px;
bottom: 0px;
background: black;
}
.count, .value{
z-index: 7;
position: absolute;
text-align: center;
width: 100%;
}
.legend{
position: relative;
text-align: center;
width: 100%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div style="padding: 20px;">
<div class="barchart" style="height: 100px; position: relative"></div>
</div>
Thumbs up for avoiding unnecessary amounts of code—a clean barchart is a perfect example of not needing a massive library.
If you use a <table> as the basic structure to create your barchart from, you get easy access to the formatting options you want—resulting in less code:
One row for the values, one row for the labels
Bars can be made by styling the border-bottom of each value cell
Text can be centered within table cells
Adding a <caption> for the legend, which is centered by default, and is easily positioned below the table with the caption-side property
Styling the vertical-align property of the value cells allows values to be positioned directly above the bars (as in the demo below) or lined up on top (as in your illustration)—this is controlled in line 20 in the js demo code below.
After all, a barchart is just a vizualisation of tabular data, so using a table makes sense.
A working demo (in about 30 lines of vanilla js and a few lines of css, which you can easily adapt to your jquery approach if required):
function barchart(containerId, options) {
var i,
html,
valueRow = '',
labelRow = '',
data = options.dataset,
maxBarHeight = 60, /* in px, could be set from options */
barWidth = 20, /* in px, could be set from options */
maxValue = Math.max.apply(
Math,
data.map(function(o) {
return o.a;
})
);
for(i = 0; i < data.length; i++){
labelRow += '<td>' + data[i].label + '</td>';
valueRow += '<td style="border-bottom:' +
(data[i].a * maxBarHeight / maxValue) +
'px solid black;vertical-align:' +
'bottom' + /* set to 'top' to get value labels lined up */
';width: ' +
barWidth + 'px">' +
data[i].a + '</td>';
}
html = '<table class="barchart" ' +
'style="font-size:' + options.fontSize + 'px">' +
'<caption>' + options.name + '</caption>' +
'<tr>' + valueRow + '</tr>' +
'<tr>' + labelRow + '</tr>' +
'</table>';
document.getElementById(containerId)
.innerHTML = html;
}
/* create a barchart */
barchart('testdiv', {
fontSize: 14,
name: 'mana cost',
dataset: [
{a: 2, label: '0'},
{a: 8, label: '1'},
{a: 9, label: '2'},
{a: 4, label: '3'},
{a: 7, label: '4'},
{a: 3, label: '5'},
{a: 1, label: '6'},
{a: 1, label: '7'},
{a: 2, label: '8'},
{a: 5, label: '9'}
]
});
.barchart td{
text-align: center;
}
.barchart{
font-family: 'arial narrow', sans-serif;
caption-side: bottom;
}
<div id="testdiv"></div>
I'm doing this plugin that takes the words and makes them pulsate on the screen:
First they appear and grow, then they vanish, change place and again appear
Working plugin:
+ function($) {
var Pulsate = function(element) {
var self = this;
self.element = element;
self.max = 70;
self.min = 0;
self.speed = 500;
self.first = true;
self.currentPlace;
self.possiblePlaces = [
{
id: 0,
top: 150,
left: 150,
},
{
id: 1,
top: 250,
left: 250,
},
{
id: 2,
top: 350,
left: 350,
},
{
id: 3,
top: 250,
left: 750,
},
{
id: 4,
top: 450,
left: 950,
}
];
};
Pulsate.prototype.defineRandomPlace = function() {
var self = this;
self.currentPlace = self.possiblePlaces[Math.floor(Math.random() * self.possiblePlaces.length)];
if(!self.possiblePlaces) self.defineRandomPlace;
self.element.css('top', self.currentPlace.top + 'px');
self.element.css('left', self.currentPlace.left + 'px');
};
Pulsate.prototype.animateToZero = function() {
var self = this;
self.element.animate({
'fontSize': 0,
'queue': true
}, self.speed, function() {
self.defineRandomPlace();
});
};
Pulsate.prototype.animateToRandomNumber = function() {
var self = this;
self.element.animate({
'fontSize': Math.floor(Math.random() * (70 - 50 + 1) + 50),
'queue': true
}, self.speed, function() {
self.first = false;
self.start();
});
};
Pulsate.prototype.start = function() {
var self = this;
if (self.first) self.defineRandomPlace();
if (!self.first) self.animateToZero();
self.animateToRandomNumber();
};
$(window).on('load', function() {
$('[data-pulsate]').each(function() {
var element = $(this).data('pulsate') || false;
if (element) {
element = new Pulsate($(this));
element.start();
}
});
});
}(jQuery);
body {
background: black;
color: white;
}
.word {
position: absolute;
text-shadow: 0 0 5px rgba(255, 255, 255, 0.9);
font-size: 0px;
}
.two {
position: absolute;
color: white;
left: 50px;
top: 50px;
}
div {
margin-left: 0px;
}
<span class="word" data-pulsate="true">Love</span>
<span class="word" data-pulsate="true">Enjoy</span>
<span class="word" data-pulsate="true">Huggs</span>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
If you notice i define the places that the word can grow in the self.possiblePlaces, and if you notice the animation, sometimes more then one word can grow in one place, my goal coming here is ask for help. How I can make two words never grow in the same place??
I was trying to do like this:
In the defineRandomPlace i pick a random object inside my possiblePlaces array:
Pulsate.prototype.defineRandomPlace = function() {
var self = this;
self.currentPlace = self.possiblePlaces[Math.floor(Math.random() * self.possiblePlaces.length)];
if(!self.possiblePlaces) self.defineRandomPlace;
delete self.possiblePlaces[self.currentPlace.id];
self.element.css('top', self.currentPlace.top + 'px');
self.element.css('left', self.currentPlace.left + 'px');
};
Notice the delete, first i clone the chosen object, after I delete it but keep his place in the array.
After the animation was over, I put the object in the array again, before starting all over again:
Pulsate.prototype.animateToZero = function() {
var self = this;
self.element.animate({
'fontSize': 0,
'queue': true
}, self.speed, function() {
self.possiblePlaces[self.currentPlace.id] = self.currentPlace;
self.defineRandomPlace();
});
But it made no difference.
Thanks!!
Pen: http://codepen.io/anon/pen/waooQB
In your example, you are randomly picking from a list that has five members, and you have three separate words that could be displayed, putting chance of overlap fairly high.
A simple approach to resolve is to pick the first item in the list, remove it from the list, and the append to the end of the list each time. Because you have more positions in the lists than items selecting from it, you're guaranteed to never collide.
Share the same list possiblePlaces between all instances.
Shift the first item off the queue, and push it onto the end when its done each time, instead of selecting randomly in defineRandomPlace.
Snippet highlighting #2:
// shift a position off the front
self.currentPlace = possiblePlaces.shift();
self.element.css('top', self.currentPlace.top + 'px');
self.element.css('left', self.currentPlace.left + 'px');
// push it back on the end
possiblePlaces.push(self.currentPlace);
If you want it truly random, you'll need to randomly select and remove an item from the array, and not put it back into the array until after it's been used. You'll also need to always ensure that you have more possiblePlaces than you have dom elements to place on the page.
Like so:
Pulsate.prototype.defineRandomPlace = function() {
var self = this;
var newPlace = possiblePlaces.splice(Math.floor(Math.random()*possiblePlaces.length), 1)[0];
if (self.currentPlace) {
possiblePlaces.push(self.currentPlace);
}
self.currentPlace = newPlace;
self.element.css('top', self.currentPlace.top + 'px');
self.element.css('left', self.currentPlace.left + 'px');
};
See http://codepen.io/anon/pen/bdBBPE
My decision is to divide the page to imaginary rows and to prohibit more than one word in the same row. Please check this out.
Note: as the code currently does not support recalculating of rows count on document resize, the full page view will not display correctly. Click "reload frame" or try JSFiddle or smth.
var pulsar = {
// Delay between words appearance
delay: 400,
// Word animation do not really depend on pulsar.delay,
// but if you set pulsar.delay small and wordAnimationDuration long
// some words will skip their turns. Try 1, 2, 3...
wordAnimationDuration: 400 * 3,
// Depending on maximum font size of words we calculate the number of rows
// to which the window can be divided
maxFontSize: 40,
start: function () {
this.computeRows();
this.fillWords();
this.animate();
},
// Calculate the height or row and store each row's properties in pulsar.rows
computeRows: function () {
var height = document.body.parentNode.clientHeight;
var rowsCount = Math.floor(height/this.maxFontSize);
this.rows = [];
for (var i = 0; i < rowsCount; i++) {
this.rows.push({
index: i,
isBusy: false
});
}
},
// Store Word instances in pulsar.words
fillWords: function () {
this.words = [];
var words = document.querySelectorAll('[data-pulsate="true"]');
for (var i = 0; i < words.length; i++) {
this.words.push(new Word(words[i], this.wordAnimationDuration, this.maxFontSize));
}
},
// When it comes time to animate another word we need to know which row to move it in
// this random row should be empty at the moment
getAnyEmptyRowIndex: function () {
var emptyRows = this.rows.filter(function(row) {
return !row.isBusy;
});
if (emptyRows.length == 0) {
return -1;
}
var index = emptyRows[Math.floor(Math.random() * emptyRows.length)].index;
this.rows[index].isBusy = true;
return index;
},
// Here we manipulate words in order of pulsar.words array
animate: function () {
var self = this;
this.interval = setInterval(function() {
var ri = self.getAnyEmptyRowIndex();
if (ri >= 0) {
self.words.push(self.words.shift());
self.words[0].animate(ri, function () {
self.rows[ri].isBusy = false;
});
}
}, this.delay);
}
}
function Word (span, duration, maxFontSize) {
this.span = span;
this.inAction = false;
this.duration = duration;
this.maxFontSize = maxFontSize;
}
/**
* #row {Numer} is a number of imaginary row to place the word into
* #callback {Function} to call on animation end
*/
Word.prototype.animate = function (row, callback) {
var self = this;
// Skip turn if the word is still busy in previous animation
if (self.inAction) {
return;
}
var start = null,
dur = self.duration,
mfs = self.maxFontSize,
top = row * mfs,
// Random left offset (in %)
left = Math.floor(Math.random() * 90),
// Vary then font size within half-max size and max size
fs = mfs - Math.floor(Math.random() * mfs / 2);
self.inAction = true;
self.span.style.top = top + 'px';
self.span.style.left = left + '%';
function step (timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
// Calculate the factor that will change from 0 to 1, then from 1 to 0 during the animation process
var factor = 1 - Math.sqrt(Math.pow(2 * Math.min(progress, dur) / dur - 1, 2));
self.span.style.fontSize = fs * factor + 'px';
if (progress < dur) {
window.requestAnimationFrame(step);
}
else {
self.inAction = false;
callback();
}
}
window.requestAnimationFrame(step);
}
pulsar.start();
body {
background: black;
color: white;
}
.word {
position: absolute;
text-shadow: 0 0 5px rgba(255, 255, 255, 0.9);
font-size: 0px;
/* To make height of the .word to equal it's font size */
line-height: 1;
}
<span class="word" data-pulsate="true">Love</span>
<span class="word" data-pulsate="true">Enjoy</span>
<span class="word" data-pulsate="true">Huggs</span>
<span class="word" data-pulsate="true">Peace</span>
I updated your plugin constructor. Notice the variable Pulsate.possiblePlaces, I have changed the variable declaration this way to be able to share variable data for all Object instance of your plugin.
var Pulsate = function(element) {
var self = this;
self.element = element;
self.max = 70;
self.min = 0;
self.speed = 500;
self.first = true;
self.currentPlace;
Pulsate.possiblePlaces = [
{
id: 0,
top: 150,
left: 150,
},
{
id: 1,
top: 250,
left: 250,
},
{
id: 2,
top: 350,
left: 350,
},
{
id: 3,
top: 250,
left: 750,
},
{
id: 4,
top: 450,
left: 950,
}
];
};
I added occupied attribute to the possible places to identify those that are already occupied. If the randomed currentPlace is already occupied, search for random place again.
Pulsate.prototype.defineRandomPlace = function() {
var self = this;
self.currentPlace = Pulsate.possiblePlaces[Math.floor(Math.random() * Pulsate.possiblePlaces.length)];
if(!Pulsate.possiblePlaces) self.defineRandomPlace;
if (!self.currentPlace.occupied) {
self.currentPlace.occupied = true;
self.element.css('top', self.currentPlace.top + 'px');
self.element.css('left', self.currentPlace.left + 'px');
} else {
self.defineRandomPlace();
}
};
Every time the element is hidden, set the occupied attribute to false.
Pulsate.prototype.animateToZero = function() {
var self = this;
self.element.animate({
'fontSize': 0,
'queue': true
}, self.speed, function() {
self.currentPlace.occupied = false;
self.defineRandomPlace();
});
};
A small hint f = 0.5px x = 100px t = 0.5s
x / f = 200
200/2 * t * 0.5 = f(shrink->expand until 100px square) per 0.5 seconds