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>
Related
If I click on a symbol on a map I want to create timeseries-plots with plotly. If i click on the next symbol there should be a new plot etc.
This is working fine, but I am not able to use the plotly interface in the first plots again.
I build a jfiddle that shows my problem and my code:
var treediv_ids = [-1];
var timeseries_div_HTML = function(id) {
var ts_div_html_vis =
"<div class='ts_con_vis' id='ts_data_" + id + "'>\n\
<div class='ts_data_div' name='ts_data_div" + id + "' id='ts_data_div_" + id + "_env'></div>"
treediv_ids.push(id);
return ts_div_html_vis;
};
var addTS = function() {
// Create the subdivs for timeseries_production
var new_id = Math.max(...treediv_ids) + 1;
var new_div = timeseries_div_HTML(new_id);
$('#timeseriescontainer').html($('#timeseriescontainer').html() + new_div);
// Create timeseries using plotly and local datasets
//
// Environmental Data
// Create layout object
var layout_E = {
title: 'Tim',
yaxis: {
title: 'Value'
},
margin: {
t: 40,
b: 20,
r: 40,
l: 40
},
}
plot("ts_data_div_" + new_id + "_env", layout_E);
}
function plot(target_div, layout) {
var data = [];
for (var i = 1; i < 3; i++) {
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
var trace = {
type: "scatter",
mode: "lines",
name: i,
x: [1, 2, 3, 4, 5, 6, 7, 8],
y: [1, 2, 3, 4, 5, 6, 7, 8],
line: {
color: randomColor
}
}
data.push(trace)
}
layout.title = layout.title + target_div.slice("ts_data_div_".length);
target = document.getElementById(target_div);
Plotly.newPlot(target, data, layout);
}
https://jsfiddle.net/wmg5k8xn/2/
I found this thread: Plotly.js only one plot working when using multiple plots on same page
But I did not understand why it is working in the accepted answer and why it is not working for me.
Edit: my HTML and CSS code:
<html>
<head>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<button onclick=addTS() style="z-index:100">
ABC
</button>
<div id="timeseriescontainer"></div>
<script data-require="plotly#1.0.0" data-semver="1.0.0" src="https://cdn.plot.ly/plotly-latest.min.js" defer></script>
<script src="script.js" defer></script>
</body>
</html>
#timeseriescontainer{
width:90%;
height:100%;
position:absolute;
right:0px;
top: 0px;
background-color: green;
overflow-y: scroll;
}
.ts_con_vis{
background-color: red;
width: 99%;
height: 49%;
margin-left:5px;
margin-top:1px;
position: relative;
display: block;
}
.ts_data_div{
width: 100%;
height:100%;
position: absolute;
}
Your problem is caused by this line:
$('#timeseriescontainer').html($('#timeseriescontainer').html() + new_div);
Just use native browser functions instead:
const container = document.getElementById('timeseriescontainer');
container.insertAdjacentHTML('beforeend', new_div);
See https://jsfiddle.net/qhk7nmsp/
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
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:
trying to compare two sensor readings - the data is coming from thingspeak. I've got the zoom part working, but for some reason I cant get the scroll to work.
<script type="text/javascript">
// variables for the first series
var series_1_channel_id = 43330;
var series_1_field_number = 4;
var series_1_read_api_key = '7ZPHNX2SXPM0CA1K';
var series_1_results = 480;
var series_1_color = '#d62020';
var series_1_name = 'Zims Sensor';
// variables for the second series
var series_2_channel_id = 45473;
var series_2_field_number = 2;
var series_2_read_api_key = 'N12T3CWQB5IWJAU9';
var series_2_results = 480;
var series_2_color = '#00aaff';
var series_2_name = 'UVM30a';
// chart title
var chart_title = 'UV Sensors Zim / UVM30A';
// y axis title
var y_axis_title = 'UV Index';
// user's timezone offset
var my_offset = new Date().getTimezoneOffset();
// chart variable
var my_chart;
// when the document is ready
$(document).on('ready', function() {
// add a blank chart
addChart();
// add the first series
addSeries(series_1_channel_id, series_1_field_number, series_1_read_api_key, series_1_results, series_1_color, series_1_name);
// add the second series
addSeries(series_2_channel_id, series_2_field_number, series_2_read_api_key, series_2_results, series_2_color, series_2_name);
});
// add the base chart
function addChart() {
// variable for the local date in milliseconds
var localDate;
// specify the chart options
var chartOptions = {
chart: {
renderTo: 'chart-container',
defaultSeriesType: 'line',
zoomType: 'x', // added here
backgroundColor: '#ffffff',
events: { }
},
title: { text: chart_title },
plotOptions: {
series: {
marker: { radius: 3 },
animation: true,
step: false,
borderWidth: 0,
turboThreshold: 0
}
},
tooltip: {
// reformat the tooltips so that local times are displayed
formatter: function() {
var d = new Date(this.x + (my_offset*60000));
var n = (this.point.name === undefined) ? '' : '<br>' + this.point.name;
return this.series.name + ':<b>' + this.y + '</b>' + n + '<br>' + d.toDateString() + '<br>' + d.toTimeString().replace(/\(.*\)/, "");
}
},
xAxis: {
type: 'datetime',
scrollbar: {
enabled: true,
barBackgroundColor: 'gray',
barBorderRadius: 7,
barBorderWidth: 0,
buttonBackgroundColor: 'gray',
buttonBorderWidth: 0,
buttonArrowColor: 'yellow',
buttonBorderRadius: 7,
rifleColor: 'yellow',
trackBackgroundColor: 'white',
trackBorderWidth: 1,
trackBorderColor: 'silver',
trackBorderRadius: 7
},
title: { text: 'Date' }
},
yAxis: { title: { text: y_axis_title } },
exporting: { enabled: true },
legend: { enabled: true },
credits: {
text: 'ThingSpeak.com',
href: 'https://thingspeak.com/',
style: { color: '#D62020' }
}
};
// draw the chart
my_chart = new Highcharts.Chart(chartOptions);
}
// add a series to the chart
function addSeries(channel_id, field_number, api_key, results, color, name) {
var field_name = 'field' + field_number;
// get the data with a webservice call
$.getJSON('https://api.thingspeak.com/channels/' + channel_id + '/fields/' + field_number + '.json?offset=0&round=2&results=' + results + '&api_key=' + api_key, function(data) {
// blank array for holding chart data
var chart_data = [];
// iterate through each feed
$.each(data.feeds, function() {
var point = new Highcharts.Point();
// set the proper values
var value = this[field_name];
point.x = getChartDate(this.created_at);
point.y = parseFloat(value);
// add location if possible
if (this.location) { point.name = this.location; }
// if a numerical value exists add it
if (!isNaN(parseInt(value))) { chart_data.push(point); }
});
// add the chart data
my_chart.addSeries({ data: chart_data, name: data.channel[field_name], color: color });
});
}
// converts date format from JSON
function getChartDate(d) {
// offset in minutes is converted to milliseconds and subtracted so that chart's x-axis is correct
return Date.parse(d) - (my_offset * 60000);
}
</script>
<style type="text/css">
body { background-color: white; height: 100%; margin: 0; padding: 0; }
#chart-container { width: 800px; height: 400px; display: block; position:absolute; bottom:0; top:0; left:0; right:0; margin: 5px 15px 15px 0; overflow: hidden; }
</style>
<!DOCTYPE html>
<html style="height: 100%;">
<head>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script type="text/javascript" src="//code.highcharts.com/stock/highstock.js"></script>
<script type="text/javascript" src="//thingspeak.com/exporting.js"></script>
</head>
<body>
<div id="chart-container">
// <img alt="Ajax loader" src="//thingspeak.com/assets/ajax-loader.gif" style="position: absolute; margin: auto; top: 0; left: 0; right: 0; bottom: 0;" />
</div>
</body>
</html>
I would also like to get the chart updating automatically, so any help on that score would also be appreciated. The final issue I am having is trying to get the legend to display the sensor names properly: UV Index (red) should read "Zims Sensor" and UV Index (blue) should read "UVM30A"