visjs timeline - set initial vertical position of items - javascript

I'm using vis-timeline to render a hundred rows of data. When the component loads, I want to see the rows starting from the beginning: 1, 2, 3, and so on. Instead, by default, vis-timeline starts by displaying the end of the list (...97, 98, 99, 100), so that you have to scroll up to get to the top?
There are methods like setWindow() for setting the horizontal time frame, but what about the vertical position? I've tried the HTML scroll() method, but that doesn't seem to do anything.
Normally I would use orientation: { item: 'top' } } in the options, but there is a bug that prevents scrolling in that case; it must be set to 'bottom'.
I've thought about initializing the component with the orientation to 'top', then once it displays, setting it to 'bottom' to allow scrolling, but that seems pretty hacky.
Is there a cleaner solution?

You can implement a custom ordering function for items (groups as well) using the order configuration option:
Provide a custom sort function to order the items. The order of the
items is determining the way they are stacked. The function order is
called with two arguments containing the data of two items to be
compared.
WARNING: Use with caution. Custom ordering is not suitable for large
amounts of items. On load, the Timeline will render all items once to
determine their width and height. Keep the number of items in this
configuration limited to a maximum of a few hundred items.
See below for an example:
var items = new vis.DataSet();
for (let i=0; i<100; i++) {
items.add({
id: i,
content: 'item ' + i,
start: new Date(2022, 4, 1),
end: new Date(2022, 4, 14)
});
}
var container = document.getElementById('visualization1');
var options = {
order: function (a, b) {
return b.id - a.id;
}
}
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setItems(items);
<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis-timeline-graph2d.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js" rel="script"></script>
<div id="visualization1"></div>

Related

How to spread divs around without overlapping each other

I'm building an event scheduler and I've gotten into a point that I can't figure out a way to spread events around without overlapping each other. (It might have events at the same time and without limits. Whenever is possible, use 100% of the available width)
Here it is a picture of this scenario.
Some considerations:
The events is wrapped inside a div with position: relative and all the events has position:absolute.
Using javascript, I have to figure out what needs to be the value of top left width and height of each "div event" on the fly.
Events are an array of objects like the code below:
{
startAt: "12:00:30",
endsAt: "13:00:00",
description: "evt1",
id: '00001'
}
I'm using Vue.js to develop this project. But this is not an issue if you don't know Vue. I've build a small project using jsbin so you can just play around with a javascript function.
Live Code: https://jsbin.com/bipesoy/
Where I'm having problem?
I can't find a algorithm to calculate the top left width and height on the fly based on an array of events.
Some considerations about the jsbin code:
All the code to find the 4 properties above is inside the function parsedEvents
Inside parsedEvents you can access the array of events using: this.events
The job of parsedEvents is loop through the array of events and add the style propertie to each one and then return a new array of events with the style object.
Each 30 minutes has a height of 40px;
Any ideas how to accomplish it or a better solution?
After some time playing with this challenge I think it's time to give up from the idea. It is possible to program many possible scenarios of arranging events, but when you dig deep you realize it's very difficult to even write what exactly you want to be done and even if you manage, some of your decisions just don't look good on the screen. This is only with expanding events in width. Positioning and even re-positioning them to fill gaps is solved and not too difficult.
Snippet is here (or JSBin if you prefer: http://jsbin.com/humiyi/99/edit?html,js,console,output).
Green events are detected as "expandable". Grey o nes cannot expand. To clarify problems with logic of expanding, some examples:
evt 1 and evt3? it can be like this or evt3 goes right and they both expand
evt7 and evt12? Many ways to expand this... how to define a rule?
imagine evt7 and evt11 ar merged into one big event. How to expand evt10, evt7/11 and evt 12?
... now try to write rules to consistently answer on above 3 (and many many more possible scenarios not in this example)
My conclusion is that writing rules and developing this is not worth the effort. UI usability will not get much. They will even loose in some scenarios, i.e. some events would be visually bigger just because they have space and not because they're more important.
I'd suggest layout similar or exactly the same as the one in example. Events just don't expand. I don't know how much daily events you expect, what's real life scenario, but only upgrade I'd potentially do is to split vertically calendar in separated regions - point in time which is not overlapping with any event, like line between evt7 and evt11 in example. Then run this same script per region independently. That will recalculate vertical slots per region so region with evt10 i evt11 will have only 2 vertical slots filling space 50% each. This maybe be worth it if your calendar has few crowded hours and only a few events later/before. This would fix an issue of too narrow events later in the day without spending much time. But if events are all over the day and overlapping alot I don't think it's worth it.
let events = [
{ startAt: "00:00", endsAt: "01:00", description: "evt1", id: '00001' },
{ startAt: "01:30", endsAt: "08:00", description: "evt2", id: '00002' },
{ startAt: "01:30", endsAt: "04:00", description: "evt3", id: '00003' },
{ startAt: "00:30", endsAt: "02:30", description: "evt3", id: '00013' },
{ startAt: "00:00", endsAt: "01:00", description: "evt3", id: '00014' },
{ startAt: "03:00", endsAt: "06:00", description: "evt4", id: '00004' },
{ startAt: "01:30", endsAt: "04:30", description: "evt5", id: '00005' },
{ startAt: "01:30", endsAt: "07:00", description: "evt6", id: '00006' },
{ startAt: "06:30", endsAt: "09:00", description: "evt7", id: '00007' },
{ startAt: "04:30", endsAt: "06:00", description: "evt8", id: '00008' },
{ startAt: "05:00", endsAt: "06:00", description: "evt9", id: '00009' },
{ startAt: "09:00", endsAt: "10:00", description: "evt10", id: '00010' },
{ startAt: "09:00", endsAt: "10:30", description: "evt11", id: '00011' },
{ startAt: "07:00", endsAt: "08:00", description: "evt12", id: '00012' }
]
console.time()
// will store counts of events in each 30-min chunk
// each element represents 30 min chunk starting from midnight
// ... so indexOf * 30 minutes = start time
// it will also store references to events for each chunk
// each element format will be: { count: <int>, eventIds: <array_of_ids> }
let counter = []
// helper to convert time to counter index
time2index = (time) => {
let splitTime = time.split(":")
return parseInt(splitTime[0]) * 2 + parseInt(splitTime[1])/30
}
// loop through events and fill up counter with data
events.map(event => {
for (let i = time2index(event.startAt); i < time2index(event.endsAt); i++) {
if (counter[i] && counter[i].count) {
counter[i].count++
counter[i].eventIds.push(event.id)
} else {
counter[i] = { count: 1, eventIds: [event.id] }
}
}
})
//find chunk with most items. This will become number of slots (vertical spaces) for our calendar grid
let calSlots = Math.max( ...counter.filter(c=>c).map(c=>c.count) ) // filtering out undefined elements
console.log("number of calendar slots: " + calSlots)
// loop through events and add some more props to each:
// - overlaps: all overlapped events (by ref)
// - maxOverlapsInChunk: number of overlapped events in the most crowded chunk
// (1/this is maximum number of slots event can occupy)
// - pos: position of event from left (in which slot it starts)
// - expandable: if maxOverlapsInChunk = calSlot, this event is not expandable for sure
events.map(event => {
let overlappedEvents = events.filter(comp => {
return !(comp.endsAt <= event.startAt || comp.startAt >= event.endsAt || comp.id === event.id)
})
event.overlaps = overlappedEvents //stores overlapped events by reference!
event.maxOverlapsInChunk = Math.max( ...counter.filter(c=>c).map(c=>c.eventIds.indexOf(event.id) > -1 ? c.count : 0))
event.expandable = event.maxOverlapsInChunk !== calSlots
event.pos = Math.max( ...counter.filter(c=>c).map( c => {
let p = c.eventIds.indexOf(event.id)
return p > -1 ? p+1 : 1
}))
})
// loop to move events leftmost possible and fill gaps if any
// some expandable events will stop being expandable if they fit gap perfectly - we will recheck those later
events.map(event => {
if (event.pos > 1) {
//find positions of overlapped events on the left side
let vertSlotsTakenLeft = event.overlaps.reduce((result, cur) => {
if (result.indexOf(cur.pos) < 0 && cur.pos < event.pos) result.push(cur.pos)
return result
}, [])
// check if empty space on the left
for (i = 1; i < event.pos; i++) {
if (vertSlotsTakenLeft.indexOf(i) < 0) {
event.pos = i
console.log("moving " + event.description + " left to pos " + i)
break
}
}
}
})
// fix moved events if they became non-expandable because of moving
events.filter(event=>event.expandable).map(event => {
let leftFixed = event.overlaps.filter(comp => {
return event.pos - 1 === comp.pos && comp.maxOverlapsInChunk === calSlots
})
let rightFixed = event.overlaps.filter(comp => {
return event.pos + 1 === comp.pos && comp.maxOverlapsInChunk === calSlots
})
event.expandable = (!leftFixed.length || !rightFixed.length)
})
//settings for calendar (positioning events)
let calendar = {width: 300, chunkHeight: 30}
// one more loop through events to calculate top, left, width and height
events.map(event => {
event.top = time2index(event.startAt) * calendar.chunkHeight
event.height = time2index(event.endsAt) * calendar.chunkHeight - event.top
//event.width = 1/event.maxOverlapsInChunk * calendar.width
event.width = calendar.width/calSlots // TODO: temporary width is 1 slot
event.left = (event.pos - 1) * calendar.width/calSlots
})
console.timeEnd()
// TEST drawing divs
events.map(event => {
$("body").append(`<div style="position: absolute;
top: ${event.top}px;
left: ${event.left}px;
width: ${event.width}px;
height: ${event.height}px;
background-color: ${event.expandable ? "green" : "grey"};
border: solid black 1px;
">${event.description}</div>`)
})
//console.log(events)
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
</body>
</html>
was thinking more about this. I covered width, height, and top. But you might have issues with calculating left. So a little change... goal to create counters for each 30-min increment still remains, but instead of looping through increments and filtering items, do it vice versa. Loop through items and fill that counter object from there. It will also be faster. While on that loop, don't just store counter to counter object, but also push itemID to another property of the same object (and the same increment). So, each increment can be represented like {span: "17:00", counter: 2, items: [135, 148]}. This items array will later help you to determine left position of each item and avoid horizontal overlapping. Because maximal poistion of the item in items array across all increments of the item = position of the item on the calendar from left. Multiply it with width (-1) and you have left.

Dynamically change column widths in ExtJS 6 dashboards

Is there a way to change the column widths in an Ext.dashboard.Dashboard after it has been rendered? It is created with a config value of:
columnWidths: [
0.35,
0.40,
0.25
]
I want to dynamically change it to:
columnWidths: [
0.5,
0.25,
0.25
]
Changing the columnWidths property directly or with setConfig does not update the dashboard. It doesn't appear to have a method for "refresh" or anything else that obviously serves the same purpose. The individual dashboard-columns have a setWidth function but that doesn't seem to do anything either.
I couldn't find anything in the API that managed this automatically but you can manually update the columnWidth values of each child component in the dashboard and then call updateLayout:
function setColumnWidths(dashboard, columnWidths){
var i = 0;
dashboard.items.each(function(item){
if(item instanceof Ext.resizer.Splitter)
return; // ignore
item.columnWidth = columnWidths[i++] || 1;
});
dashboard.updateLayout();
}
» Fiddle

Row Chart grouping on two text dimensions [duplicate]

I need to create a rowchart in dc.js with inputs from multiple columns in a csv. So i need to map a column to each row and each columns total number to the row value.
There may be an obvious solution to this but i cant seem to find any examples.
many thanks
S
update:
Here's a quick sketch. Apologies for the standard
Row chart;
column1 ----------------- 64 (total of column 1)
column2 ------- 35 (total of column 2)
column3 ------------ 45 (total of column 3)
Interesting problem! It sounds somewhat similar to a pivot, requested for crossfilter here. A solution comes to mind using "fake groups" and "fake dimensions", however there are a couple of caveats:
it will reflect filters on other dimensions
but, you will not be able to click on the rows in the chart in order to filter anything else (because what records would it select?)
The fake group constructor looks like this:
function regroup(dim, cols) {
var _groupAll = dim.groupAll().reduce(
function(p, v) { // add
cols.forEach(function(c) {
p[c] += v[c];
});
return p;
},
function(p, v) { // remove
cols.forEach(function(c) {
p[c] -= v[c];
});
return p;
},
function() { // init
var p = {};
cols.forEach(function(c) {
p[c] = 0;
});
return p;
});
return {
all: function() {
// or _.pairs, anything to turn the object into an array
return d3.map(_groupAll.value()).entries();
}
};
}
What it is doing is reducing all the requested rows to an object, and then turning the object into the array format dc.js expects group.all to return.
You can pass any arbitrary dimension to this constructor - it doesn't matter what it's indexed on because you can't filter on these rows... but you probably want it to have its own dimension so it's affected by all other dimension filters. Also give this constructor an array of columns you want turned into groups, and use the result as your "group".
E.g.
var dim = ndx.dimension(function(r) { return r.a; });
var sidewaysGroup = regroup(dim, ['a', 'b', 'c', 'd']);
Full example here: https://jsfiddle.net/gordonwoodhull/j4nLt5xf/5/
(Notice how clicking on the rows in the chart results in badness, because, what is it supposed to filter?)
Are you looking for stacked row charts? For example, this chart has each row represent a category and each color represents a sub-category:
Unfortunately, this feature is not yet supported at DC.js. The feature request is at https://github.com/dc-js/dc.js/issues/397. If you are willing to wade into some non-library code, you could check out the examples referenced in that issue log.
Alternatively, you could use a stackable bar chart. This link seems to have a good description of how this works: http://www.solinea.com/blog/coloring-dcjs-stacked-bar-charts

Chart.js (line chart) tooltip duration/delay

There does not seem to be any option or implementation on showing the tooltip for a second or two before it dissapears when going from a hovering to non-hovering state. So when you have your mouse hovering over the tooltip, great, it shows but then when you remove your mouse from the point I would like to have that showing for an extra 2 seconds instead of dissapearing instantly as it does currently.
What I've done so far
I've looked through the documentation and the available tooltip options. It has a customTooltip function available but that is for when you want to implement a completely custom tooltip.
Gone through the chart.js line chart's codebase where it attaches the events and can't seem to figure out how to add in a delay.
Seen the 'mouseout', 'mouseover' events array list of strings but can't seem to figure out how to use them.
Can someone please point me in the right direction as to what I need to do on implementing this delay/fade effect on the tooltip.
The tooltips are cleared by the showTooltip function (the redraw clears off the existing tooltips). So one naïve way would be to hook into this to introduce your delay, like so
var data = {
labels: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
datasets: [
{
data: [12, 23, 23, 43, 45, 12, 33]
}
]
};
var ctx = document.getElementById("myChart").getContext("2d");
var myLineChart = new Chart(ctx).Line(data);
var originalShowTooltip = myLineChart.showTooltip;
var timeout;
myLineChart.showTooltip = function (activeElements) {
var delay = (activeElements.length === 0) ? 2000 : 0;
clearTimeout(timeout);
timeout = setTimeout(function () {
originalShowTooltip.call(myLineChart, activeElements);
}, delay);
}
This delays the tooltips if the chart is going to clear off all tooltips.
Notice that there is no delay in removing the old tooltip if you move on to another tooltip. If you want it this to be a delayed disappearance, you'll need to maintain your own array of active points pushing in elements (instantaneously) / popping out elements (with a delay)
Fiddle - http://jsfiddle.net/zubynd0c/

How to get proper stack labels with 'datetime' axis?

First off, the fiddle to my question is here: http://jsfiddle.net/pfifas/aTh7F/
The problem
I want to display the sum of the values of all series at each point on the horizontal axis as stack labels above each stack. Because I have negative stacks and I do not want to display positive and negative totals above and beneath separately, I set up a formatter function manually (lines 42 through 50 in the fiddle). The stack labels that I want should be the sum over all values at a given point, i.e. the sum of the positive stacks minus the sum of all negative stacks. E.g. if the data series at the first point is
data: [1,-2,3,4]
I would like the stack label to display a "6" above the stacked bar. My formatter function works fine with data series of the sort
data: [1,-2,3,4]
But once I add a date variable to each series like
data: [
[Date.UTC(2010, 0, 1), 4],
[Date.UTC(2010, 3, 1), 15],
[Date.UTC(2010, 6, 1), 17],
[Date.UTC(2010, 9, 1), 10]
]
the stack labels are not displayed anymore. I think I am making a mistake with the indexing of the array in the formatter function. How do I have to change the indexing to achieve the correct result?
On a different note, where can I learn more about which variables are available inside the formatter environment and how to handle them in highcharts?
Edit:
In response to Onkloss answer, I should highlight that this issue is related to setting the axis to type 'datetime', not to whether the array of data is multidimensional (as the previous question title was suggesting).
The issue is the use of this.x here:
for (i = 0; i < 4; i++) {
sum += series[i].yData[this.x];
}
For "normal" x-axis types this works well since you can open the yData by index. In your case it will end up trying to find something like series[0].yData[1262304000000], which won't work.
I'm not aware of any easy way to find the relationship between the timestamp this.x and which index that has on the x-axis. My solution uses another for loop to compare the timestamp from this.x with the xData array, and if it matches we use the data in the sum, as shown in this updated formatter:
formatter: function () {
// If doing negative, ignore
if(this.total <= 0)
return;
var sum = 0;
var series = this.axis.series;
for (var i = 0; i < series.length; i++){
for(var j = 0; j < series[i].xData.length; j++) {
if(series[i].options.type == 'column' && series[i].xData[j] == this.x) {
sum += series[i].yData[j];
break;
}
}
}
return sum;
}
This updated JSFiddle shows it in action.
If I understand what you're asking for correctly, the following formatter function should work:
formatter: function () {
return this.total >= 0 ?this.total : null ;
},
http://jsfiddle.net/aTh7F/1/
As to the available options in the formatter function, http://api.highcharts.com/highcharts#plotOptions.column.dataLabels.formatter should help. But, I tend to just put a breakpoint in the function and inspect what this is. :)

Categories

Resources